Skip to content

Commit 6953ef8

Browse files
committed
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 <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Carlos Amedee <[email protected]>
1 parent 8ec5559 commit 6953ef8

24 files changed

+272
-148
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
On Windows, the [TCPConn.File], [UDPConn.File], [UnixConn.File],
2+
[IPConn.File], [TCPListener.File], and [UnixListener.File]
3+
methods are now supported.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
On Windows, the [FileConn], [FilePacketConn], [FileListener]
2+
functions are now supported.

src/internal/syscall/windows/net_windows.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ func WSASendtoInet4(s syscall.Handle, bufs *syscall.WSABuf, bufcnt uint32, sent
1818
func WSASendtoInet6(s syscall.Handle, bufs *syscall.WSABuf, bufcnt uint32, sent *uint32, flags uint32, to *syscall.SockaddrInet6, overlapped *syscall.Overlapped, croutine *byte) (err error)
1919

2020
const (
21+
SO_TYPE = 0x1008
2122
SIO_TCP_INITIAL_RTO = syscall.IOC_IN | syscall.IOC_VENDOR | 17
2223
TCP_INITIAL_RTO_UNSPECIFIED_RTT = ^uint16(0)
2324
TCP_INITIAL_RTO_NO_SYN_RETRANSMISSIONS = ^uint8(1)

src/internal/syscall/windows/syscall_windows.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ type WSAMsg struct {
268268
}
269269

270270
//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
271+
//sys WSADuplicateSocket(s syscall.Handle, processID uint32, info *syscall.WSAProtocolInfo) (err error) [failretval!=0] = ws2_32.WSADuplicateSocketW
271272
//sys WSAGetOverlappedResult(h syscall.Handle, o *syscall.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) = ws2_32.WSAGetOverlappedResult
272273

273274
func loadWSASendRecvMsg() error {

src/internal/syscall/windows/zsyscall_windows.go

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/net/error_test.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -736,11 +736,6 @@ third:
736736
}
737737

738738
func TestFileError(t *testing.T) {
739-
switch runtime.GOOS {
740-
case "windows":
741-
t.Skipf("not supported on %s", runtime.GOOS)
742-
}
743-
744739
f, err := os.CreateTemp("", "go-nettest")
745740
if err != nil {
746741
t.Fatal(err)

src/net/fd_posix.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ type netFD struct {
2626
raddr Addr
2727
}
2828

29+
func (fd *netFD) name() string {
30+
var ls, rs string
31+
if fd.laddr != nil {
32+
ls = fd.laddr.String()
33+
}
34+
if fd.raddr != nil {
35+
rs = fd.raddr.String()
36+
}
37+
return fd.net + ":" + ls + "->" + rs
38+
}
39+
2940
func (fd *netFD) setAddr(laddr, raddr Addr) {
3041
fd.laddr = laddr
3142
fd.raddr = raddr

src/net/fd_unix.go

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,6 @@ func (fd *netFD) init() error {
4141
return fd.pfd.Init(fd.net, true)
4242
}
4343

44-
func (fd *netFD) name() string {
45-
var ls, rs string
46-
if fd.laddr != nil {
47-
ls = fd.laddr.String()
48-
}
49-
if fd.raddr != nil {
50-
rs = fd.raddr.String()
51-
}
52-
return fd.net + ":" + ls + "->" + rs
53-
}
54-
5544
func (fd *netFD) connect(ctx context.Context, la, ra syscall.Sockaddr) (rsa syscall.Sockaddr, ret error) {
5645
// Do not need to call fd.writeLock here,
5746
// because fd is not yet accessible to user,

src/net/fd_windows.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -233,9 +233,23 @@ func (fd *netFD) accept() (*netFD, error) {
233233
return netfd, nil
234234
}
235235

236-
// Unimplemented functions.
237-
238236
func (fd *netFD) dup() (*os.File, error) {
239-
// TODO: Implement this, perhaps using internal/poll.DupCloseOnExec.
240-
return nil, syscall.EWINDOWS
237+
// Disassociate the IOCP from the socket,
238+
// it is not safe to share a duplicated handle
239+
// that is associated with IOCP.
240+
if err := fd.pfd.DisassociateIOCP(); err != nil {
241+
return nil, err
242+
}
243+
var h syscall.Handle
244+
var syserr error
245+
err := fd.pfd.RawControl(func(fd uintptr) {
246+
h, syserr = dupSocket(syscall.Handle(fd))
247+
})
248+
if err != nil {
249+
err = syserr
250+
}
251+
if err != nil {
252+
return nil, err
253+
}
254+
return os.NewFile(uintptr(h), fd.name()), nil
241255
}

src/net/file.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ package net
66

77
import "os"
88

9-
// BUG(mikio): On JS and Windows, the FileConn, FileListener and
9+
// BUG(mikio): On JS, the FileConn, FileListener and
1010
// FilePacketConn functions are not implemented.
1111

1212
type fileAddr string

src/net/file_posix.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build unix || windows
6+
7+
package net
8+
9+
import (
10+
"internal/poll"
11+
"os"
12+
"syscall"
13+
)
14+
15+
func newFileFD(f *os.File) (*netFD, error) {
16+
s, err := dupFileSocket(f)
17+
if err != nil {
18+
return nil, err
19+
}
20+
family := syscall.AF_UNSPEC
21+
sotype, err := syscall.GetsockoptInt(s, syscall.SOL_SOCKET, _SO_TYPE)
22+
if err != nil {
23+
poll.CloseFunc(s)
24+
return nil, os.NewSyscallError("getsockopt", err)
25+
}
26+
lsa, _ := syscall.Getsockname(s)
27+
rsa, _ := syscall.Getpeername(s)
28+
switch lsa.(type) {
29+
case *syscall.SockaddrInet4:
30+
family = syscall.AF_INET
31+
case *syscall.SockaddrInet6:
32+
family = syscall.AF_INET6
33+
case *syscall.SockaddrUnix:
34+
family = syscall.AF_UNIX
35+
default:
36+
poll.CloseFunc(s)
37+
return nil, syscall.EPROTONOSUPPORT
38+
}
39+
fd, err := newFD(s, family, sotype, "")
40+
if err != nil {
41+
poll.CloseFunc(s)
42+
return nil, err
43+
}
44+
laddr := fd.addrFunc()(lsa)
45+
raddr := fd.addrFunc()(rsa)
46+
fd.net = laddr.Network()
47+
if err := fd.init(); err != nil {
48+
fd.Close()
49+
return nil, err
50+
}
51+
fd.setAddr(laddr, raddr)
52+
return fd, nil
53+
}
54+
55+
func fileConn(f *os.File) (Conn, error) {
56+
fd, err := newFileFD(f)
57+
if err != nil {
58+
return nil, err
59+
}
60+
switch fd.laddr.(type) {
61+
case *TCPAddr:
62+
return newTCPConn(fd, defaultTCPKeepAliveIdle, KeepAliveConfig{}, testPreHookSetKeepAlive, testHookSetKeepAlive), nil
63+
case *UDPAddr:
64+
return newUDPConn(fd), nil
65+
case *IPAddr:
66+
return newIPConn(fd), nil
67+
case *UnixAddr:
68+
return newUnixConn(fd), nil
69+
}
70+
fd.Close()
71+
return nil, syscall.EINVAL
72+
}
73+
74+
func fileListener(f *os.File) (Listener, error) {
75+
fd, err := newFileFD(f)
76+
if err != nil {
77+
return nil, err
78+
}
79+
switch laddr := fd.laddr.(type) {
80+
case *TCPAddr:
81+
return &TCPListener{fd: fd}, nil
82+
case *UnixAddr:
83+
return &UnixListener{fd: fd, path: laddr.Name, unlink: false}, nil
84+
}
85+
fd.Close()
86+
return nil, syscall.EINVAL
87+
}
88+
89+
func filePacketConn(f *os.File) (PacketConn, error) {
90+
fd, err := newFileFD(f)
91+
if err != nil {
92+
return nil, err
93+
}
94+
switch fd.laddr.(type) {
95+
case *UDPAddr:
96+
return newUDPConn(fd), nil
97+
case *IPAddr:
98+
return newIPConn(fd), nil
99+
case *UnixAddr:
100+
return newUnixConn(fd), nil
101+
}
102+
fd.Close()
103+
return nil, syscall.EINVAL
104+
}

src/net/file_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ var fileConnTests = []struct {
2929

3030
func TestFileConn(t *testing.T) {
3131
switch runtime.GOOS {
32-
case "plan9", "windows", "js", "wasip1":
32+
case "plan9", "js", "wasip1":
3333
t.Skipf("not supported on %s", runtime.GOOS)
3434
}
3535

@@ -130,7 +130,7 @@ var fileListenerTests = []struct {
130130

131131
func TestFileListener(t *testing.T) {
132132
switch runtime.GOOS {
133-
case "plan9", "windows", "js", "wasip1":
133+
case "plan9", "js", "wasip1":
134134
t.Skipf("not supported on %s", runtime.GOOS)
135135
}
136136

@@ -222,7 +222,7 @@ var filePacketConnTests = []struct {
222222

223223
func TestFilePacketConn(t *testing.T) {
224224
switch runtime.GOOS {
225-
case "plan9", "windows", "js", "wasip1":
225+
case "plan9", "js", "wasip1":
226226
t.Skipf("not supported on %s", runtime.GOOS)
227227
}
228228

@@ -289,7 +289,7 @@ func TestFilePacketConn(t *testing.T) {
289289
// Issue 24483.
290290
func TestFileCloseRace(t *testing.T) {
291291
switch runtime.GOOS {
292-
case "plan9", "windows", "js", "wasip1":
292+
case "plan9", "js", "wasip1":
293293
t.Skipf("not supported on %s", runtime.GOOS)
294294
}
295295
if !testableNetwork("tcp") {

src/net/file_unix.go

Lines changed: 3 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import (
1212
"syscall"
1313
)
1414

15-
func dupSocket(f *os.File) (int, error) {
15+
const _SO_TYPE = syscall.SO_TYPE
16+
17+
func dupFileSocket(f *os.File) (int, error) {
1618
s, call, err := poll.DupCloseOnExec(int(f.Fd()))
1719
if err != nil {
1820
if call != "" {
@@ -26,94 +28,3 @@ func dupSocket(f *os.File) (int, error) {
2628
}
2729
return s, nil
2830
}
29-
30-
func newFileFD(f *os.File) (*netFD, error) {
31-
s, err := dupSocket(f)
32-
if err != nil {
33-
return nil, err
34-
}
35-
family := syscall.AF_UNSPEC
36-
sotype, err := syscall.GetsockoptInt(s, syscall.SOL_SOCKET, syscall.SO_TYPE)
37-
if err != nil {
38-
poll.CloseFunc(s)
39-
return nil, os.NewSyscallError("getsockopt", err)
40-
}
41-
lsa, _ := syscall.Getsockname(s)
42-
rsa, _ := syscall.Getpeername(s)
43-
switch lsa.(type) {
44-
case *syscall.SockaddrInet4:
45-
family = syscall.AF_INET
46-
case *syscall.SockaddrInet6:
47-
family = syscall.AF_INET6
48-
case *syscall.SockaddrUnix:
49-
family = syscall.AF_UNIX
50-
default:
51-
poll.CloseFunc(s)
52-
return nil, syscall.EPROTONOSUPPORT
53-
}
54-
fd, err := newFD(s, family, sotype, "")
55-
if err != nil {
56-
poll.CloseFunc(s)
57-
return nil, err
58-
}
59-
laddr := fd.addrFunc()(lsa)
60-
raddr := fd.addrFunc()(rsa)
61-
fd.net = laddr.Network()
62-
if err := fd.init(); err != nil {
63-
fd.Close()
64-
return nil, err
65-
}
66-
fd.setAddr(laddr, raddr)
67-
return fd, nil
68-
}
69-
70-
func fileConn(f *os.File) (Conn, error) {
71-
fd, err := newFileFD(f)
72-
if err != nil {
73-
return nil, err
74-
}
75-
switch fd.laddr.(type) {
76-
case *TCPAddr:
77-
return newTCPConn(fd, defaultTCPKeepAliveIdle, KeepAliveConfig{}, testPreHookSetKeepAlive, testHookSetKeepAlive), nil
78-
case *UDPAddr:
79-
return newUDPConn(fd), nil
80-
case *IPAddr:
81-
return newIPConn(fd), nil
82-
case *UnixAddr:
83-
return newUnixConn(fd), nil
84-
}
85-
fd.Close()
86-
return nil, syscall.EINVAL
87-
}
88-
89-
func fileListener(f *os.File) (Listener, error) {
90-
fd, err := newFileFD(f)
91-
if err != nil {
92-
return nil, err
93-
}
94-
switch laddr := fd.laddr.(type) {
95-
case *TCPAddr:
96-
return &TCPListener{fd: fd}, nil
97-
case *UnixAddr:
98-
return &UnixListener{fd: fd, path: laddr.Name, unlink: false}, nil
99-
}
100-
fd.Close()
101-
return nil, syscall.EINVAL
102-
}
103-
104-
func filePacketConn(f *os.File) (PacketConn, error) {
105-
fd, err := newFileFD(f)
106-
if err != nil {
107-
return nil, err
108-
}
109-
switch fd.laddr.(type) {
110-
case *UDPAddr:
111-
return newUDPConn(fd), nil
112-
case *IPAddr:
113-
return newIPConn(fd), nil
114-
case *UnixAddr:
115-
return newUnixConn(fd), nil
116-
}
117-
fd.Close()
118-
return nil, syscall.EINVAL
119-
}

0 commit comments

Comments
 (0)