From 25c11e2d2b808ff62ec0766b4345f1966ff2fd96 Mon Sep 17 00:00:00 2001 From: Maksim Varentsov Date: Wed, 26 Aug 2026 23:05:05 +0200 Subject: [PATCH] Tunnel inbound: SO_REUSEPORT fix and IPv6 support for the OpenBSD transparent proxy (#6624) Completes https://github.com/XTLS/Xray-core/pull/6546 --- proxy/dokodemo/fakeudp_openbsd.go | 32 ++++++++++++--- transport/internet/sockopt_openbsd.go | 15 +++++-- transport/internet/udp/hub_openbsd.go | 46 +++++++++++++++------- transport/internet/udp/hub_openbsd_test.go | 42 ++++++++++++++++++++ 4 files changed, 110 insertions(+), 25 deletions(-) diff --git a/proxy/dokodemo/fakeudp_openbsd.go b/proxy/dokodemo/fakeudp_openbsd.go index 8fcbcbea9..7f714e4b6 100644 --- a/proxy/dokodemo/fakeudp_openbsd.go +++ b/proxy/dokodemo/fakeudp_openbsd.go @@ -12,12 +12,29 @@ import ( ) func FakeUDP(addr *net.UDPAddr, mark int) (net.PacketConn, error) { - ip4 := addr.IP.To4() - if ip4 == nil { - return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("IPv6 is not supported by the OpenBSD transparent UDP patch")} + domain := unix.AF_INET6 + var sockaddr unix.Sockaddr + if ip4 := addr.IP.To4(); ip4 != nil { + domain = unix.AF_INET + sa := &unix.SockaddrInet4{Port: addr.Port} + copy(sa.Addr[:], ip4) + sockaddr = sa + } else if ip6 := addr.IP.To16(); ip6 != nil { + sa := &unix.SockaddrInet6{Port: addr.Port} + copy(sa.Addr[:], ip6) + if addr.Zone != "" { + iface, err := net.InterfaceByName(addr.Zone) + if err != nil { + return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("resolve zone %s: %w", addr.Zone, err)} + } + sa.ZoneId = uint32(iface.Index) + } + sockaddr = sa + } else { + return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("unsupported address %v", addr.IP)} } - fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0) + fd, err := unix.Socket(domain, unix.SOCK_DGRAM, 0) if err != nil { return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("socket open: %w", err)} } @@ -35,9 +52,12 @@ func FakeUDP(addr *net.UDPAddr, mark int) (net.PacketConn, error) { if err = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEADDR, 1); err != nil { return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("set socket option SO_REUSEADDR: %w", err)} } + // Several client sessions can be answered from the same original + // destination at the same time, so the address has to be shareable. + if err = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil { + return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("set socket option SO_REUSEPORT: %w", err)} + } - sockaddr := &unix.SockaddrInet4{Port: addr.Port} - copy(sockaddr.Addr[:], ip4) if err = unix.Bind(fd, sockaddr); err != nil { return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("bind %s: %w", addr.String(), err)} } diff --git a/transport/internet/sockopt_openbsd.go b/transport/internet/sockopt_openbsd.go index 888a73cb0..1efa56674 100644 --- a/transport/internet/sockopt_openbsd.go +++ b/transport/internet/sockopt_openbsd.go @@ -14,11 +14,18 @@ func applyOutboundSocketOptions(network string, address string, fd uintptr, conf func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig) error { if config.ReceiveOriginalDestAddress && isUDPSocket(network) { - if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_RECVDSTADDR, 1); err != nil { - return errors.New("failed to set IP_RECVDSTADDR").Base(err) + // Only the options matching the socket's address family are accepted, + // so one of the two pairs succeeding is enough. + err6 := unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_RECVPKTINFO, 1) + if err6 == nil { + err6 = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_RECVDSTPORT, 1) } - if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_RECVDSTPORT, 1); err != nil { - return errors.New("failed to set IP_RECVDSTPORT").Base(err) + err4 := unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_RECVDSTADDR, 1) + if err4 == nil { + err4 = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_RECVDSTPORT, 1) + } + if err4 != nil && err6 != nil { + return errors.New("failed to enable receiving the original destination").Base(err4) } } return nil diff --git a/transport/internet/udp/hub_openbsd.go b/transport/internet/udp/hub_openbsd.go index a036a7458..04c465a9d 100644 --- a/transport/internet/udp/hub_openbsd.go +++ b/transport/internet/udp/hub_openbsd.go @@ -17,23 +17,39 @@ func retrieveOriginalDestFromControlMessages(msgs []unix.SocketControlMessage) n var havePort bool for _, msg := range msgs { - if msg.Header.Level != unix.IPPROTO_IP { - continue - } - - switch msg.Header.Type { - case unix.IP_RECVDSTADDR: - if len(msg.Data) < 4 { - continue + switch msg.Header.Level { + case unix.IPPROTO_IP: + switch msg.Header.Type { + case unix.IP_RECVDSTADDR: + if len(msg.Data) < 4 { + continue + } + ip = append(ip[:0], msg.Data[:4]...) + haveAddress = true + case unix.IP_RECVDSTPORT: + if len(msg.Data) < 2 { + continue + } + port = binary.BigEndian.Uint16(msg.Data[:2]) + havePort = true } - ip = append(ip[:0], msg.Data[:4]...) - haveAddress = true - case unix.IP_RECVDSTPORT: - if len(msg.Data) < 2 { - continue + case unix.IPPROTO_IPV6: + switch msg.Header.Type { + case unix.IPV6_PKTINFO: + // struct in6_pktinfo: the destination address is followed by + // the interface index. + if len(msg.Data) < 16 { + continue + } + ip = append(ip[:0], msg.Data[:16]...) + haveAddress = true + case unix.IPV6_RECVDSTPORT: + if len(msg.Data) < 2 { + continue + } + port = binary.BigEndian.Uint16(msg.Data[:2]) + havePort = true } - port = binary.BigEndian.Uint16(msg.Data[:2]) - havePort = true } } diff --git a/transport/internet/udp/hub_openbsd_test.go b/transport/internet/udp/hub_openbsd_test.go index 084a117bd..a5c3e250e 100644 --- a/transport/internet/udp/hub_openbsd_test.go +++ b/transport/internet/udp/hub_openbsd_test.go @@ -33,6 +33,36 @@ func TestRetrieveOriginalDestFromControlMessages(t *testing.T) { } } +func TestRetrieveOriginalDestFromControlMessagesIPv6(t *testing.T) { + // 2001:db8::7 followed by the interface index, as in struct in6_pktinfo. + pktinfo := []byte{ + 0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0x07, + 0x02, 0, 0, 0, + } + msgs := []unix.SocketControlMessage{ + { + Header: unix.Cmsghdr{Level: unix.IPPROTO_IPV6, Type: unix.IPV6_RECVDSTPORT}, + Data: []byte{0x30, 0x39}, + }, + { + Header: unix.Cmsghdr{Level: unix.IPPROTO_IPV6, Type: unix.IPV6_PKTINFO}, + Data: pktinfo, + }, + } + + dest := retrieveOriginalDestFromControlMessages(msgs) + if !dest.IsValid() { + t.Fatal("destination is invalid") + } + if got, want := dest.Address.IP().String(), "2001:db8::7"; got != want { + t.Fatalf("address = %q, want %q", got, want) + } + if got, want := dest.Port.Value(), uint16(12345); got != want { + t.Fatalf("port = %d, want %d", got, want) + } +} + func TestRetrieveOriginalDestRequiresAddressAndPort(t *testing.T) { tests := [][]unix.SocketControlMessage{ { @@ -47,6 +77,18 @@ func TestRetrieveOriginalDestRequiresAddressAndPort(t *testing.T) { Data: []byte{0x30, 0x39}, }, }, + { + { + Header: unix.Cmsghdr{Level: unix.IPPROTO_IPV6, Type: unix.IPV6_PKTINFO}, + Data: make([]byte, 20), + }, + }, + { + { + Header: unix.Cmsghdr{Level: unix.IPPROTO_IPV6, Type: unix.IPV6_RECVDSTPORT}, + Data: []byte{0x30, 0x39}, + }, + }, } for _, msgs := range tests {