Files
xboard-node/internal/kernel/singbox/singbox_test.go
T
2026-04-11 05:39:59 +08:00

205 lines
6.3 KiB
Go

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)
}
}
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)
}
}