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 e522a08..ccec323 100644 --- a/netdev_native.go +++ b/netdev_native.go @@ -137,14 +137,29 @@ 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 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 { + 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..8e04194 100644 --- a/tcpsock.go +++ b/tcpsock.go @@ -395,6 +395,18 @@ func listenTCP(laddr *TCPAddr) (Listener, error) { return nil, err } + if laddr.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()), Zone: laddr.Zone} + } + } + } + return &listener{fd: fd, laddr: laddr}, nil }