package singbox import ( "context" "errors" "net" "net/netip" "testing" "time" "github.com/cedar2025/xboard-node/internal/config" "github.com/sagernet/sing-box/adapter" singM "github.com/sagernet/sing/common/metadata" "golang.org/x/time/rate" ) func TestSingBoxCapabilities(t *testing.T) { s := New(config.KernelConfig{Type: "sing-box"}) caps := s.Capabilities() if !caps.PerUserSpeedLimit || !caps.DeviceLimit || !caps.AliveIPTracking || !caps.ForceCloseUser { t.Fatalf("unexpected sing-box capabilities: %+v", caps) } if caps.BuiltInTrafficStats || caps.ForceCloseConnection { t.Fatalf("unexpected sing-box capabilities: %+v", caps) } protocols := s.Protocols() found := false for _, protocol := range protocols { if protocol == "hysteria" { found = true break } } if !found { t.Fatalf("Protocols() missing hysteria: %v", protocols) } } type testConn struct { closed bool reads [][]byte writes [][]byte } func (c *testConn) Read(b []byte) (int, error) { if len(c.reads) == 0 { return 0, errors.New("eof") } chunk := c.reads[0] c.reads = c.reads[1:] n := copy(b, chunk) return n, nil } func (c *testConn) Write(b []byte) (int, error) { cp := append([]byte(nil), b...) c.writes = append(c.writes, cp) return len(b), nil } func (c *testConn) Close() error { c.closed = true; return nil } func (c *testConn) LocalAddr() net.Addr { return &net.TCPAddr{} } func (c *testConn) RemoteAddr() net.Addr { return &net.TCPAddr{} } func (c *testConn) SetDeadline(time.Time) error { return nil } func (c *testConn) SetReadDeadline(time.Time) error { return nil } func (c *testConn) SetWriteDeadline(time.Time) error { return nil } func testInboundContext(uuid, ip string) adapter.InboundContext { return adapter.InboundContext{ User: uuid, Source: singM.Socksaddr{Addr: netip.MustParseAddr(ip)}, } } func TestConnTrackerRoutedConnectionTracksTrafficAndAliveIPs(t *testing.T) { tracker := NewConnTracker(0) tracker.SetUserMap(map[string]int{"uuid-1": 1}) base := &testConn{reads: [][]byte{[]byte("hello")}} wrapped := tracker.RoutedConnection(context.Background(), base, testInboundContext("uuid-1", "1.1.1.1"), nil, nil) buf := make([]byte, 16) n, err := wrapped.Read(buf) if err != nil { t.Fatalf("Read() error = %v", err) } if n != 5 { t.Fatalf("Read() bytes = %d, want 5", n) } if _, err := wrapped.Write([]byte("bye")); err != nil { t.Fatalf("Write() error = %v", err) } traffic, aliveIPs, connCount := tracker.GetUserTraffic() if got := traffic[1]; got != [2]int64{5, 3} { t.Fatalf("traffic[1] = %v, want [5 3]", got) } if !aliveIPs[1]["1.1.1.1"] { t.Fatal("expected alive IP to include source address") } if connCount != 1 { t.Fatalf("connCount = %d, want 1", connCount) } if err := wrapped.Close(); err != nil { t.Fatalf("Close() error = %v", err) } _, aliveIPs, connCount = tracker.GetUserTraffic() if aliveIPs[1] != nil { t.Fatalf("aliveIPs after close = %v, want nil", aliveIPs[1]) } if connCount != 0 { t.Fatalf("connCount after close = %d, want 0", connCount) } } func TestConnTrackerRoutedConnectionRejectsWhenDeviceLimitExceeded(t *testing.T) { tracker := NewConnTracker(0) tracker.SetUserMap(map[string]int{"uuid-1": 1}) tracker.SetDeviceLimitFunc(func(uuid string) (int, bool) { if uuid != "uuid-1" { return 0, false } return 1, true }) first := &testConn{} wrapped1 := tracker.RoutedConnection(context.Background(), first, testInboundContext("uuid-1", "1.1.1.1"), nil, nil) if wrapped1 == first { t.Fatal("expected first connection to be wrapped") } second := &testConn{} wrapped2 := tracker.RoutedConnection(context.Background(), second, testInboundContext("uuid-1", "2.2.2.2"), nil, nil) if wrapped2 != second { t.Fatal("expected rejected connection to be returned unwrapped") } if !second.closed { t.Fatal("expected rejected connection to be closed") } } func TestConnTrackerCheckDeviceGateMergesFreshGlobalDevices(t *testing.T) { tracker := NewConnTracker(0) tracker.SetUserMap(map[string]int{"uuid-1": 1}) us := tracker.users[1] us.addConn("1.1.1.1") tracker.UpdateGlobalDevices(map[int][]string{1: {"9.9.9.9"}}) if tracker.checkDeviceGate(us, 1, "2.2.2.2", 2) { t.Fatal("expected lexicographically earlier candidate to remain allowed") } if !tracker.checkDeviceGate(us, 1, "99.99.99.99", 2) { t.Fatal("expected merged local+global device state to reject lexicographically later third device") } if tracker.checkDeviceGate(us, 1, "1.1.1.1", 2) { t.Fatal("existing local IP should still be allowed") } if tracker.checkDeviceGate(us, 1, "9.9.9.9", 2) { t.Fatal("existing global IP should still be allowed") } } func TestConnTrackerCloseByIDClosesTrackedConnection(t *testing.T) { tracker := NewConnTracker(0) tracker.SetUserMap(map[string]int{"uuid-1": 1}) base := &testConn{} wrapped := tracker.RoutedConnection(context.Background(), base, testInboundContext("uuid-1", "1.1.1.1"), nil, nil) tracked, ok := wrapped.(*trackedConn) if !ok { t.Fatalf("wrapped type = %T, want *trackedConn", wrapped) } if !tracker.CloseByID(tracked.connID) { t.Fatal("CloseByID() = false, want true") } if !base.closed { t.Fatal("expected underlying connection to be closed") } } func TestConnTrackerRateLimitHonorsContextCancellation(t *testing.T) { tracker := NewConnTracker(0) tracker.SetUserMap(map[string]int{"uuid-1": 1}) tracker.SetSpeedLimitFunc(func(uuid string) *rate.Limiter { if uuid != "uuid-1" { return nil } return rate.NewLimiter(rate.Limit(1), 1) }) readCtx, cancelRead := context.WithCancel(context.Background()) readBase := &testConn{reads: [][]byte{[]byte("a"), []byte("a")}} readTracked := tracker.RoutedConnection(readCtx, readBase, testInboundContext("uuid-1", "1.1.1.1"), nil, nil).(*trackedConn) buf := make([]byte, 8) if n, err := readTracked.Read(buf); err != nil || n != 1 { t.Fatalf("first Read() = (%d, %v), want (1, nil)", n, err) } cancelRead() if n, err := readTracked.Read(buf); !errors.Is(err, context.Canceled) || n != 1 { t.Fatalf("second Read() = (%d, %v), want (1, context canceled)", n, err) } writeCtx, cancelWrite := context.WithCancel(context.Background()) writeBase := &testConn{} writeTracked := tracker.RoutedConnection(writeCtx, writeBase, testInboundContext("uuid-1", "1.1.1.2"), nil, nil).(*trackedConn) if n, err := writeTracked.Write([]byte("a")); err != nil || n != 1 { t.Fatalf("first Write() = (%d, %v), want (1, nil)", n, err) } cancelWrite() if n, err := writeTracked.Write([]byte("a")); !errors.Is(err, context.Canceled) || n != 0 { t.Fatalf("second Write() = (%d, %v), want (0, context canceled)", n, err) } }