Files
Xray-core/proxy/tun/tun_darwin_test.go
T
patternihaandClaude Opus 5.5 3d830eea02 TUN inbound: Don't set IP_BOUND_IF on IPv6 sockets on Darwin
Since #7173, a failed interface binding fails the dial. On Darwin (macOS
and iOS), setinterface fell through from IPV6_BOUND_IF to IP_BOUND_IF on
IPv6 sockets, which Darwin rejects with EINVAL. So every UDP dial failed,
as Go opens a dual-stack IPv6 socket for it, and so did TCP to IPv6. TCP to
IPv4 uses an IPv4 socket and kept working. Before #7173 the error was only
logged.

IPV6_BOUND_IF binds the whole socket, so on a dual-stack socket it covers
its IPv4 traffic as well. Set only the option of the socket's family, as
sockopt_darwin.go already does for sockopt.interface. The new test binds
each kind of socket to lo0.

Linux, Android and FreeBSD bind with socket-level options (SO_BINDTODEVICE,
SO_SETFIB) that don't depend on the IP version. On Windows, the related case
of an adapter without IPv4 or IPv6 is handled in #7214.

Fixes #7228

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 11:17:33 +03:30

274 lines
7.9 KiB
Go

//go:build darwin
package tun
import (
"context"
"net"
"sync"
"syscall"
"testing"
"time"
"golang.org/x/sys/unix"
)
func TestSelectDarwinGatewayDefault(t *testing.T) {
gateway, err := selectDarwinGateway(nil)
if err != nil {
t.Fatal(err)
}
if got := gateway.String(); got != defaultDarwinGateway {
t.Fatalf("unexpected default gateway: got %s, want %s", got, defaultDarwinGateway)
}
}
func TestSelectDarwinGatewayConfiguredIPv4(t *testing.T) {
gateway, err := selectDarwinGateway([]string{"198.18.0.1/15"})
if err != nil {
t.Fatal(err)
}
if got := gateway.String(); got != "198.18.0.1/15" {
t.Fatalf("unexpected gateway: got %s", got)
}
}
func TestSelectDarwinGatewaySkipsIPv6(t *testing.T) {
gateway, err := selectDarwinGateway([]string{"fc00::1/64", "198.18.0.1/15"})
if err != nil {
t.Fatal(err)
}
if got := gateway.String(); got != "198.18.0.1/15" {
t.Fatalf("unexpected gateway: got %s", got)
}
}
func TestSelectDarwinGatewayRequiresIPv4(t *testing.T) {
if _, err := selectDarwinGateway([]string{"fc00::1/64"}); err == nil {
t.Fatal("expected error")
}
}
func TestSelectDarwinGatewayRequiresUsableLocalAddress(t *testing.T) {
if _, err := selectDarwinGateway([]string{"198.18.0.1/32"}); err == nil {
t.Fatal("expected error")
}
}
// newTestSocketpair returns a connected AF_UNIX/SOCK_DGRAM pair -- a real
// fd DarwinTun.Wait's kqueue can register EVFILT_READ against, without
// needing an actual utun interface (which requires root/network
// entitlements this test environment doesn't have). Datagram sockets
// (unlike pipes) support both "write makes readable" and "close makes
// readable" the same way a tun fd's read-readiness behaves.
func newTestSocketpair(t *testing.T) (a, b int) {
t.Helper()
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_DGRAM, 0)
if err != nil {
t.Fatalf("socketpair: %v", err)
}
t.Cleanup(func() {
_ = unix.Close(fds[0])
_ = unix.Close(fds[1])
})
return fds[0], fds[1]
}
// Reviewer feedback, XTLS/Xray-core#6580: "blocking with no data" case --
// wait() must not return before the timeout when nothing is written.
func TestWaitKqueueBlocksWithNoData(t *testing.T) {
a, _ := newTestSocketpair(t)
kq := newWaitKqueue(a)
if kq == nil {
t.Fatal("newWaitKqueue returned nil")
}
defer kq.close()
start := time.Now()
ok := kq.wait(150 * time.Millisecond)
elapsed := time.Since(start)
if !ok {
t.Fatal("wait() returned false on a healthy kqueue with a plain timeout")
}
if elapsed < 100*time.Millisecond {
t.Fatalf("wait() returned after only %v, expected it to block close to the 150ms timeout", elapsed)
}
}
// Reviewer feedback: "wake up with a readable fd" case.
func TestWaitKqueueWakesOnReadable(t *testing.T) {
a, b := newTestSocketpair(t)
kq := newWaitKqueue(a)
if kq == nil {
t.Fatal("newWaitKqueue returned nil")
}
defer kq.close()
done := make(chan bool, 1)
go func() {
done <- kq.wait(5 * time.Second)
}()
time.Sleep(20 * time.Millisecond) // let wait() actually enter the syscall first
if _, err := unix.Write(b, []byte{0x1}); err != nil {
t.Fatalf("write: %v", err)
}
select {
case ok := <-done:
if !ok {
t.Fatal("wait() returned false after the fd became readable")
}
case <-time.After(2 * time.Second):
t.Fatal("wait() did not wake up within 2s of the fd becoming readable")
}
}
// Reviewer feedback: "timeout" case, explicitly (distinct from the
// no-data test above, which also checks blocking duration -- this one
// only checks the return value).
func TestWaitKqueueTimesOut(t *testing.T) {
a, _ := newTestSocketpair(t)
kq := newWaitKqueue(a)
if kq == nil {
t.Fatal("newWaitKqueue returned nil")
}
defer kq.close()
if !kq.wait(50 * time.Millisecond) {
t.Fatal("wait() returned false on a plain timeout with no error condition")
}
}
// Reviewer feedback: "Close() wakes a blocked wait" case, and the
// no-double-close/no-fd-reuse concern (P1) -- close() while wait() is
// parked in its syscall must not panic, must not leave wait() hung, and a
// second close() call (from a caller that, say, calls Close() twice on
// the same DarwinTun) must be safe.
func TestWaitKqueueCloseDuringWaitIsSafe(t *testing.T) {
a, _ := newTestSocketpair(t)
kq := newWaitKqueue(a)
if kq == nil {
t.Fatal("newWaitKqueue returned nil")
}
started := make(chan struct{})
done := make(chan bool, 1)
go func() {
close(started)
done <- kq.wait(5 * time.Second)
}()
<-started
time.Sleep(20 * time.Millisecond) // let wait() actually enter the syscall first
kq.close()
kq.close() // double-close must be idempotent, not panic or double-free the fd
select {
case <-done:
// Either true (the close-of-the-underlying-fd unblocked kevent, a
// real kqueue behavior) or false (wait() observed the closed flag
// first) is acceptable -- what matters is that it returned at all,
// promptly, without hanging or crashing.
case <-time.After(2 * time.Second):
t.Fatal("wait() did not return within 2s of close() being called")
}
// A wait() call *after* close() must return false immediately (the
// closed-flag fast path), not attempt a syscall against the
// already-closed (and potentially since-reused, on a real system) fd
// number.
if kq.wait(time.Second) {
t.Fatal("wait() returned true after close() -- should short-circuit via the closed flag")
}
}
// Reviewer feedback: "multiple/concurrent close guard" case -- many
// goroutines calling close() concurrently must close the underlying fd
// exactly once.
func TestWaitKqueueConcurrentCloseIsSafe(t *testing.T) {
a, _ := newTestSocketpair(t)
kq := newWaitKqueue(a)
if kq == nil {
t.Fatal("newWaitKqueue returned nil")
}
var wg sync.WaitGroup
for range 20 {
wg.Add(1)
go func() {
defer wg.Done()
kq.close()
}()
}
wg.Wait()
if !kq.closed.Load() {
t.Fatal("closed flag not set after concurrent close() calls")
}
}
// Reviewer feedback: "kevent runtime failure without spinning" case (P2).
// Simulates a kqueue that has gone bad (closed out from under it, as if a
// concurrent/erroneous close happened) and confirms wait() reports it as
// unusable (false) rather than silently returning true forever, which is
// what DarwinTun.Wait relies on to permanently fall back to the sleep
// path instead of re-entering a failing syscall on every dispatchLoop
// iteration.
func TestWaitKqueueReportsPersistentFailure(t *testing.T) {
a, _ := newTestSocketpair(t)
kq := newWaitKqueue(a)
if kq == nil {
t.Fatal("newWaitKqueue returned nil")
}
// Close the underlying kqueue fd directly (bypassing kq.close(), which
// would also set the closed flag) to simulate the fd going bad for a
// reason other than this type's own close() -- e.g. some other code
// path in the process closing it, or the kernel invalidating it.
_ = unix.Close(kq.fd)
for i := 0; i < 5; i++ {
if kq.wait(50 * time.Millisecond) {
t.Fatalf("wait() call %d returned true against a closed underlying fd -- should report failure, not spin", i)
}
}
}
// TestSetinterfaceSocketFamilies binds each kind of socket Xray's dialers
// open to lo0, including the dual-stack one Go opens for UDP.
func TestSetinterfaceSocketFamilies(t *testing.T) {
lo, err := net.InterfaceByName("lo0")
if err != nil {
t.Skip(err)
}
lc := net.ListenConfig{Control: func(network, address string, c syscall.RawConn) error {
var err error
if controlErr := c.Control(func(fd uintptr) {
err = setinterface(network, address, fd, lo)
}); controlErr != nil {
return controlErr
}
return err
}}
for _, s := range []struct{ network, address string }{
{"udp", "0.0.0.0:0"}, // dual-stack
{"udp4", "127.0.0.1:0"},
{"udp6", "[::1]:0"},
{"tcp4", "127.0.0.1:0"},
{"tcp6", "[::1]:0"},
} {
var closer interface{ Close() error }
if s.network[:3] == "udp" {
closer, err = lc.ListenPacket(context.Background(), s.network, s.address)
} else {
closer, err = lc.Listen(context.Background(), s.network, s.address)
}
if err != nil {
t.Errorf("%s %s: %v", s.network, s.address, err)
continue
}
closer.Close()
}
}