package main // Tests for round-3 items 20+21: build-before-bind (no client-count // deploy gate, no auto-removal at zero bindings) and the socks5-auth // node-level fixed account. import ( "encoding/base64" "os" "path/filepath" "strings" "testing" ) func socksNodeData() *Data { d := EmptyData() d.Servers = []*Server{{ID: "srv_1", Name: "S1", PublicAddress: "entry.example.com", PortRangeStart: 20000, PortRangeEnd: 50000, Status: "online", LastSeenAt: StrPtr(NowISO())}} d.Customers = []*Customer{ {ID: "cus_1", Name: "A", Status: "active", Credentials: &Credentials{ClientID: "11111111-2222-3333-4444-000000000001", RelayPassword: "FRYXGBkaGxwdHh8gISIjJA==", ShortID: "abcd0001"}}, {ID: "cus_2", Name: "B", Status: "active", Credentials: &Credentials{ClientID: "11111111-2222-3333-4444-000000000002", RelayPassword: "FRYXGBkaGxwdHh8gISIjJA==", ShortID: "abcd0002"}}, } return d } func TestSocksInboundAlwaysFixedAccount(t *testing.T) { node := &Node{ID: "nod_sk", Protocol: "socks5-auth", Port: 23462, NetworkMode: "ipv4", SocksUser: "fixeduser", SocksPassword: "fixedpass"} server := &Server{ID: "srv_1", PublicAddress: "entry.example.com"} for _, nClients := range []int{0, 1, 2} { clients := []*NodeClient{} for i := 0; i < nClients; i++ { clients = append(clients, &NodeClient{NodeID: node.ID, CustomerID: "cus_1", Status: "active", Credentials: &Credentials{ClientID: "x", RelayPassword: "y", ShortID: "z"}}) } inbound, err := buildSingBoxInbound("t-in", node, clients, server, "") if err != nil { t.Fatalf("clients=%d: %v", nClients, err) } users, _ := inbound["users"].([]any) if len(users) != 1 { t.Fatalf("clients=%d: users = %d, want exactly the fixed account", nClients, len(users)) } u, _ := users[0].(map[string]any) if u["username"] != "fixeduser" || u["password"] != "fixedpass" { t.Fatalf("clients=%d: user = %v, want the fixed pair", nClients, u) } } // A node without a stored pair gets one minted, never an ng: identity. bare := &Node{ID: "nod_bare", Protocol: "socks5-auth", Port: 23463, NetworkMode: "ipv4"} if _, err := buildSingBoxInbound("t-in", bare, nil, server, ""); err != nil { t.Fatal(err) } if bare.SocksUser == "" || strings.HasPrefix(bare.SocksUser, "ng:") { t.Fatalf("minted user %q must exist and avoid the ng: namespace", bare.SocksUser) } if raw, err := base64.StdEncoding.DecodeString(bare.SocksPassword); err != nil || len(raw) != 32 { t.Fatalf("minted password must be a base64 32-byte key, got %q", bare.SocksPassword) } } func TestSocksMigrationIdempotent(t *testing.T) { d := socksNodeData() d.Nodes = []*Node{ {ID: "nod_old", Protocol: "socks5-auth", Status: "active"}, {ID: "nod_set", Protocol: "socks5-auth", Status: "active", SocksUser: "kept", SocksPassword: "keptpw"}, {ID: "nod_vless", Protocol: "vless-reality", Status: "active"}, } if !MigrateSocksNodeCredentials(d) { t.Fatal("first migration must report a change") } old := d.Nodes[0] if old.SocksUser == "" || old.SocksPassword == "" { t.Fatal("legacy socks node must receive a fixed pair") } if d.Nodes[1].SocksUser != "kept" || d.Nodes[1].SocksPassword != "keptpw" { t.Fatal("an existing pair must be preserved") } if d.Nodes[2].SocksUser != "" { t.Fatal("non-socks nodes must not gain an account") } user, password := old.SocksUser, old.SocksPassword if MigrateSocksNodeCredentials(d) { t.Fatal("second migration must be a no-op") } if old.SocksUser != user || old.SocksPassword != password { t.Fatal("migration must be idempotent") } } func TestSocksMigrationRunsOnStoreLoad(t *testing.T) { dir := t.TempDir() file := filepath.Join(dir, "data.json") d := socksNodeData() seed := EmptyData() seed.Servers = d.Servers seed.Customers = d.Customers seed.Nodes = []*Node{{ID: "nod_old", Protocol: "socks5-auth", Status: "active"}} raw, err := marshalJSON(seed) if err != nil { t.Fatal(err) } if err := os.WriteFile(file, append(raw, '\n'), 0o600); err != nil { t.Fatal(err) } store := NewStore(file) if err := store.Init(); err != nil { t.Fatal(err) } node := store.Data.Nodes[0] if node.SocksUser == "" || node.SocksPassword == "" { t.Fatal("store load must migrate legacy socks nodes") } // And the migration is persisted, not just in memory. reloaded := NewStore(file) if err := reloaded.Init(); err != nil { t.Fatal(err) } if reloaded.Data.Nodes[0].SocksUser != node.SocksUser { t.Fatal("migrated pair must be persisted to disk") } } func TestZeroClientDeployNotGated(t *testing.T) { d := socksNodeData() vless := &Node{ID: "nod_v", Protocol: "vless-reality-vision", ServerID: "srv_1", Topology: "direct", Status: "draft", Port: 23456, Reality: &RealityConfig{KeyID: "reality-nod_v", ServerName: "www.tesla.com", DestPort: 443}} socks := &Node{ID: "nod_s", Protocol: "socks5-auth", ServerID: "srv_1", Topology: "direct", Status: "draft", Port: 23457} d.Nodes = []*Node{vless, socks} for _, node := range d.Nodes { if err := ValidateNodeDeploy(d, node); err != nil { t.Fatalf("%s zero-client deploy must be allowed: %v", node.Protocol, err) } } } func TestZeroClientRebuildKeepsNode(t *testing.T) { d := socksNodeData() node := &Node{ID: "nod_v", Protocol: "vless-reality-vision", ServerID: "srv_1", Topology: "direct", Status: "active", ResourceID: "res_v", Reality: &RealityConfig{KeyID: "reality-nod_v", ServerName: "www.tesla.com", DestPort: 443}} d.Nodes = []*Node{node} if err := RebuildNode(d, node, true); err != nil { t.Fatal(err) } if node.Status == "removing" { t.Fatal("zero-client rebuild must not auto-remove the node") } applies, deletes := 0, 0 for _, job := range d.Jobs { if job.NodeID == node.ID && job.Action == "apply_resource" { applies++ } if job.NodeID == node.ID && job.Action == "delete_resource" { deletes++ } } if applies != 1 || deletes != 0 { t.Fatalf("jobs: applies=%d deletes=%d, want 1/0", applies, deletes) } } func TestSocksSubscriptionCarriesFixedAccount(t *testing.T) { d := socksNodeData() node := &Node{ID: "nod_s", Name: "Socks", Protocol: "socks5-auth", ServerID: "srv_1", Topology: "direct", NetworkMode: "ipv4", Port: 23462, Status: "active", SocksUser: "fixeduser", SocksPassword: "fixedpass"} d.Nodes = []*Node{node} for _, customerID := range []string{"cus_1", "cus_2"} { nc := &NodeClient{NodeID: node.ID, CustomerID: customerID, Status: "active"} uri := NodeClientURI(d, nc) if !strings.HasPrefix(uri, "socks5://fixeduser:fixedpass@entry.example.com:23462") { t.Fatalf("%s: uri = %q, want the fixed account", customerID, uri) } } }