From 7bf5e5281e41b52cb8d0ab771e772de2f587284c Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Tue, 1 Sep 2026 17:46:13 -0500 Subject: [PATCH 1/2] net: report the kernel-assigned port after listening on port 0 Listening on ":0" asks the kernel for an ephemeral port, but the listener kept the requested address, so ln.Addr() reported port 0 and the common pattern of binding an ephemeral port and dialing ln.Addr() (used by httptest.NewServer among others) could not work. Add GetSockname to the host netdev and look it up from listenTCP through an optional interface, so netdevs that cannot report the bound address are unaffected and keep the previous behavior. UDP does not have this problem: ListenUDP already fills in a port of its own before binding. --- netdev_native.go | 25 +++++++++++++++++++++---- tcpsock.go | 13 +++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/netdev_native.go b/netdev_native.go index e522a08..4556d6a 100644 --- a/netdev_native.go +++ b/netdev_native.go @@ -137,14 +137,31 @@ func (*hostNetdev) Accept(sockfd int) (int, netip.AddrPort, error) { if err != nil { return -1, netip.AddrPort{}, err } - var raddr netip.AddrPort + return nfd, addrPortFromSockaddr(sa), nil +} + +// addrPortFromSockaddr converts a syscall.Sockaddr, as returned by +// Accept/Getsockname, into a netip.AddrPort. +func addrPortFromSockaddr(sa syscall.Sockaddr) netip.AddrPort { switch s := sa.(type) { case *syscall.SockaddrInet4: - raddr = netip.AddrPortFrom(netip.AddrFrom4(s.Addr), uint16(s.Port)) + return netip.AddrPortFrom(netip.AddrFrom4(s.Addr), uint16(s.Port)) case *syscall.SockaddrInet6: - raddr = netip.AddrPortFrom(netip.AddrFrom16(s.Addr), uint16(s.Port)) + return netip.AddrPortFrom(netip.AddrFrom16(s.Addr), uint16(s.Port)) + } + return netip.AddrPort{} +} + +// GetSockname reports the socket's actual local address — in particular the +// port the kernel assigned when binding port 0. The net package looks this up +// through an optional interface, so netdevs that cannot provide it simply +// don't implement it. +func (*hostNetdev) GetSockname(sockfd int) (netip.AddrPort, error) { + sa, err := syscall.Getsockname(sockfd) + if err != nil { + return netip.AddrPort{}, err } - return nfd, raddr, nil + return addrPortFromSockaddr(sa), nil } func (*hostNetdev) Send(sockfd int, buf []byte, flags int, deadline time.Time) (int, error) { diff --git a/tcpsock.go b/tcpsock.go index 4e3c864..c3db04b 100644 --- a/tcpsock.go +++ b/tcpsock.go @@ -395,6 +395,19 @@ func listenTCP(laddr *TCPAddr) (Listener, error) { return nil, err } + if laddr.Port == 0 { + // Binding port 0 asks for an ephemeral port; report the port that was + // actually assigned, like the standard library, so callers can dial + // ln.Addr(). Netdevs that cannot report it keep port 0. + if g, ok := netdev.(interface { + GetSockname(sockfd int) (netip.AddrPort, error) + }); ok { + if ap, err := g.GetSockname(fd); err == nil && ap.Port() != 0 { + laddr = &TCPAddr{IP: laddr.IP, Port: int(ap.Port())} + } + } + } + return &listener{fd: fd, laddr: laddr}, nil } From 7b7aacb2692382befc58283c098facaf492561f8 Mon Sep 17 00:00:00 2001 From: yohimik Date: Sat, 5 Sep 2026 06:43:12 +0400 Subject: [PATCH 2/2] net: preserve listener zone when applying the bound port (cherry picked from commit 0f460803c832e5edad095ce02731f1000496dd88) --- ephemeral_darwin_test.go | 18 ++++++++++ ephemeral_native_test.go | 78 ++++++++++++++++++++++++++++++++++++++++ netdev_native.go | 6 ++-- tcpsock.go | 7 ++-- 4 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 ephemeral_darwin_test.go create mode 100644 ephemeral_native_test.go diff --git a/ephemeral_darwin_test.go b/ephemeral_darwin_test.go new file mode 100644 index 0000000..d987546 --- /dev/null +++ b/ephemeral_darwin_test.go @@ -0,0 +1,18 @@ +//go:build darwin && !baremetal && !tinygo.wasm + +package net + +import ( + "syscall" + "testing" +) + +func TestEphemeralAcceptedNoSigpipe(t *testing.T) { + client, server := ephemeralTestPair(t) + defer client.Close() + defer server.Close() + value, err := syscall.GetsockoptInt(server.(*TCPConn).fd, syscall.SOL_SOCKET, syscall.SO_NOSIGPIPE) + if err != nil || value != 1 { + t.Fatalf("accepted SO_NOSIGPIPE=%d error=%v", value, err) + } +} diff --git a/ephemeral_native_test.go b/ephemeral_native_test.go new file mode 100644 index 0000000..eb3a7bf --- /dev/null +++ b/ephemeral_native_test.go @@ -0,0 +1,78 @@ +//go:build (linux || darwin) && !baremetal && !tinygo.wasm + +package net + +import ( + "net/netip" + "testing" + "time" +) + +func ephemeralTestPair(t *testing.T) (Conn, Conn) { + t.Helper() + l, err := Listen("tcp4", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer l.Close() + if l.Addr().(*TCPAddr).Port == 0 { + t.Fatalf("listener reports port 0: %v", l.Addr()) + } + client, err := Dial("tcp4", l.Addr().String()) + if err != nil { + t.Fatal(err) + } + server, err := l.Accept() + if err != nil { + client.Close() + t.Fatal(err) + } + return client, server +} + +func TestEphemeralListenDial(t *testing.T) { + client, server := ephemeralTestPair(t) + defer client.Close() + defer server.Close() + server.SetReadDeadline(time.Now().Add(time.Second)) + if _, err := client.Write([]byte("x")); err != nil { + t.Fatal(err) + } + b := make([]byte, 1) + if n, err := server.Read(b); n != 1 || err != nil || b[0] != 'x' { + t.Fatalf("read=%d %q error=%v", n, b, err) + } +} + +// ephemeralTestNetdev lets listenTCP succeed without a kernel socket, and +// reports a fixed bound address so the applied port and zone are predictable. +type ephemeralTestNetdev struct { + nopNetdev +} + +func (*ephemeralTestNetdev) Socket(int, int, int) (int, error) { return 42, nil } +func (*ephemeralTestNetdev) Bind(int, netip.AddrPort) error { return nil } +func (*ephemeralTestNetdev) Listen(int, int) error { return nil } +func (*ephemeralTestNetdev) Close(int) error { return nil } +func (*ephemeralTestNetdev) GetSockname(int) (netip.AddrPort, error) { + return netip.MustParseAddrPort("[fe80::1]:12345"), nil +} + +func TestEphemeralPreservesZone(t *testing.T) { + previous := netdev + defer func() { netdev = previous }() + netdev = &ephemeralTestNetdev{} + addr := &TCPAddr{IP: ParseIP("fe80::1"), Zone: "test-zone"} + l, err := listenTCP(addr) + if err != nil { + t.Fatal(err) + } + defer l.Close() + got := l.Addr().(*TCPAddr) + if got.Port != 12345 || got.Zone != addr.Zone || !got.IP.Equal(addr.IP) { + t.Fatalf("bound address=%v, want [%s%%%s]:12345", got, addr.IP, addr.Zone) + } + if addr.Port != 0 { + t.Fatalf("input address changed: %v", addr) + } +} diff --git a/netdev_native.go b/netdev_native.go index 4556d6a..ccec323 100644 --- a/netdev_native.go +++ b/netdev_native.go @@ -152,10 +152,8 @@ func addrPortFromSockaddr(sa syscall.Sockaddr) netip.AddrPort { return netip.AddrPort{} } -// GetSockname reports the socket's actual local address — in particular the -// port the kernel assigned when binding port 0. The net package looks this up -// through an optional interface, so netdevs that cannot provide it simply -// don't implement it. +// GetSockname reports the bound address. +// See https://pkg.go.dev/syscall#Getsockname. func (*hostNetdev) GetSockname(sockfd int) (netip.AddrPort, error) { sa, err := syscall.Getsockname(sockfd) if err != nil { diff --git a/tcpsock.go b/tcpsock.go index c3db04b..8e04194 100644 --- a/tcpsock.go +++ b/tcpsock.go @@ -396,14 +396,13 @@ func listenTCP(laddr *TCPAddr) (Listener, error) { } if laddr.Port == 0 { - // Binding port 0 asks for an ephemeral port; report the port that was - // actually assigned, like the standard library, so callers can dial - // ln.Addr(). Netdevs that cannot report it keep port 0. + // Report the selected port. See https://pkg.go.dev/net#Listen. + // Drivers without GetSockname keep the requested address. if g, ok := netdev.(interface { GetSockname(sockfd int) (netip.AddrPort, error) }); ok { if ap, err := g.GetSockname(fd); err == nil && ap.Port() != 0 { - laddr = &TCPAddr{IP: laddr.IP, Port: int(ap.Port())} + laddr = &TCPAddr{IP: laddr.IP, Port: int(ap.Port()), Zone: laddr.Zone} } } }