net,os: support converting between *os.File and net.Conn on Windows

The runtime poller and os.NewFile recently gained support for
disassociating the handle from the runtime poller IOCP (see CL 664455).
This was the main blocker for allowing the conversion between *os.File
and net.Conn.

Implementing the conversion is now trivial. The only remaining work,
implemented in this CL, is improving os.NewFile to also support
socket handles and updating some build tags so that Windows can share
almost the same net's File implementation as Unix.

There is one important limitation, though: the duplicated socket handle
returned by the various File methods in the net package is not
usable on other process. If someone needs to pass a socket handle to
another process, they should manually call the WSADuplicateSocket
Windows API passing the process ID of the target process.

Fixes #9503.
Fixes #10350.
Updates #19098.

Cq-Include-Trybots: luci.golang.try:gotip-windows-amd64-race,gotip-windows-amd64-longtest,gotip-windows-arm64
Change-Id: Ic43cadaac2662b925d57a9d362ddc7ae21d1b56e
Reviewed-on: https://go-review.googlesource.com/c/go/+/668195
Reviewed-by: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Carlos Amedee <carlos@golang.org>
This commit is contained in:
qmuntal 2025-04-25 14:11:31 +02:00 committed by Quim Muntal
parent 8ec555931d
commit 6953ef86cd
24 changed files with 273 additions and 149 deletions

View File

@ -0,0 +1,3 @@
On Windows, the [TCPConn.File], [UDPConn.File], [UnixConn.File],
[IPConn.File], [TCPListener.File], and [UnixListener.File]
methods are now supported.

View File

@ -0,0 +1,2 @@
On Windows, the [FileConn], [FilePacketConn], [FileListener]
functions are now supported.

View File

@ -18,6 +18,7 @@ func WSASendtoInet4(s syscall.Handle, bufs *syscall.WSABuf, bufcnt uint32, sent
func WSASendtoInet6(s syscall.Handle, bufs *syscall.WSABuf, bufcnt uint32, sent *uint32, flags uint32, to *syscall.SockaddrInet6, overlapped *syscall.Overlapped, croutine *byte) (err error)
const (
SO_TYPE = 0x1008
SIO_TCP_INITIAL_RTO = syscall.IOC_IN | syscall.IOC_VENDOR | 17
TCP_INITIAL_RTO_UNSPECIFIED_RTT = ^uint16(0)
TCP_INITIAL_RTO_NO_SYN_RETRANSMISSIONS = ^uint8(1)

View File

@ -268,6 +268,7 @@ type WSAMsg struct {
}
//sys WSASocket(af int32, typ int32, protocol int32, protinfo *syscall.WSAProtocolInfo, group uint32, flags uint32) (handle syscall.Handle, err error) [failretval==syscall.InvalidHandle] = ws2_32.WSASocketW
//sys WSADuplicateSocket(s syscall.Handle, processID uint32, info *syscall.WSAProtocolInfo) (err error) [failretval!=0] = ws2_32.WSADuplicateSocketW
//sys WSAGetOverlappedResult(h syscall.Handle, o *syscall.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) = ws2_32.WSAGetOverlappedResult
func loadWSASendRecvMsg() error {

View File

@ -106,6 +106,7 @@ var (
procCreateEnvironmentBlock = moduserenv.NewProc("CreateEnvironmentBlock")
procDestroyEnvironmentBlock = moduserenv.NewProc("DestroyEnvironmentBlock")
procGetProfilesDirectoryW = moduserenv.NewProc("GetProfilesDirectoryW")
procWSADuplicateSocketW = modws2_32.NewProc("WSADuplicateSocketW")
procWSAGetOverlappedResult = modws2_32.NewProc("WSAGetOverlappedResult")
procWSASocketW = modws2_32.NewProc("WSASocketW")
)
@ -591,6 +592,14 @@ func GetProfilesDirectory(dir *uint16, dirLen *uint32) (err error) {
return
}
func WSADuplicateSocket(s syscall.Handle, processID uint32, info *syscall.WSAProtocolInfo) (err error) {
r1, _, e1 := syscall.Syscall(procWSADuplicateSocketW.Addr(), 3, uintptr(s), uintptr(processID), uintptr(unsafe.Pointer(info)))
if r1 != 0 {
err = errnoErr(e1)
}
return
}
func WSAGetOverlappedResult(h syscall.Handle, o *syscall.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) {
var _p0 uint32
if wait {

View File

@ -736,11 +736,6 @@ third:
}
func TestFileError(t *testing.T) {
switch runtime.GOOS {
case "windows":
t.Skipf("not supported on %s", runtime.GOOS)
}
f, err := os.CreateTemp("", "go-nettest")
if err != nil {
t.Fatal(err)

View File

@ -26,6 +26,17 @@ type netFD struct {
raddr Addr
}
func (fd *netFD) name() string {
var ls, rs string
if fd.laddr != nil {
ls = fd.laddr.String()
}
if fd.raddr != nil {
rs = fd.raddr.String()
}
return fd.net + ":" + ls + "->" + rs
}
func (fd *netFD) setAddr(laddr, raddr Addr) {
fd.laddr = laddr
fd.raddr = raddr

View File

@ -41,17 +41,6 @@ func (fd *netFD) init() error {
return fd.pfd.Init(fd.net, true)
}
func (fd *netFD) name() string {
var ls, rs string
if fd.laddr != nil {
ls = fd.laddr.String()
}
if fd.raddr != nil {
rs = fd.raddr.String()
}
return fd.net + ":" + ls + "->" + rs
}
func (fd *netFD) connect(ctx context.Context, la, ra syscall.Sockaddr) (rsa syscall.Sockaddr, ret error) {
// Do not need to call fd.writeLock here,
// because fd is not yet accessible to user,

View File

@ -233,9 +233,23 @@ func (fd *netFD) accept() (*netFD, error) {
return netfd, nil
}
// Unimplemented functions.
func (fd *netFD) dup() (*os.File, error) {
// TODO: Implement this, perhaps using internal/poll.DupCloseOnExec.
return nil, syscall.EWINDOWS
// Disassociate the IOCP from the socket,
// it is not safe to share a duplicated handle
// that is associated with IOCP.
if err := fd.pfd.DisassociateIOCP(); err != nil {
return nil, err
}
var h syscall.Handle
var syserr error
err := fd.pfd.RawControl(func(fd uintptr) {
h, syserr = dupSocket(syscall.Handle(fd))
})
if err != nil {
err = syserr
}
if err != nil {
return nil, err
}
return os.NewFile(uintptr(h), fd.name()), nil
}

View File

@ -6,7 +6,7 @@ package net
import "os"
// BUG(mikio): On JS and Windows, the FileConn, FileListener and
// BUG(mikio): On JS, the FileConn, FileListener and
// FilePacketConn functions are not implemented.
type fileAddr string

104
src/net/file_posix.go Normal file
View File

@ -0,0 +1,104 @@
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build unix || windows
package net
import (
"internal/poll"
"os"
"syscall"
)
func newFileFD(f *os.File) (*netFD, error) {
s, err := dupFileSocket(f)
if err != nil {
return nil, err
}
family := syscall.AF_UNSPEC
sotype, err := syscall.GetsockoptInt(s, syscall.SOL_SOCKET, _SO_TYPE)
if err != nil {
poll.CloseFunc(s)
return nil, os.NewSyscallError("getsockopt", err)
}
lsa, _ := syscall.Getsockname(s)
rsa, _ := syscall.Getpeername(s)
switch lsa.(type) {
case *syscall.SockaddrInet4:
family = syscall.AF_INET
case *syscall.SockaddrInet6:
family = syscall.AF_INET6
case *syscall.SockaddrUnix:
family = syscall.AF_UNIX
default:
poll.CloseFunc(s)
return nil, syscall.EPROTONOSUPPORT
}
fd, err := newFD(s, family, sotype, "")
if err != nil {
poll.CloseFunc(s)
return nil, err
}
laddr := fd.addrFunc()(lsa)
raddr := fd.addrFunc()(rsa)
fd.net = laddr.Network()
if err := fd.init(); err != nil {
fd.Close()
return nil, err
}
fd.setAddr(laddr, raddr)
return fd, nil
}
func fileConn(f *os.File) (Conn, error) {
fd, err := newFileFD(f)
if err != nil {
return nil, err
}
switch fd.laddr.(type) {
case *TCPAddr:
return newTCPConn(fd, defaultTCPKeepAliveIdle, KeepAliveConfig{}, testPreHookSetKeepAlive, testHookSetKeepAlive), nil
case *UDPAddr:
return newUDPConn(fd), nil
case *IPAddr:
return newIPConn(fd), nil
case *UnixAddr:
return newUnixConn(fd), nil
}
fd.Close()
return nil, syscall.EINVAL
}
func fileListener(f *os.File) (Listener, error) {
fd, err := newFileFD(f)
if err != nil {
return nil, err
}
switch laddr := fd.laddr.(type) {
case *TCPAddr:
return &TCPListener{fd: fd}, nil
case *UnixAddr:
return &UnixListener{fd: fd, path: laddr.Name, unlink: false}, nil
}
fd.Close()
return nil, syscall.EINVAL
}
func filePacketConn(f *os.File) (PacketConn, error) {
fd, err := newFileFD(f)
if err != nil {
return nil, err
}
switch fd.laddr.(type) {
case *UDPAddr:
return newUDPConn(fd), nil
case *IPAddr:
return newIPConn(fd), nil
case *UnixAddr:
return newUnixConn(fd), nil
}
fd.Close()
return nil, syscall.EINVAL
}

View File

@ -29,7 +29,7 @@ var fileConnTests = []struct {
func TestFileConn(t *testing.T) {
switch runtime.GOOS {
case "plan9", "windows", "js", "wasip1":
case "plan9", "js", "wasip1":
t.Skipf("not supported on %s", runtime.GOOS)
}
@ -130,7 +130,7 @@ var fileListenerTests = []struct {
func TestFileListener(t *testing.T) {
switch runtime.GOOS {
case "plan9", "windows", "js", "wasip1":
case "plan9", "js", "wasip1":
t.Skipf("not supported on %s", runtime.GOOS)
}
@ -222,7 +222,7 @@ var filePacketConnTests = []struct {
func TestFilePacketConn(t *testing.T) {
switch runtime.GOOS {
case "plan9", "windows", "js", "wasip1":
case "plan9", "js", "wasip1":
t.Skipf("not supported on %s", runtime.GOOS)
}
@ -289,7 +289,7 @@ func TestFilePacketConn(t *testing.T) {
// Issue 24483.
func TestFileCloseRace(t *testing.T) {
switch runtime.GOOS {
case "plan9", "windows", "js", "wasip1":
case "plan9", "js", "wasip1":
t.Skipf("not supported on %s", runtime.GOOS)
}
if !testableNetwork("tcp") {

View File

@ -12,7 +12,9 @@ import (
"syscall"
)
func dupSocket(f *os.File) (int, error) {
const _SO_TYPE = syscall.SO_TYPE
func dupFileSocket(f *os.File) (int, error) {
s, call, err := poll.DupCloseOnExec(int(f.Fd()))
if err != nil {
if call != "" {
@ -26,94 +28,3 @@ func dupSocket(f *os.File) (int, error) {
}
return s, nil
}
func newFileFD(f *os.File) (*netFD, error) {
s, err := dupSocket(f)
if err != nil {
return nil, err
}
family := syscall.AF_UNSPEC
sotype, err := syscall.GetsockoptInt(s, syscall.SOL_SOCKET, syscall.SO_TYPE)
if err != nil {
poll.CloseFunc(s)
return nil, os.NewSyscallError("getsockopt", err)
}
lsa, _ := syscall.Getsockname(s)
rsa, _ := syscall.Getpeername(s)
switch lsa.(type) {
case *syscall.SockaddrInet4:
family = syscall.AF_INET
case *syscall.SockaddrInet6:
family = syscall.AF_INET6
case *syscall.SockaddrUnix:
family = syscall.AF_UNIX
default:
poll.CloseFunc(s)
return nil, syscall.EPROTONOSUPPORT
}
fd, err := newFD(s, family, sotype, "")
if err != nil {
poll.CloseFunc(s)
return nil, err
}
laddr := fd.addrFunc()(lsa)
raddr := fd.addrFunc()(rsa)
fd.net = laddr.Network()
if err := fd.init(); err != nil {
fd.Close()
return nil, err
}
fd.setAddr(laddr, raddr)
return fd, nil
}
func fileConn(f *os.File) (Conn, error) {
fd, err := newFileFD(f)
if err != nil {
return nil, err
}
switch fd.laddr.(type) {
case *TCPAddr:
return newTCPConn(fd, defaultTCPKeepAliveIdle, KeepAliveConfig{}, testPreHookSetKeepAlive, testHookSetKeepAlive), nil
case *UDPAddr:
return newUDPConn(fd), nil
case *IPAddr:
return newIPConn(fd), nil
case *UnixAddr:
return newUnixConn(fd), nil
}
fd.Close()
return nil, syscall.EINVAL
}
func fileListener(f *os.File) (Listener, error) {
fd, err := newFileFD(f)
if err != nil {
return nil, err
}
switch laddr := fd.laddr.(type) {
case *TCPAddr:
return &TCPListener{fd: fd}, nil
case *UnixAddr:
return &UnixListener{fd: fd, path: laddr.Name, unlink: false}, nil
}
fd.Close()
return nil, syscall.EINVAL
}
func filePacketConn(f *os.File) (PacketConn, error) {
fd, err := newFileFD(f)
if err != nil {
return nil, err
}
switch fd.laddr.(type) {
case *UDPAddr:
return newUDPConn(fd), nil
case *IPAddr:
return newIPConn(fd), nil
case *UnixAddr:
return newUnixConn(fd), nil
}
fd.Close()
return nil, syscall.EINVAL
}

View File

@ -5,21 +5,28 @@
package net
import (
"internal/syscall/windows"
"os"
"syscall"
)
func fileConn(f *os.File) (Conn, error) {
// TODO: Implement this
return nil, syscall.EWINDOWS
const _SO_TYPE = windows.SO_TYPE
func dupSocket(h syscall.Handle) (syscall.Handle, error) {
var info syscall.WSAProtocolInfo
err := windows.WSADuplicateSocket(h, uint32(syscall.Getpid()), &info)
if err != nil {
return 0, err
}
return windows.WSASocket(-1, -1, -1, &info, 0, windows.WSA_FLAG_OVERLAPPED|windows.WSA_FLAG_NO_HANDLE_INHERIT)
}
func fileListener(f *os.File) (Listener, error) {
// TODO: Implement this
return nil, syscall.EWINDOWS
}
func filePacketConn(f *os.File) (PacketConn, error) {
// TODO: Implement this
return nil, syscall.EWINDOWS
func dupFileSocket(f *os.File) (syscall.Handle, error) {
// The resulting handle should not be associated to an IOCP, else the IO operations
// will block an OS thread, and that's not what net package users expect.
h, err := dupSocket(syscall.Handle(f.Fd()))
if err != nil {
return 0, err
}
return h, nil
}

View File

@ -4,6 +4,8 @@
package net
import "os/exec"
func installTestHooks() {}
func uninstallTestHooks() {}
@ -14,3 +16,5 @@ func forceCloseSockets() {}
func enableSocketConnect() {}
func disableSocketConnect(network string) {}
func addCmdInheritedHandle(cmd *exec.Cmd, fd uintptr) {}

View File

@ -6,7 +6,10 @@
package net
import "internal/poll"
import (
"internal/poll"
"os/exec"
)
var (
// Placeholders for saving original socket system calls.
@ -53,3 +56,5 @@ func forceCloseSockets() {
poll.CloseFunc(s)
}
}
func addCmdInheritedHandle(cmd *exec.Cmd, fd uintptr) {}

View File

@ -6,8 +6,12 @@
package net
import "os/exec"
func installTestHooks() {}
func uninstallTestHooks() {}
func forceCloseSockets() {}
func addCmdInheritedHandle(cmd *exec.Cmd, fd uintptr) {}

View File

@ -4,7 +4,11 @@
package net
import "internal/poll"
import (
"internal/poll"
"os/exec"
"syscall"
)
var (
// Placeholders for saving original socket system calls.
@ -40,3 +44,14 @@ func forceCloseSockets() {
poll.CloseFunc(s)
}
}
func addCmdInheritedHandle(cmd *exec.Cmd, fd uintptr) {
// Inherited handles are not inherited by default in Windows.
// We need to set the handle inheritance flag explicitly.
// See https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessa#parameters
// for more details.
if cmd.SysProcAttr == nil {
cmd.SysProcAttr = &syscall.SysProcAttr{}
}
cmd.SysProcAttr.AdditionalInheritedHandles = append(cmd.SysProcAttr.AdditionalInheritedHandles, syscall.Handle(fd))
}

View File

@ -509,6 +509,10 @@ func packetTransceiver(c PacketConn, wb []byte, dst Addr, ch chan<- error) {
func spawnTestSocketPair(t testing.TB, net string) (client, server Conn) {
t.Helper()
if !testableNetwork(net) {
t.Skipf("network %q not supported", net)
}
ln := newLocalListener(t, net)
defer ln.Close()
var cerr, serr error
@ -536,13 +540,6 @@ func spawnTestSocketPair(t testing.TB, net string) (client, server Conn) {
func startTestSocketPeer(t testing.TB, conn Conn, op string, chunkSize, totalSize int) (func(t testing.TB), error) {
t.Helper()
if runtime.GOOS == "windows" {
// TODO(panjf2000): Windows has not yet implemented FileConn,
// remove this when it's implemented in https://go.dev/issues/9503.
t.Fatalf("startTestSocketPeer is not supported on %s", runtime.GOOS)
}
f, err := conn.(interface{ File() (*os.File, error) }).File()
if err != nil {
return nil, err
@ -556,7 +553,14 @@ func startTestSocketPeer(t testing.TB, conn Conn, op string, chunkSize, totalSiz
"GO_NET_TEST_TRANSFER_TOTAL_SIZE=" + strconv.Itoa(totalSize),
"TMPDIR=" + os.Getenv("TMPDIR"),
}
cmd.ExtraFiles = append(cmd.ExtraFiles, f)
if runtime.GOOS == "windows" {
// Windows doesn't support ExtraFiles
fd := f.Fd()
cmd.Env = append(cmd.Env, "GO_NET_TEST_TRANSFER_FD="+strconv.FormatUint(uint64(fd), 10))
addCmdInheritedHandle(cmd, fd)
} else {
cmd.ExtraFiles = append(cmd.ExtraFiles, f)
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
@ -586,7 +590,17 @@ func init() {
}
defer os.Exit(0)
f := os.NewFile(uintptr(3), "splice-test-conn")
var fd uintptr
if runtime.GOOS == "windows" {
v, err := strconv.ParseUint(os.Getenv("GO_NET_TEST_TRANSFER_FD"), 10, 0)
if err != nil {
log.Fatal(err)
}
fd = uintptr(v)
} else {
fd = uintptr(3)
}
f := os.NewFile(fd, "splice-test-conn")
defer f.Close()
conn, err := FileConn(f)

View File

@ -308,6 +308,9 @@ func (c *conn) SetWriteBuffer(bytes int) error {
// The returned os.File's file descriptor is different from the connection's.
// Attempting to change properties of the original using this duplicate
// may or may not have the desired effect.
//
// On Windows, the returned os.File's file descriptor is not usable
// on other processes.
func (c *conn) File() (f *os.File, err error) {
f, err = c.fd.dup()
if err != nil {

View File

@ -417,6 +417,9 @@ func (l *TCPListener) SetDeadline(t time.Time) error {
// The returned os.File's file descriptor is different from the
// connection's. Attempting to change properties of the original
// using this duplicate may or may not have the desired effect.
//
// On Windows, the returned os.File's file descriptor is not
// usable on other processes.
func (l *TCPListener) File() (f *os.File, err error) {
if !l.ok() {
return nil, syscall.EINVAL

View File

@ -297,6 +297,9 @@ func (l *UnixListener) SetDeadline(t time.Time) error {
// The returned [os.File]'s file descriptor is different from the
// connection's. Attempting to change properties of the original
// using this duplicate may or may not have the desired effect.
//
// On Windows, the returned os.File's file descriptor is not
// usable on other processes.
func (l *UnixListener) File() (f *os.File, err error) {
if !l.ok() {
return nil, syscall.EINVAL

View File

@ -398,9 +398,6 @@ func TestUnixUnlink(t *testing.T) {
// FileListener should not.
t.Run("FileListener", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping: FileListener not implemented on windows")
}
l := listen(t)
f, _ := l.File()
l1, _ := FileListener(f)
@ -448,9 +445,6 @@ func TestUnixUnlink(t *testing.T) {
})
t.Run("FileListener/SetUnlinkOnClose(true)", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping: FileListener not implemented on windows")
}
l := listen(t)
f, _ := l.File()
l1, _ := FileListener(f)
@ -464,9 +458,6 @@ func TestUnixUnlink(t *testing.T) {
})
t.Run("FileListener/SetUnlinkOnClose(false)", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping: FileListener not implemented on windows")
}
l := listen(t)
f, _ := l.File()
l1, _ := FileListener(f)

View File

@ -82,14 +82,49 @@ func newConsoleFile(h syscall.Handle, name string) *File {
return newFile(h, name, "console", false)
}
var wsaLoaded atomic.Bool
// isWSALoaded returns true if the ws2_32.dll module is loaded.
func isWSALoaded() bool {
// ws2_32.dll may be delay loaded, we can only short-circuit
// if we know it is loaded.
if wsaLoaded.Load() {
return true
}
var ws2_32_dll = [...]uint16{'w', 's', '2', '_', '3', '2', '.', 'd', 'l', 'l', 0}
_, err := windows.GetModuleHandle(unsafe.SliceData(ws2_32_dll[:]))
wsaLoaded.Store(err == nil)
return err == nil
}
// newFileFromNewFile is called by [NewFile].
func newFileFromNewFile(fd uintptr, name string) *File {
h := syscall.Handle(fd)
if h == syscall.InvalidHandle {
return nil
}
kind := "file"
var sotype int
if t, err := syscall.GetFileType(h); err == nil && t == syscall.FILE_TYPE_PIPE {
kind = "pipe"
// Windows reports sockets as FILE_TYPE_PIPE.
// We need to call getsockopt and check the socket type to distinguish between sockets and pipes.
// If the call fails, we assume it's a pipe.
// Avoid calling getsockopt if the WSA module is not loaded, it is a heavy dependency
// and sockets can only be created using that module.
if isWSALoaded() {
if sotype, err = syscall.GetsockoptInt(h, syscall.SOL_SOCKET, windows.SO_TYPE); err == nil {
kind = "net"
}
}
}
nonBlocking, _ := windows.IsNonblock(syscall.Handle(fd))
return newFile(h, name, "file", nonBlocking)
f := newFile(h, name, kind, nonBlocking)
if kind == "net" {
f.pfd.IsStream = sotype == syscall.SOCK_STREAM
f.pfd.ZeroReadIsEOF = sotype != syscall.SOCK_DGRAM && sotype != syscall.SOCK_RAW
}
return f
}
func epipecheck(file *File, e error) {