mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-09-15 22:10:26 +00:00
234 lines
6.8 KiB
Go
234 lines
6.8 KiB
Go
//go:build darwin
|
|
|
|
package tun
|
|
|
|
import (
|
|
"sync"
|
|
"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)
|
|
}
|
|
}
|
|
}
|