package service import ( "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) // Public-address resolution: the node list must show the address a // client can actually connect to. An agent behind 1:1 NAT only ever // reports its private interface address; the master-observed source // address (SeenIPv4/SeenIPv6) is the public side. Resolution order per // family: public telemetry > public observed > telemetry as-is. func TestIsPublicIPLiteral(t *testing.T) { cases := map[string]bool{ "8.8.8.8": true, "111.91.29.150": true, "203.0.113.7": true, // documentation range: not excluded (codebase treats it as public) "2602:f32b:1:310f::1": true, "2001:db8::7": true, "10.0.0.1": false, // RFC1918 "172.31.2.240": false, // RFC1918 (the HWSG case) "192.168.1.1": false, // RFC1918 "100.64.1.2": false, // CGNAT shared space "100.127.255.254": false, // CGNAT shared space, top edge "127.0.0.1": false, // loopback "169.254.1.1": false, // link-local "fd00::1": false, // ULA "fe80::1": false, // link-local v6 "::1": false, "::": false, "224.0.0.1": false, // multicast "": false, "not-an-ip": false, "999.1.1.1": false, } for in, want := range cases { if got := IsPublicIPLiteral(in); got != want { t.Errorf("IsPublicIPLiteral(%q) = %v, want %v", in, got, want) } } } func TestResolveAgentAddresses(t *testing.T) { cases := []struct { name string ag *model.AgentNode want4, want6 string }{ { name: "telemetry public wins over seen public", ag: &model.AgentNode{IPv4: "140.235.229.19", SeenIPv4: "8.8.8.8"}, want4: "140.235.229.19", }, { name: "NAT: private telemetry, public seen", ag: &model.AgentNode{IPv4: "172.31.2.240", SeenIPv4: "111.91.29.150"}, want4: "111.91.29.150", }, { name: "both private: telemetry fallback", ag: &model.AgentNode{IPv4: "172.31.2.240", SeenIPv4: "192.168.0.8"}, want4: "172.31.2.240", }, { name: "no telemetry yet, public seen", ag: &model.AgentNode{SeenIPv4: "111.91.29.150"}, want4: "111.91.29.150", }, { name: "CGNAT seen is not trusted", ag: &model.AgentNode{IPv4: "172.31.2.240", SeenIPv4: "100.64.1.2"}, want4: "172.31.2.240", }, { name: "v6 only, telemetry public", ag: &model.AgentNode{IPv6: "2602:f32b:1:310f::1"}, want6: "2602:f32b:1:310f::1", }, { name: "v6 ULA telemetry, public seen", ag: &model.AgentNode{IPv6: "fd00::1", SeenIPv6: "2401:1da0:22:148::a"}, want6: "2401:1da0:22:148::a", }, { name: "garbage telemetry ignored", ag: &model.AgentNode{IPv4: "not-an-ip", SeenIPv4: "111.91.29.150"}, want4: "111.91.29.150", }, { name: "families resolve independently", ag: &model.AgentNode{IPv4: "140.235.229.19", IPv6: "fd00::9", SeenIPv6: "2401:1da0:22:148::a"}, want4: "140.235.229.19", want6: "2401:1da0:22:148::a", }, { name: "nothing known", ag: &model.AgentNode{}, }, { name: "nil agent", ag: nil, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { v4, v6 := ResolveAgentAddresses(tc.ag) if v4 != tc.want4 || v6 != tc.want6 { t.Fatalf("ResolveAgentAddresses(%+v) = (%q, %q), want (%q, %q)", tc.ag, v4, v6, tc.want4, tc.want6) } }) } } // Display: the node list row address comes from ResolveAgentAddresses, // including for an agent that has not reported telemetry yet. func TestAnnotateAgentTelemetryResolvedAddress(t *testing.T) { setupConflictDB(t) db := database.GetDB() natNode := &model.Node{Id: 7501, Name: "nat-agent-node", Address: ""} preNode := &model.Node{Id: 7502, Name: "pre-telemetry-agent-node", Address: ""} for _, n := range []*model.Node{natNode, preNode} { if err := db.Create(n).Error; err != nil { t.Fatalf("create node %d: %v", n.Id, err) } } nat := &model.AgentNode{ TokenHash: "hash-nat", Name: "nat-agent-node", NodeID: &natNode.Id, AgentVersion: "A1.0.0", XrayVersion: "26.6.27", TelemetryAt: 1700000000000, IPv4: "172.31.2.240", SeenIPv4: "111.91.29.150", } pre := &model.AgentNode{ TokenHash: "hash-pre", Name: "pre-telemetry-agent-node", NodeID: &preNode.Id, SeenIPv4: "203.0.113.55", SeenIPAt: 1700000000000, } for _, ag := range []*model.AgentNode{nat, pre} { if err := db.Create(ag).Error; err != nil { t.Fatalf("create agent row: %v", err) } } views := toNodeViews([]*model.Node{natNode, preNode}) annotateNodeTypes(views) annotateAgentTelemetry(views) byID := map[int]*NodeView{} for _, v := range views { byID[v.Id] = v } if got := byID[7501].Ipv4; got != "111.91.29.150" { t.Fatalf("NAT node address = %q, want observed public 111.91.29.150 (view %+v)", got, byID[7501]) } if got := byID[7502].Ipv4; got != "203.0.113.55" { t.Fatalf("pre-telemetry node address = %q, want observed 203.0.113.55 (view %+v)", got, byID[7502]) } // The pre-telemetry overlay must not invent other fields. if pv := byID[7502]; pv.XrayVersion != "" || pv.UptimeSecs != 0 || pv.PanelVersion != "" { t.Fatalf("pre-telemetry view picked up telemetry fields: %+v", pv) } }