Просмотр исходного кода

fix(sub): don't advertise a leaked client IP for local wildcard inbounds (#5425)

For a local inbound with no node, no custom share address, and a wildcard/blank
listen, resolveInboundAddress fell straight through to the subscriber's request
host. Behind NAT/proxy/CDN that Host can be the requesting client's own IP, so
the subscription wrote the client's address into the inbound instead of the
server's — while the panel's own share link (which doesn't use the request host)
stayed correct.

Prefer the admin's configured public host (Sub/Web domain) over the raw request
host for this last-resort fallback. With no configured host the request host
still stands, so existing single-domain setups are unaffected.
MHSanaei 12 часов назад
Родитель
Сommit
837300b127
2 измененных файлов с 35 добавлено и 4 удалено
  1. 25 0
      internal/sub/build_urls_test.go
  2. 10 4
      internal/sub/service.go

+ 25 - 0
internal/sub/build_urls_test.go

@@ -6,6 +6,7 @@ import (
 	"testing"
 
 	"github.com/mhsanaei/3x-ui/v3/internal/database"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
 )
 
 func initSubDB(t *testing.T) {
@@ -60,6 +61,30 @@ func TestBuildURLs_UsesSubscriberDomain(t *testing.T) {
 	}
 }
 
+// A local wildcard inbound (no node, no custom share address, blank/0.0.0.0
+// listen) must not advertise the raw request host when it carries a client IP
+// that leaked in behind NAT/proxy. The admin's configured panel host wins for
+// this last-resort fallback; without a configured host the request host stands.
+func TestResolveInboundAddress_PrefersConfiguredHostOverClientIP(t *testing.T) {
+	initSubDB(t)
+	local := &model.Inbound{Listen: "", ShareAddrStrategy: "node"}
+
+	s := &SubService{}
+	s.PrepareForRequest("192.168.1.50") // a client LAN IP that reached the panel
+	if got := s.resolveInboundAddress(local); got != "192.168.1.50" {
+		t.Fatalf("with no configured host the request host stands, got %q", got)
+	}
+
+	if err := database.GetDB().Create(&model.Setting{Key: "subDomain", Value: "panel.example.com"}).Error; err != nil {
+		t.Fatalf("set subDomain: %v", err)
+	}
+	s2 := &SubService{}
+	s2.PrepareForRequest("192.168.1.50")
+	if got := s2.resolveInboundAddress(local); got != "panel.example.com" {
+		t.Fatalf("configured host must win over the leaked client IP, got %q", got)
+	}
+}
+
 func TestBuildURLs_EmptySubId(t *testing.T) {
 	initSubDB(t)
 	s := &SubService{}

+ 10 - 4
internal/sub/service.go

@@ -929,10 +929,13 @@ func (s *SubService) loadNodes() {
 //   - "node" (default, and any unknown value): the node's address for
 //     node-managed inbounds, then a routable Listen — the pre-strategy order.
 //
-// Every chain ends at 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 still overrides everything
-// upstream of this call.
+// Every chain ends at the admin's configured public host (Sub/Web domain) and
+// then the subscriber's request host (s.address). Preferring the configured
+// host over the request host for this last resort keeps a wildcard local inbound
+// from advertising a bogus client IP that leaked into the request Host header
+// behind NAT/proxy/CDN (#5425). A loopback/wildcard bind or a unix-domain-socket
+// listen is a server-side detail and is never advertised; External Proxy still
+// overrides everything upstream of this call.
 func (s *SubService) resolveInboundAddress(inbound *model.Inbound) string {
 	var nodeAddr string
 	if inbound.NodeID != nil && s.nodesByID != nil {
@@ -957,6 +960,9 @@ func (s *SubService) resolveInboundAddress(inbound *model.Inbound) string {
 			return c
 		}
 	}
+	if d := s.configuredPublicHost(); d != "" {
+		return d
+	}
 	return s.address
 }