mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
fix(sub): advertise routable inbound Listen in subscription links
resolveInboundAddress stopped using the inbound's bind Listen in 3.2.5/3.2.6, so a per-inbound Address/IP no longer appeared in generated subscription/share links - they always used the host the subscriber reached the panel on. The frontend QR path still honored Listen, so the panel and the subscription disagreed (issue #4798). Restore advertising Listen when it is a routable host (real IP or hostname), reusing isRoutableHost and excluding unix-domain sockets. Loopback/wildcard binds still fall back to the subscriber host, keeping the earlier loopback-leak fix intact. Precedence is now node address > routable Listen > subscriber host; External Proxy still overrides everything. Closes #4798
This commit is contained in:
parent
f901cd42a5
commit
a40d85ce53
2 changed files with 29 additions and 11 deletions
|
|
@ -713,16 +713,23 @@ func (s *SubService) loadNodes() {
|
||||||
s.nodesByID = m
|
s.nodesByID = m
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveInboundAddress returns the node's address for node-managed inbounds,
|
// resolveInboundAddress picks the host an external client should connect to:
|
||||||
// otherwise the subscriber's host (s.address). The inbound's bind Listen is
|
// 1. node-managed inbound -> the node's address
|
||||||
// deliberately ignored: it's a server-side address, not a client-reachable
|
// 2. an explicit, client-reachable bind Listen -> that Listen
|
||||||
// host, so operators advertise a specific endpoint via External Proxy instead.
|
// 3. otherwise the subscriber's request host (s.address)
|
||||||
|
// A loopback/wildcard bind or a unix-domain-socket listen is a server-side
|
||||||
|
// detail and is never advertised; External Proxy remains the way to advertise
|
||||||
|
// an arbitrary endpoint. Mirrors the frontend's resolveAddr so the panel QR and
|
||||||
|
// the subscription agree.
|
||||||
func (s *SubService) resolveInboundAddress(inbound *model.Inbound) string {
|
func (s *SubService) resolveInboundAddress(inbound *model.Inbound) string {
|
||||||
if inbound.NodeID != nil && s.nodesByID != nil {
|
if inbound.NodeID != nil && s.nodesByID != nil {
|
||||||
if n, ok := s.nodesByID[*inbound.NodeID]; ok && n.Address != "" {
|
if n, ok := s.nodesByID[*inbound.NodeID]; ok && n.Address != "" {
|
||||||
return n.Address
|
return n.Address
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if listen := inbound.Listen; listen != "" && listen[0] != '@' && listen[0] != '/' && isRoutableHost(listen) {
|
||||||
|
return listen
|
||||||
|
}
|
||||||
return s.address
|
return s.address
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,15 +64,26 @@ func TestIsRoutableHost(t *testing.T) {
|
||||||
func TestResolveInboundAddress(t *testing.T) {
|
func TestResolveInboundAddress(t *testing.T) {
|
||||||
const reqHost = "sub.example.com"
|
const reqHost = "sub.example.com"
|
||||||
|
|
||||||
// A subscriber reaches the panel through reqHost; the inbound's own
|
// A routable bind Listen (a real IP or hostname the operator set as the
|
||||||
// bind Listen IP (loopback, private, or even a public secondary IP) is
|
// inbound's advertised endpoint) becomes the link's connect host.
|
||||||
// a server-side detail and must never become the link's connect host.
|
t.Run("routable listen is advertised as the link host", func(t *testing.T) {
|
||||||
t.Run("bind listen IP must not leak into the link host", func(t *testing.T) {
|
|
||||||
s := &SubService{address: reqHost}
|
s := &SubService{address: reqHost}
|
||||||
for _, listen := range []string{"127.0.0.1", "10.0.0.5", "192.168.1.10", "1.2.3.4", "0.0.0.0", "::", "::0", ""} {
|
for _, listen := range []string{"1.2.3.4", "10.0.0.5", "192.168.1.10", "203.0.113.7", "vpn.example.com"} {
|
||||||
|
ib := &model.Inbound{Listen: listen}
|
||||||
|
if got := s.resolveInboundAddress(ib); got != listen {
|
||||||
|
t.Fatalf("listen %q: address = %q, want %q (advertised listen)", listen, got, listen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// A loopback/wildcard bind or a unix-domain-socket listen is a
|
||||||
|
// server-side detail and must never leak into the link host.
|
||||||
|
t.Run("non-routable listen falls back to subscriber host", func(t *testing.T) {
|
||||||
|
s := &SubService{address: reqHost}
|
||||||
|
for _, listen := range []string{"", "0.0.0.0", "::", "::0", "127.0.0.1", "::1", "@fallback", "/run/x.sock"} {
|
||||||
ib := &model.Inbound{Listen: listen}
|
ib := &model.Inbound{Listen: listen}
|
||||||
if got := s.resolveInboundAddress(ib); got != reqHost {
|
if got := s.resolveInboundAddress(ib); got != reqHost {
|
||||||
t.Fatalf("listen %q: address = %q, want %q (subscriber host, not bind IP)", listen, got, reqHost)
|
t.Fatalf("listen %q: address = %q, want %q (subscriber host, not bind detail)", listen, got, reqHost)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -92,7 +103,7 @@ func TestResolveInboundAddress(t *testing.T) {
|
||||||
t.Run("node id with no known node falls back to subscriber host", func(t *testing.T) {
|
t.Run("node id with no known node falls back to subscriber host", func(t *testing.T) {
|
||||||
id := 9
|
id := 9
|
||||||
s := &SubService{address: reqHost, nodesByID: map[int]*model.Node{}}
|
s := &SubService{address: reqHost, nodesByID: map[int]*model.Node{}}
|
||||||
ib := &model.Inbound{NodeID: &id, Listen: "10.0.0.1"}
|
ib := &model.Inbound{NodeID: &id, Listen: "0.0.0.0"}
|
||||||
if got := s.resolveInboundAddress(ib); got != reqHost {
|
if got := s.resolveInboundAddress(ib); got != reqHost {
|
||||||
t.Fatalf("unknown-node address = %q, want subscriber host %q", got, reqHost)
|
t.Fatalf("unknown-node address = %q, want subscriber host %q", got, reqHost)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue