diff --git a/internal/sub/service.go b/internal/sub/service.go index 48dec16d5..f1e58e3e3 100644 --- a/internal/sub/service.go +++ b/internal/sub/service.go @@ -788,23 +788,42 @@ func (s *SubService) loadNodes() { s.nodesByID = m } -// resolveInboundAddress picks the host an external client should connect to: -// 1. node-managed inbound -> the node's address -// 2. an explicit, client-reachable bind Listen -> that Listen -// 3. otherwise the subscriber's request host (s.address) +// resolveInboundAddress picks the host an external client should connect to, +// honoring the inbound's share address strategy the same way the panel's +// share/QR link builder does (#5208): +// - "listen": an explicit, client-reachable bind Listen wins, backed by the +// node's address for node-managed inbounds; +// - "custom": the inbound's ShareAddr wins, then node, then listen; +// - "node" (default, and any unknown value): the node's address for +// node-managed inbounds, then a routable Listen — the pre-strategy order. // -// 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. This subscription path intentionally ignores -// per-inbound share address settings because subscription URLs are panel-owned. +// 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. func (s *SubService) resolveInboundAddress(inbound *model.Inbound) string { + var nodeAddr string if inbound.NodeID != nil && s.nodesByID != nil { - if n, ok := s.nodesByID[*inbound.NodeID]; ok && n.Address != "" { - return n.Address + if n, ok := s.nodesByID[*inbound.NodeID]; ok { + nodeAddr = n.Address } } + var listenAddr string if listen := inbound.Listen; listen != "" && listen[0] != '@' && listen[0] != '/' && isRoutableHost(listen) { - return listen + listenAddr = listen + } + + candidates := []string{nodeAddr, listenAddr} + switch inbound.ShareAddrStrategy { + case "listen": + candidates = []string{listenAddr, nodeAddr} + case "custom": + candidates = []string{strings.TrimSpace(inbound.ShareAddr), nodeAddr, listenAddr} + } + for _, c := range candidates { + if c != "" { + return c + } } return s.address } diff --git a/internal/sub/service_test.go b/internal/sub/service_test.go index e01b052a2..71b5209cd 100644 --- a/internal/sub/service_test.go +++ b/internal/sub/service_test.go @@ -127,6 +127,68 @@ func TestResolveInboundAddress(t *testing.T) { t.Fatalf("unknown-node address = %q, want subscriber host %q", got, reqHost) } }) + + // Per-inbound share address strategy (#5208): subscriptions follow the + // same order as the panel's share/QR links. + t.Run("listen strategy prefers the bind over the node address", func(t *testing.T) { + id := 7 + s := &SubService{ + address: reqHost, + nodesByID: map[int]*model.Node{7: {Id: 7, Address: "node7.example.com"}}, + } + ib := &model.Inbound{NodeID: &id, Listen: "203.0.113.7", ShareAddrStrategy: "listen"} + if got := s.resolveInboundAddress(ib); got != "203.0.113.7" { + t.Fatalf("listen-strategy address = %q, want the bind 203.0.113.7", got) + } + }) + + t.Run("listen strategy falls back to node address on a wildcard bind", func(t *testing.T) { + id := 7 + s := &SubService{ + address: reqHost, + nodesByID: map[int]*model.Node{7: {Id: 7, Address: "node7.example.com"}}, + } + ib := &model.Inbound{NodeID: &id, Listen: "0.0.0.0", ShareAddrStrategy: "listen"} + if got := s.resolveInboundAddress(ib); got != "node7.example.com" { + t.Fatalf("listen-strategy wildcard address = %q, want node7.example.com", got) + } + }) + + t.Run("custom strategy uses the share address", func(t *testing.T) { + id := 7 + s := &SubService{ + address: reqHost, + nodesByID: map[int]*model.Node{7: {Id: 7, Address: "node7.example.com"}}, + } + ib := &model.Inbound{NodeID: &id, Listen: "203.0.113.7", ShareAddrStrategy: "custom", ShareAddr: "edge.example.com"} + if got := s.resolveInboundAddress(ib); got != "edge.example.com" { + t.Fatalf("custom-strategy address = %q, want edge.example.com", got) + } + }) + + t.Run("custom strategy with empty share address falls back to node", func(t *testing.T) { + id := 7 + s := &SubService{ + address: reqHost, + nodesByID: map[int]*model.Node{7: {Id: 7, Address: "node7.example.com"}}, + } + ib := &model.Inbound{NodeID: &id, ShareAddrStrategy: "custom"} + if got := s.resolveInboundAddress(ib); got != "node7.example.com" { + t.Fatalf("custom-strategy fallback address = %q, want node7.example.com", got) + } + }) + + t.Run("node strategy keeps the pre-strategy order", func(t *testing.T) { + id := 7 + s := &SubService{ + address: reqHost, + nodesByID: map[int]*model.Node{7: {Id: 7, Address: "node7.example.com"}}, + } + ib := &model.Inbound{NodeID: &id, Listen: "203.0.113.7", ShareAddrStrategy: "node"} + if got := s.resolveInboundAddress(ib); got != "node7.example.com" { + t.Fatalf("node-strategy address = %q, want node7.example.com", got) + } + }) } func TestUnmarshalStreamSettings(t *testing.T) { diff --git a/internal/web/translation/ar-EG.json b/internal/web/translation/ar-EG.json index fc064137e..1f39393fd 100644 --- a/internal/web/translation/ar-EG.json +++ b/internal/web/translation/ar-EG.json @@ -591,7 +591,7 @@ "getNewSeed": "احصل على Seed جديد", "listenHelp": "يمكنك أيضًا إدخال مسار Unix socket (مثل /run/xray/in.sock) للاستماع على socket بدلاً من منفذ TCP — في هذه الحالة اضبط المنفذ على 0.", "shareAddrStrategy": "استراتيجية عنوان المشاركة", - "shareAddrStrategyHelp": "تحدد العنوان الذي يُكتب في روابط المشاركة المصدّرة ورموز QR. لا تتأثر روابط الاشتراك.", + "shareAddrStrategyHelp": "تحدد العنوان الذي يُكتب في روابط المشاركة المصدّرة ورموز QR ومخرجات الاشتراك.", "shareAddr": "عنوان مشاركة مخصص", "shareAddrHelp": "يُستخدم فقط عندما تكون استراتيجية عنوان المشاركة مخصصة. أدخل اسم مضيف أو عنوان IP بدون بروتوكول أو منفذ.", "shareAddrStrategyOptions": { diff --git a/internal/web/translation/en-US.json b/internal/web/translation/en-US.json index 265736017..bd5ed41dd 100644 --- a/internal/web/translation/en-US.json +++ b/internal/web/translation/en-US.json @@ -592,7 +592,7 @@ "getNewSeed": "Get New Seed", "listenHelp": "You can also enter a Unix socket path (e.g. /run/xray/in.sock) to listen on a socket instead of a TCP port — set Port to 0 in that case.", "shareAddrStrategy": "Share address strategy", - "shareAddrStrategyHelp": "Controls which address is written into exported share links and QR codes. Subscription links are not affected.", + "shareAddrStrategyHelp": "Controls which address is written into exported share links, QR codes, and subscription output.", "shareAddr": "Custom share address", "shareAddrHelp": "Used only when the share address strategy is Custom. Enter a host or IP without a scheme or port.", "shareAddrStrategyOptions": { diff --git a/internal/web/translation/es-ES.json b/internal/web/translation/es-ES.json index debf51b07..6ba89d6e0 100644 --- a/internal/web/translation/es-ES.json +++ b/internal/web/translation/es-ES.json @@ -591,7 +591,7 @@ "getNewSeed": "Obtener nuevo Seed", "listenHelp": "También puedes introducir una ruta de socket Unix (p. ej. /run/xray/in.sock) para escuchar en un socket en lugar de un puerto TCP; en ese caso, establece el Puerto en 0.", "shareAddrStrategy": "Estrategia de dirección para compartir", - "shareAddrStrategyHelp": "Controla qué dirección se escribe en los enlaces compartidos exportados y códigos QR. Los enlaces de suscripción no se ven afectados.", + "shareAddrStrategyHelp": "Controla qué dirección se escribe en los enlaces compartidos exportados, códigos QR y la salida de suscripción.", "shareAddr": "Dirección compartida personalizada", "shareAddrHelp": "Solo se usa cuando la estrategia de dirección para compartir es Personalizada. Introduce un host o IP sin esquema ni puerto.", "shareAddrStrategyOptions": { diff --git a/internal/web/translation/fa-IR.json b/internal/web/translation/fa-IR.json index 2a45d65d3..bdae05085 100644 --- a/internal/web/translation/fa-IR.json +++ b/internal/web/translation/fa-IR.json @@ -591,7 +591,7 @@ "getNewSeed": "دریافت Seed جدید", "listenHelp": "می‌توانید به‌جای پورت TCP یک مسیر سوکت یونیکس وارد کنید (مثلاً /run/xray/in.sock) تا روی سوکت گوش داده شود — در این حالت پورت را روی ۰ بگذارید.", "shareAddrStrategy": "راهبرد آدرس اشتراک‌گذاری", - "shareAddrStrategyHelp": "مشخص می‌کند کدام آدرس در لینک‌های اشتراک‌گذاری خروجی و کدهای QR نوشته شود. لینک‌های اشتراک تحت تأثیر قرار نمی‌گیرند.", + "shareAddrStrategyHelp": "مشخص می‌کند کدام آدرس در لینک‌های اشتراک‌گذاری خروجی، کدهای QR و خروجی اشتراک نوشته شود.", "shareAddr": "آدرس اشتراک‌گذاری سفارشی", "shareAddrHelp": "فقط زمانی استفاده می‌شود که راهبرد آدرس اشتراک‌گذاری روی سفارشی باشد. میزبان یا IP را بدون طرح و پورت وارد کنید.", "shareAddrStrategyOptions": { diff --git a/internal/web/translation/id-ID.json b/internal/web/translation/id-ID.json index fcd2e381a..07242ebea 100644 --- a/internal/web/translation/id-ID.json +++ b/internal/web/translation/id-ID.json @@ -591,7 +591,7 @@ "getNewSeed": "Dapatkan Seed baru", "listenHelp": "Anda juga dapat memasukkan path Unix socket (mis. /run/xray/in.sock) untuk listen pada socket alih-alih port TCP — dalam hal ini setel Port ke 0.", "shareAddrStrategy": "Strategi alamat berbagi", - "shareAddrStrategyHelp": "Menentukan alamat yang ditulis ke tautan berbagi yang diekspor dan kode QR. Tautan langganan tidak terpengaruh.", + "shareAddrStrategyHelp": "Menentukan alamat yang ditulis ke tautan berbagi yang diekspor, kode QR, dan keluaran langganan.", "shareAddr": "Alamat berbagi kustom", "shareAddrHelp": "Hanya digunakan saat strategi alamat berbagi adalah Kustom. Masukkan host atau IP tanpa skema atau port.", "shareAddrStrategyOptions": { diff --git a/internal/web/translation/ja-JP.json b/internal/web/translation/ja-JP.json index 470533caf..74511ac1c 100644 --- a/internal/web/translation/ja-JP.json +++ b/internal/web/translation/ja-JP.json @@ -591,7 +591,7 @@ "getNewSeed": "新しい Seed を取得", "listenHelp": "TCP ポートの代わりに Unix ソケットのパス(例: /run/xray/in.sock)を入力してソケットでリッスンすることもできます。その場合はポートを 0 に設定してください。", "shareAddrStrategy": "共有アドレス戦略", - "shareAddrStrategyHelp": "エクスポートされる共有リンクとQRコードに書き込むアドレスを制御します。サブスクリプションリンクには影響しません。", + "shareAddrStrategyHelp": "エクスポートされる共有リンク、QRコード、サブスクリプション出力に書き込むアドレスを制御します。", "shareAddr": "カスタム共有アドレス", "shareAddrHelp": "共有アドレス戦略がカスタムの場合のみ使用されます。スキームやポートを含めずにホスト名またはIPを入力してください。", "shareAddrStrategyOptions": { diff --git a/internal/web/translation/pt-BR.json b/internal/web/translation/pt-BR.json index 804473c7b..2547dee73 100644 --- a/internal/web/translation/pt-BR.json +++ b/internal/web/translation/pt-BR.json @@ -591,7 +591,7 @@ "getNewSeed": "Obter novo Seed", "listenHelp": "Você também pode informar um caminho de socket Unix (ex.: /run/xray/in.sock) para escutar em um socket em vez de uma porta TCP — nesse caso, defina a Porta como 0.", "shareAddrStrategy": "Estratégia de endereço de compartilhamento", - "shareAddrStrategyHelp": "Controla qual endereço é gravado nos links de compartilhamento exportados e nos códigos QR. Links de assinatura não são afetados.", + "shareAddrStrategyHelp": "Controla qual endereço é gravado nos links de compartilhamento exportados, códigos QR e na saída de assinatura.", "shareAddr": "Endereço de compartilhamento personalizado", "shareAddrHelp": "Usado apenas quando a estratégia de endereço de compartilhamento é Personalizada. Informe um host ou IP sem esquema nem porta.", "shareAddrStrategyOptions": { diff --git a/internal/web/translation/ru-RU.json b/internal/web/translation/ru-RU.json index e7b88250f..a422cd9fd 100644 --- a/internal/web/translation/ru-RU.json +++ b/internal/web/translation/ru-RU.json @@ -591,7 +591,7 @@ "getNewSeed": "Получить новый Seed", "listenHelp": "Можно также указать путь Unix-сокета (например, /run/xray/in.sock), чтобы слушать сокет вместо TCP-порта — в этом случае задайте порт 0.", "shareAddrStrategy": "Стратегия адреса для ссылок", - "shareAddrStrategyHelp": "Определяет, какой адрес записывать в экспортируемые ссылки и QR-коды. Ссылки подписки не затрагиваются.", + "shareAddrStrategyHelp": "Определяет, какой адрес записывать в экспортируемые ссылки, QR-коды и выдачу подписки.", "shareAddr": "Пользовательский адрес для ссылок", "shareAddrHelp": "Используется только когда стратегия адреса для ссылок — пользовательская. Укажите хост или IP без схемы и порта.", "shareAddrStrategyOptions": { diff --git a/internal/web/translation/tr-TR.json b/internal/web/translation/tr-TR.json index 946174a68..9c77a475d 100644 --- a/internal/web/translation/tr-TR.json +++ b/internal/web/translation/tr-TR.json @@ -592,7 +592,7 @@ "getNewSeed": "Yeni Seed Al", "listenHelp": "TCP portu yerine bir Unix soket yolu da girebilirsiniz (örn. /run/xray/in.sock) — bu durumda Port'u 0 olarak ayarlayın.", "shareAddrStrategy": "Paylaşım adresi stratejisi", - "shareAddrStrategyHelp": "Dışa aktarılan paylaşım bağlantılarına ve QR kodlarına hangi adresin yazılacağını belirler. Abonelik bağlantıları etkilenmez.", + "shareAddrStrategyHelp": "Dışa aktarılan paylaşım bağlantılarına, QR kodlarına ve abonelik çıktısına hangi adresin yazılacağını belirler.", "shareAddr": "Özel paylaşım adresi", "shareAddrHelp": "Yalnızca paylaşım adresi stratejisi Özel olduğunda kullanılır. Şema veya port olmadan bir ana makine ya da IP girin.", "shareAddrStrategyOptions": { diff --git a/internal/web/translation/uk-UA.json b/internal/web/translation/uk-UA.json index 7f2d88453..5accfc9d6 100644 --- a/internal/web/translation/uk-UA.json +++ b/internal/web/translation/uk-UA.json @@ -591,7 +591,7 @@ "getNewSeed": "Отримати новий Seed", "listenHelp": "Можна також указати шлях Unix-сокета (наприклад, /run/xray/in.sock), щоб слухати сокет замість TCP-порту — у цьому разі встановіть порт 0.", "shareAddrStrategy": "Стратегія адреси поширення", - "shareAddrStrategyHelp": "Визначає, яку адресу записувати в експортовані посилання поширення та QR-коди. Посилання підписки не змінюються.", + "shareAddrStrategyHelp": "Визначає, яку адресу записувати в експортовані посилання поширення, QR-коди та вивід підписки.", "shareAddr": "Користувацька адреса поширення", "shareAddrHelp": "Використовується лише коли стратегія адреси поширення — користувацька. Введіть хост або IP без схеми та порту.", "shareAddrStrategyOptions": { diff --git a/internal/web/translation/vi-VN.json b/internal/web/translation/vi-VN.json index 847c39649..fc8667475 100644 --- a/internal/web/translation/vi-VN.json +++ b/internal/web/translation/vi-VN.json @@ -591,7 +591,7 @@ "getNewSeed": "Lấy Seed mới", "listenHelp": "Bạn cũng có thể nhập đường dẫn Unix socket (ví dụ /run/xray/in.sock) để lắng nghe trên socket thay vì cổng TCP — khi đó hãy đặt Port là 0.", "shareAddrStrategy": "Chiến lược địa chỉ chia sẻ", - "shareAddrStrategyHelp": "Kiểm soát địa chỉ được ghi vào liên kết chia sẻ đã xuất và mã QR. Liên kết đăng ký không bị ảnh hưởng.", + "shareAddrStrategyHelp": "Kiểm soát địa chỉ được ghi vào liên kết chia sẻ đã xuất, mã QR và nội dung đăng ký.", "shareAddr": "Địa chỉ chia sẻ tùy chỉnh", "shareAddrHelp": "Chỉ dùng khi chiến lược địa chỉ chia sẻ là Tùy chỉnh. Nhập host hoặc IP không kèm giao thức hoặc cổng.", "shareAddrStrategyOptions": { diff --git a/internal/web/translation/zh-CN.json b/internal/web/translation/zh-CN.json index 8a548146c..baaa24542 100644 --- a/internal/web/translation/zh-CN.json +++ b/internal/web/translation/zh-CN.json @@ -591,7 +591,7 @@ "getNewSeed": "获取新 Seed", "listenHelp": "也可以填写 Unix socket 路径(例如 /run/xray/in.sock),以使用套接字而非 TCP 端口监听——此时请将端口设为 0。", "shareAddrStrategy": "分享地址策略", - "shareAddrStrategyHelp": "控制导出分享链接和二维码时写入哪个地址,不影响订阅链接。", + "shareAddrStrategyHelp": "控制导出分享链接、二维码和订阅输出时写入哪个地址。", "shareAddr": "自定义分享地址", "shareAddrHelp": "仅在分享地址策略为自定义时使用。填写不带协议和端口的域名或 IP。", "shareAddrStrategyOptions": { diff --git a/internal/web/translation/zh-TW.json b/internal/web/translation/zh-TW.json index e3c1b0fd6..56ffbcbc9 100644 --- a/internal/web/translation/zh-TW.json +++ b/internal/web/translation/zh-TW.json @@ -591,7 +591,7 @@ "getNewSeed": "取得新 Seed", "listenHelp": "也可以填寫 Unix socket 路徑(例如 /run/xray/in.sock),以使用通訊端而非 TCP 連接埠監聽——此時請將連接埠設為 0。", "shareAddrStrategy": "分享地址策略", - "shareAddrStrategyHelp": "控制匯出分享連結和 QR Code 時寫入哪個地址,不影響訂閱連結。", + "shareAddrStrategyHelp": "控制匯出分享連結、QR Code 和訂閱輸出時寫入哪個地址。", "shareAddr": "自訂分享地址", "shareAddrHelp": "僅在分享地址策略為自訂時使用。填寫不帶協定和連接埠的網域或 IP。", "shareAddrStrategyOptions": {