Files
3x-ui/internal/web/service/inbound_tuic_test.go
T
libmur-dev b3f1cced70 fix(tuic): preserve IP destinations during sniffing (#6753)
Keep sniffed TLS domains available for routing without replacing the destination address requested by the TUIC client. Add a regression assertion for the generated relay sniffing settings.
2026-10-07 10:53:46 +02:00

350 lines
11 KiB
Go

package service
import (
"encoding/json"
"fmt"
"slices"
"strings"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/tuic"
"github.com/mhsanaei/3x-ui/v3/internal/xray"
)
func TestInjectTuicSocks(t *testing.T) {
cfg := &xray.Config{}
inbounds := []*model.Inbound{
{
Id: 5,
Tag: "tuic-in-5",
Protocol: model.TUIC,
Enable: true,
Settings: `{
"certificate": "dummy-cert",
"private_key": "dummy-key",
"clients": [
{"uuid": "a0000000-0000-0000-0000-000000000001", "password": "pass1", "email": "user1@example.com", "enable": true}
]
}`,
},
}
injectTuicSocks(cfg, inbounds)
if len(cfg.InboundConfigs) != 1 {
t.Fatalf("expected 1 injected SOCKS inbound, got %d", len(cfg.InboundConfigs))
}
sc := cfg.InboundConfigs[0]
if sc.Tag != "tuic-in-5" {
t.Fatalf("expected tag tuic-in-5, got %s", sc.Tag)
}
if sc.Protocol != "socks" {
t.Fatalf("expected protocol socks, got %s", sc.Protocol)
}
expectedPort := tuic.SOCKSPortForInbound(5)
if sc.Port != expectedPort {
t.Fatalf("expected port %d, got %d", expectedPort, sc.Port)
}
if string(sc.Listen) != `"127.0.0.1"` {
t.Fatalf("expected listen 127.0.0.1, got %s", sc.Listen)
}
var parsedSniffing struct {
Enabled bool `json:"enabled"`
DestOverride []string `json:"destOverride"`
RouteOnly bool `json:"routeOnly"`
}
if err := json.Unmarshal(sc.Sniffing, &parsedSniffing); err != nil {
t.Fatalf("failed to unmarshal sniffing settings: %v", err)
}
if !parsedSniffing.Enabled || !parsedSniffing.RouteOnly {
t.Fatalf("sniffing must be enabled with routeOnly, got %+v", parsedSniffing)
}
if want := []string{"http", "tls", "quic", "fakedns"}; !slices.Equal(parsedSniffing.DestOverride, want) {
t.Fatalf("destOverride = %v, want %v", parsedSniffing.DestOverride, want)
}
var parsedSettings struct {
Auth string `json:"auth"`
UDP bool `json:"udp"`
}
if err := json.Unmarshal(sc.Settings, &parsedSettings); err != nil {
t.Fatalf("failed to unmarshal settings: %v", err)
}
if parsedSettings.Auth != "noauth" || !parsedSettings.UDP {
t.Fatalf("expected auth=noauth, udp=true, got %+v", parsedSettings)
}
}
func TestCheckTuicSocksConflict(t *testing.T) {
setupConflictDB(t)
// Seed TUIC inbound with ID 10
tuicIb := &model.Inbound{
Id: 10,
Tag: "tuic-10",
Protocol: model.TUIC,
Enable: true,
Listen: "0.0.0.0",
Port: 8443,
Settings: `{"clients":[{"uuid":"a0000000-0000-0000-0000-000000000001","password":"p","email":"u@test.com"}]}`,
}
if err := database.GetDB().Create(tuicIb).Error; err != nil {
t.Fatalf("failed to seed TUIC inbound: %v", err)
}
relayPort := tuic.SOCKSPortForInbound(10)
// Try to create a new TCP inbound on that relayPort on 127.0.0.1
newIb := &model.Inbound{
Tag: "colliding-inbound",
Protocol: model.Mixed,
Enable: true,
Listen: "127.0.0.1",
Port: relayPort,
}
detail, err := checkTuicSocksConflict(database.GetDB(), newIb, 0, transportTCP)
if err != nil {
t.Fatalf("checkTuicSocksConflict error: %v", err)
}
if detail == nil {
t.Fatalf("expected conflict on port %d, got none", relayPort)
}
if detail.Tag != "tuic-10" {
t.Fatalf("expected conflict tag tuic-10, got %s", detail.Tag)
}
}
func TestCheckTuicSocksReverseConflict(t *testing.T) {
setupConflictDB(t)
targetPort := tuic.SOCKSPortForInbound(20)
// Seed existing inbound on targetPort on 127.0.0.1
existing := &model.Inbound{
Id: 99,
Tag: "existing-on-relay-port",
Protocol: model.Mixed,
Enable: true,
Listen: "127.0.0.1",
Port: targetPort,
}
if err := database.GetDB().Create(existing).Error; err != nil {
t.Fatalf("failed to seed existing inbound: %v", err)
}
detail, err := checkTuicSocksReverseConflict(database.GetDB(), 20)
if err != nil {
t.Fatalf("checkTuicSocksReverseConflict error: %v", err)
}
if detail == nil {
t.Fatalf("expected reverse conflict for id 20 on port %d, got none", targetPort)
}
if detail.Tag != "existing-on-relay-port" {
t.Fatalf("expected tag existing-on-relay-port, got %s", detail.Tag)
}
}
func TestDesiredTuicInstances(t *testing.T) {
setupConflictDB(t)
ib := &model.Inbound{
Id: 30,
Tag: "tuic-desired-test",
Protocol: model.TUIC,
Enable: true,
Listen: "0.0.0.0",
Port: 9443,
Settings: `{
"certificate": "cert",
"private_key": "key",
"clients": [
{"uuid": "a0000000-0000-0000-0000-000000000001", "password": "p1", "email": "active@test.com", "enable": true},
{"uuid": "a0000000-0000-0000-0000-000000000002", "password": "p2", "email": "disabled@test.com", "enable": true}
]
}`,
}
if err := database.GetDB().Create(ib).Error; err != nil {
t.Fatalf("failed to seed inbound: %v", err)
}
// Add client traffic entry disabling disabled@test.com
ct := &xray.ClientTraffic{
InboundId: 30,
Email: "disabled@test.com",
Enable: false,
}
if err := database.GetDB().Create(ct).Error; err != nil {
t.Fatalf("failed to seed client traffic: %v", err)
}
svc := &InboundService{}
instances, err := svc.DesiredTuicInstances()
if err != nil {
t.Fatalf("DesiredTuicInstances failed: %v", err)
}
found := false
for _, inst := range instances {
if inst.Id == 30 {
found = true
if len(inst.Clients) != 1 || inst.Clients[0].Email != "active@test.com" {
t.Fatalf("expected only active@test.com, got %+v", inst.Clients)
}
}
}
if !found {
t.Fatal("expected to find instance for inbound 30")
}
}
func TestCheckForwardedPortsConflict_CollidesWithTuicSocksPort(t *testing.T) {
setupConflictDB(t)
seedInboundConflict(t, "tuic-1", "0.0.0.0", 8443, model.TUIC, ``, `{"clients":[{"uuid":"u","password":"p","email":"e"}]}`)
var tuicInbound model.Inbound
if err := database.GetDB().Where("tag = ?", "tuic-1").First(&tuicInbound).Error; err != nil {
t.Fatalf("read seeded row: %v", err)
}
relayPort := tuic.SOCKSPortForInbound(tuicInbound.Id)
svc := &InboundService{}
ctx, err := svc.loadPortConflictContext(database.GetDB(), nil)
if err != nil {
t.Fatalf("loadPortConflictContext: %v", err)
}
hit := svc.checkForwardedPortsConflict(ctx, fmt.Sprintf("%d", relayPort))
if !strings.Contains(hit, "SOCKS5") {
t.Fatalf("expected a collision naming the TUIC inbound's SOCKS5 relay port, got %q", hit)
}
}
func TestCheckTuicSocksConflict_DisabledInboundRetainsReservation(t *testing.T) {
setupConflictDB(t)
seedInboundConflict(t, "tuic-disabled", "0.0.0.0", 8443, model.TUIC, ``, `{"clients":[{"uuid":"u","password":"p","email":"e"}]}`)
var tuicIb model.Inbound
if err := database.GetDB().Where("tag = ?", "tuic-disabled").First(&tuicIb).Error; err != nil {
t.Fatalf("read seeded row: %v", err)
}
if err := database.GetDB().Model(&tuicIb).Update("enable", false).Error; err != nil {
t.Fatalf("disable inbound: %v", err)
}
relayPort := tuic.SOCKSPortForInbound(tuicIb.Id)
testIb := &model.Inbound{
Tag: "conflict-test",
Protocol: model.VLESS,
Listen: "127.0.0.1",
Port: relayPort,
Enable: true,
}
conflict, err := checkTuicSocksConflict(database.GetDB(), testIb, 0, transportTCP)
if err != nil {
t.Fatalf("checkTuicSocksConflict: %v", err)
}
if conflict == nil {
t.Fatal("expected conflict on disabled TUIC inbound's SOCKS port, got nil")
}
if conflict.InboundID != tuicIb.Id {
t.Fatalf("expected conflict with inbound %d, got %d", tuicIb.Id, conflict.InboundID)
}
}
func TestCheckTuicSocksRelayCollision(t *testing.T) {
setupConflictDB(t)
// Seed first TUIC inbound with ID 1
ib1 := &model.Inbound{
Id: 1,
Tag: "tuic-1",
Protocol: model.TUIC,
Enable: true,
Listen: "0.0.0.0",
Port: 8443,
Settings: `{"clients":[{"uuid":"a0000000-0000-0000-0000-000000000001","password":"p","email":"u1@test.com"}]}`,
}
if err := database.GetDB().Create(ib1).Error; err != nil {
t.Fatalf("seed ib1: %v", err)
}
// ID 1001 wraps to the same relay port (64001) as ID 1
conflict, err := checkTuicSocksRelayCollision(database.GetDB(), 1001)
if err != nil {
t.Fatalf("checkTuicSocksRelayCollision: %v", err)
}
if conflict == nil {
t.Fatal("expected collision between ID 1001 and ID 1, got nil")
}
if conflict.InboundID != 1 {
t.Fatalf("expected collision with inbound 1, got %d", conflict.InboundID)
}
}
func TestTuicSocksSelfConflict(t *testing.T) {
ib := &model.Inbound{
Protocol: model.TUIC,
Listen: "127.0.0.1",
Port: tuic.SOCKSPortForInbound(5),
}
errStr := tuicSocksSelfConflict(ib, 5)
if errStr == "" {
t.Fatal("expected self conflict error string, got empty")
}
if !strings.Contains(errStr, "own SOCKS5 relay port") {
t.Fatalf("unexpected error string: %s", errStr)
}
// Different port should not conflict
ib.Port = 9999
if diff := tuicSocksSelfConflict(ib, 5); diff != "" {
t.Fatalf("expected no conflict for different port, got %s", diff)
}
}
func TestInboundTuicServerParsesLegacyFlatSettings(t *testing.T) {
server := inboundTuicServer(string(model.TUIC), `{"certificate":"/cert.pem","private_key":"/secret-key.pem","congestion_control":" CuBiC ","udp_relay_mode":"quic","sni":"profile.example"}`)
if server == nil {
t.Fatal("expected legacy flat TUIC settings")
}
if server.CongestionControl != "cubic" || server.UDPRelayMode != "quic" || server.SNI != "profile.example" {
t.Fatalf("legacy flat fields were not normalized: %+v", server)
}
if server.PrivateKey != "" {
t.Fatal("client preview exposed the inbound private key")
}
}
func TestNormalizeTuicSettingsCanonicalizesCongestionAndPacketLimit(t *testing.T) {
ib := &model.Inbound{Protocol: model.TUIC, Settings: `{"congestion_control":"RENO","max_udp_relay_packet_size":65507,"server":{"congestion_control":" CuBiC ","max_udp_relay_packet_size":65500}}`}
if err := normalizeTuicSettings(ib); err != nil {
t.Fatalf("normalizeTuicSettings: %v", err)
}
var got struct {
CongestionControl string `json:"congestion_control"`
MaxPacketSize int `json:"max_udp_relay_packet_size"`
Server struct {
CongestionControl string `json:"congestion_control"`
MaxPacketSize int `json:"max_udp_relay_packet_size"`
} `json:"server"`
}
if err := json.Unmarshal([]byte(ib.Settings), &got); err != nil {
t.Fatalf("unmarshal normalized settings: %v", err)
}
if got.CongestionControl != "new_reno" || got.Server.CongestionControl != "cubic" {
t.Fatalf("congestion controllers were not canonicalized: %+v", got)
}
if got.MaxPacketSize != 65245 || got.Server.MaxPacketSize != 65245 {
t.Fatalf("packet limits were not clamped: %+v", got)
}
ib.Settings = `{"server":{"congestion_control":"experimental"}}`
if err := normalizeTuicSettings(ib); err == nil {
t.Fatal("unsupported congestion controller was accepted")
}
}