1
0

8 Коммиты ccd56a56a8 ... f90e4a6962

Автор SHA1 Сообщение Дата
  Grigoriy f90e4a6962 fix(panel): use the hosting node address for WireGuard client configs (#5679) 13 часов назад
  Nebulosa dbdecda03f Env vars example file update (#5678) 14 часов назад
  Volov Vyacheslav 6e0067fca3 docs(settings): clarify Sub Port/Sub Domain double as subscription-link fallback (#5721) 14 часов назад
  Vitaliy Pavlov ed95acdd47 fix(scripts): avoid rpm package upgrades before installs (#5750) 14 часов назад
  MHSanaei 1afab47f04 feat(frontend): show client group in the client info modal 14 часов назад
  MHSanaei 258d8b7344 feat(frontend): add targetStrategy field to the outbound editor 15 часов назад
  MHSanaei 9f760cf0fa fix(frontend): stop group modals clearing selection on background refetch 15 часов назад
  MHSanaei 1bf6f606bc refactor(sub): drop unused subReq parameter from genHy 16 часов назад
38 измененных файлов с 612 добавлено и 124 удалено
  1. 172 14
      .env.example
  2. 17 0
      frontend/public/openapi.json
  3. 4 1
      frontend/src/api/queries/useNodeMutations.ts
  4. 4 0
      frontend/src/generated/examples.ts
  5. 13 0
      frontend/src/generated/schemas.ts
  6. 4 0
      frontend/src/generated/types.ts
  7. 4 0
      frontend/src/generated/zod.ts
  8. 39 19
      frontend/src/lib/xray/inbound-link.ts
  9. 21 8
      frontend/src/lib/xray/outbound-form-adapter.ts
  10. 6 0
      frontend/src/pages/clients/ClientInfoModal.tsx
  11. 2 2
      frontend/src/pages/clients/wireguardConfig.ts
  12. 1 1
      frontend/src/pages/groups/GroupAddClientsModal.tsx
  13. 1 1
      frontend/src/pages/groups/GroupRemoveClientsModal.tsx
  14. 9 0
      frontend/src/pages/xray/outbounds/OutboundFormModal.tsx
  15. 5 0
      frontend/src/pages/xray/outbounds/outbound-form-constants.ts
  16. 7 0
      frontend/src/schemas/client.ts
  17. 4 2
      frontend/src/schemas/forms/outbound-form.ts
  18. 48 0
      frontend/src/test/outbound-form-adapter.test.ts
  19. 37 0
      frontend/src/test/wireguard-client-config.test.ts
  20. 3 3
      install.sh
  21. 2 2
      internal/sub/json_service.go
  22. 45 22
      internal/web/service/inbound.go
  23. 89 0
      internal/web/service/inbound_options_node_address_test.go
  24. 5 3
      internal/web/translation/ar-EG.json
  25. 5 3
      internal/web/translation/en-US.json
  26. 5 3
      internal/web/translation/es-ES.json
  27. 5 3
      internal/web/translation/fa-IR.json
  28. 5 3
      internal/web/translation/id-ID.json
  29. 5 3
      internal/web/translation/ja-JP.json
  30. 5 3
      internal/web/translation/pt-BR.json
  31. 5 3
      internal/web/translation/ru-RU.json
  32. 5 3
      internal/web/translation/tr-TR.json
  33. 5 3
      internal/web/translation/uk-UA.json
  34. 5 3
      internal/web/translation/vi-VN.json
  35. 5 3
      internal/web/translation/zh-CN.json
  36. 5 3
      internal/web/translation/zh-TW.json
  37. 1 1
      update.sh
  38. 9 9
      x-ui.sh

+ 172 - 14
.env.example

@@ -1,19 +1,177 @@
+# This file serves a dual purpose:
+# 1. Developer Bootstrap: The active (uncommented) variables directly below 
+#    configure a safe, unprivileged local environment for 'go run .'. 
+#    This allows 'cp .env.example .env' to work out-of-the-box without root.
+# 2. Production Reference: All available XUI_* configuration options are 
+#    documented and commented out in the reference section further below.
+#
+# 3x-ui reads its runtime configuration from XUI_* environment variables. 
+# On a script install, the installer writes them to the service environment file 
+# (/etc/default/x-ui, /etc/conf.d/x-ui, or /etc/sysconfig/x-ui depending on the distro).
+# For Docker, you set them in docker-compose.yml or via 'docker run -e'.
+#
+# Defaults are sensible — set only what you need to change, then restart:
+# systemctl restart x-ui
+
+# ------------------------------------------------------------------------------
+# LOCAL DEVELOPMENT OVERRIDES (ACTIVE BY DEFAULT)
+# ------------------------------------------------------------------------------
 XUI_DEBUG=true
 XUI_DB_FOLDER=x-ui
 XUI_LOG_FOLDER=x-ui
 XUI_BIN_FOLDER=x-ui
 XUI_INIT_WEB_BASE_PATH=/
-# XUI_PORT=8080
-
-# Optional tunnel health monitor (disabled by default). It periodically probes a
-# URL and restarts xray-core after repeated failures. Point XUI_TUNNEL_HEALTH_PROXY
-# at a local xray inbound so the probe tests the tunnel; without it the probe only
-# checks host connectivity and a restart will not fix host network issues. A restart
-# drops every connected client.
-# XUI_TUNNEL_HEALTH_MONITOR=true
-# XUI_TUNNEL_HEALTH_PROXY=socks5://127.0.0.1:1080
-# XUI_TUNNEL_HEALTH_URL=https://www.cloudflare.com/cdn-cgi/trace
-# XUI_TUNNEL_HEALTH_INTERVAL=30s
-# XUI_TUNNEL_HEALTH_TIMEOUT=10s
-# XUI_TUNNEL_HEALTH_FAILURES=3
-# XUI_TUNNEL_HEALTH_COOLDOWN=5m
+
+# ==============================================================================
+# REFERENCE CONFIGURATION (ALL OPTIONS)
+# ==============================================================================
+
+# ------------------------------------------------------------------------------
+# Database
+# ------------------------------------------------------------------------------
+# Backend database type: sqlite, or postgres (also accepts postgresql / pg)
+# Default: sqlite
+#XUI_DB_TYPE=sqlite
+
+# Folder for the SQLite database file (x-ui.db)
+# Default: /etc/x-ui (Overridden to 'x-ui' in the development block above)
+#XUI_DB_FOLDER=/etc/x-ui
+
+# PostgreSQL connection string (used when XUI_DB_TYPE=postgres)
+# Example: postgres://user:password@localhost:5432/dbname?sslmode=disable
+#XUI_DB_DSN=
+
+# Max open connections in the PostgreSQL pool
+#XUI_DB_MAX_OPEN_CONNS=
+
+# Max idle connections in the PostgreSQL pool
+#XUI_DB_MAX_IDLE_CONNS=
+
+# PostgreSQL Docker Container Settings
+# Default credentials used if you are running PostgreSQL via docker-compose
+#POSTGRES_USER=xui
+#POSTGRES_PASSWORD=xui
+#POSTGRES_DB=xui
+
+
+# ------------------------------------------------------------------------------
+# Panel
+# ------------------------------------------------------------------------------
+# Override the panel port (1–65535). Takes precedence over the stored setting.
+#XUI_PORT=
+
+# Initial web base path on FIRST launch (e.g., /panel)
+# Default: /
+#XUI_INIT_WEB_BASE_PATH=/
+
+# Enable Fail2ban-based IP-limit enforcement
+# Default: true
+#XUI_ENABLE_FAIL2BAN=true
+
+# Skip the HSTS header — set true when TLS is terminated by a reverse proxy
+# Default: false
+#XUI_SKIP_HSTS=false
+
+
+# ------------------------------------------------------------------------------
+# Logging & binaries
+# ------------------------------------------------------------------------------
+# Logging level: debug, info, notice, warning, or error
+# Default: info
+#XUI_LOG_LEVEL=info
+
+# Debug mode. Forces log level to debug, enables Gin debug mode, 
+# and ensures frontend assets are served directly from disk (see CLAUDE.md).
+# Default: false (Overridden to 'true' in the development block at the top)
+#XUI_DEBUG=false
+
+# Log output directory
+# Default: /var/log/x-ui (Overridden to 'x-ui' in the development block above)
+#XUI_LOG_FOLDER=/var/log/x-ui
+
+# Folder for the Xray-core binary and geosite/geoip files
+# Default: bin (Overridden to 'x-ui' in the development block above)
+#XUI_BIN_FOLDER=bin
+
+# Legacy Path Settings
+# Main installation folder (Default: /usr/local/x-ui for Linux, /app for Docker)
+#XUI_MAIN_FOLDER=/usr/local/x-ui
+# Path to the systemd service file (Default: /etc/systemd/system)
+#XUI_SERVICE=/etc/systemd/system
+
+
+# ------------------------------------------------------------------------------
+# Memory & profiling
+# ------------------------------------------------------------------------------
+# Go GC target percentage; lower = less RAM, slightly more CPU.
+# Default: 75
+#XUI_GOGC=
+
+# Minutes between FreeOSMemory calls; 0 disables.
+# Default: 10
+#XUI_MEMORY_RELEASE_INTERVAL=
+
+# Go soft memory limit in MiB
+#XUI_MEMORY_LIMIT=
+
+# Go-syntax soft limit (e.g., 400MiB); takes precedence over XUI_MEMORY_LIMIT
+#GOMEMLIMIT=
+
+# Expose pprof profiling on 127.0.0.1:6060
+# Default: false
+#XUI_PPROF=false
+
+# Automatically set to 'true' inside the official Docker image.
+# Consumed by internal scripts (x-ui.sh) to detect the environment.
+# There is normally no need to set or toggle this variable manually.
+# Default: false (automatically 'true' in Docker environments)
+#XUI_IN_DOCKER=false
+
+
+# ------------------------------------------------------------------------------
+# Xray
+# ------------------------------------------------------------------------------
+# Force VMess AEAD
+# Default: false
+#XRAY_VMESS_AEAD_FORCED=false
+
+
+# ------------------------------------------------------------------------------
+# Tunnel health monitor
+# ------------------------------------------------------------------------------
+# Optional watchdog: probes a URL (optionally through a local Xray inbound)
+# and restarts Xray after repeated failures. A restart drops all connected clients.
+# Default: false
+#XUI_TUNNEL_HEALTH_MONITOR=false
+
+# Proxy to send the probe through, e.g., socks5://127.0.0.1:1080
+# Empty = only checks host connectivity
+#XUI_TUNNEL_HEALTH_PROXY=
+
+# URL to probe
+# Default: https://www.cloudflare.com/cdn-cgi/trace
+#XUI_TUNNEL_HEALTH_URL=https://www.cloudflare.com/cdn-cgi/trace
+
+# Interval between probes
+# Default: 30s
+#XUI_TUNNEL_HEALTH_INTERVAL=30s
+
+# Per-probe timeout
+# Default: 10s
+#XUI_TUNNEL_HEALTH_TIMEOUT=10s
+
+# Consecutive failures before a restart
+# Default: 3
+#XUI_TUNNEL_HEALTH_FAILURES=3
+
+# Minimum delay between restarts
+# Default: 5m
+#XUI_TUNNEL_HEALTH_COOLDOWN=5m
+
+
+# ------------------------------------------------------------------------------
+# Unattended install
+# ------------------------------------------------------------------------------
+# Set to 1 (or run with no TTY) to install with zero prompts.
+# Generated credentials will be written to /etc/x-ui/install-result.env
+#XUI_NONINTERACTIVE=1

+ 17 - 0
frontend/public/openapi.json

@@ -1828,6 +1828,13 @@
             "example": 1,
             "type": "integer"
           },
+          "listen": {
+            "type": "string"
+          },
+          "nodeAddress": {
+            "description": "Share-host resolution inputs, mirroring the subscription's\nresolveInboundAddress so the clients page renders a node-managed WireGuard\nEndpoint that points at the node, not the master panel. NodeAddress is the\nhosting node's externally reachable address (empty for this panel's own\ninbounds); Listen and ShareAddrStrategy/ShareAddr feed the same\nnode→listen→custom fallback the share/QR links already use.",
+            "type": "string"
+          },
           "nodeId": {
             "description": "Hosting node; nil for this panel's own inbounds. Lets the clients\npage map a node filter onto inbound IDs (#4997).",
             "nullable": true,
@@ -1845,6 +1852,12 @@
             "example": "VLESS-443",
             "type": "string"
           },
+          "shareAddr": {
+            "type": "string"
+          },
+          "shareAddrStrategy": {
+            "type": "string"
+          },
           "ssMethod": {
             "type": "string"
           },
@@ -2783,10 +2796,14 @@
                   "obj": [
                     {
                       "id": 1,
+                      "listen": "",
+                      "nodeAddress": "",
                       "nodeId": null,
                       "port": 443,
                       "protocol": "vless",
                       "remark": "VLESS-443",
+                      "shareAddr": "",
+                      "shareAddrStrategy": "",
                       "ssMethod": "",
                       "tag": "in-443-tcp",
                       "tlsFlowCapable": true,

+ 4 - 1
frontend/src/api/queries/useNodeMutations.ts

@@ -24,7 +24,10 @@ export interface RemoteInboundOption {
 
 export function useNodeMutations() {
   const queryClient = useQueryClient();
-  const invalidate = () => queryClient.invalidateQueries({ queryKey: keys.nodes.root() });
+  const invalidate = () => {
+    queryClient.invalidateQueries({ queryKey: keys.nodes.root() });
+    queryClient.invalidateQueries({ queryKey: keys.inbounds.options() });
+  };
 
   const createMut = useMutation({
     mutationFn: (payload: Partial<NodeRecord>) =>

+ 4 - 0
frontend/src/generated/examples.ts

@@ -400,10 +400,14 @@ export const EXAMPLES: Record<string, unknown> = {
   },
   "InboundOption": {
     "id": 1,
+    "listen": "",
+    "nodeAddress": "",
     "nodeId": null,
     "port": 443,
     "protocol": "vless",
     "remark": "VLESS-443",
+    "shareAddr": "",
+    "shareAddrStrategy": "",
     "ssMethod": "",
     "tag": "in-443-tcp",
     "tlsFlowCapable": true,

+ 13 - 0
frontend/src/generated/schemas.ts

@@ -1802,6 +1802,13 @@ export const SCHEMAS: Record<string, unknown> = {
         "example": 1,
         "type": "integer"
       },
+      "listen": {
+        "type": "string"
+      },
+      "nodeAddress": {
+        "description": "Share-host resolution inputs, mirroring the subscription's\nresolveInboundAddress so the clients page renders a node-managed WireGuard\nEndpoint that points at the node, not the master panel. NodeAddress is the\nhosting node's externally reachable address (empty for this panel's own\ninbounds); Listen and ShareAddrStrategy/ShareAddr feed the same\nnode→listen→custom fallback the share/QR links already use.",
+        "type": "string"
+      },
       "nodeId": {
         "description": "Hosting node; nil for this panel's own inbounds. Lets the clients\npage map a node filter onto inbound IDs (#4997).",
         "nullable": true,
@@ -1819,6 +1826,12 @@ export const SCHEMAS: Record<string, unknown> = {
         "example": "VLESS-443",
         "type": "string"
       },
+      "shareAddr": {
+        "type": "string"
+      },
+      "shareAddrStrategy": {
+        "type": "string"
+      },
       "ssMethod": {
         "type": "string"
       },

+ 4 - 0
frontend/src/generated/types.ts

@@ -394,10 +394,14 @@ export interface InboundFallback {
 
 export interface InboundOption {
   id: number;
+  listen?: string;
+  nodeAddress?: string;
   nodeId?: number | null;
   port: number;
   protocol: string;
   remark: string;
+  shareAddr?: string;
+  shareAddrStrategy?: string;
   ssMethod: string;
   tag: string;
   tlsFlowCapable: boolean;

+ 4 - 0
frontend/src/generated/zod.ts

@@ -421,10 +421,14 @@ export type InboundFallback = z.infer<typeof InboundFallbackSchema>;
 
 export const InboundOptionSchema = z.object({
   id: z.number().int(),
+  listen: z.string().optional(),
+  nodeAddress: z.string().optional(),
   nodeId: z.number().int().nullable().optional(),
   port: z.number().int(),
   protocol: z.string(),
   remark: z.string(),
+  shareAddr: z.string().optional(),
+  shareAddrStrategy: z.string().optional(),
   ssMethod: z.string(),
   tag: z.string(),
   tlsFlowCapable: z.boolean(),

+ 39 - 19
frontend/src/lib/xray/inbound-link.ts

@@ -967,33 +967,44 @@ function isShareableHost(host: string): boolean {
   return true;
 }
 
-function shareableListen(inbound: Inbound): string {
-  const listen = inbound.listen.trim();
-  return listen.length > 0 && !isUnixSocketListen(listen) && isShareableHost(listen)
-    ? normalizeShareHost(listen)
+function shareableListenFrom(listen: string): string {
+  const trimmed = listen.trim();
+  return trimmed.length > 0 && !isUnixSocketListen(trimmed) && isShareableHost(trimmed)
+    ? normalizeShareHost(trimmed)
     : '';
 }
 
 type ShareAddrStrategy = 'node' | 'listen' | 'custom';
 
-function shareAddrStrategy(inbound: Inbound): ShareAddrStrategy {
-  const strategy = inbound.shareAddrStrategy;
-  return strategy === 'listen' || strategy === 'custom'
-    ? strategy
-    : 'node';
+function normalizeShareAddrStrategy(strategy: string | undefined): ShareAddrStrategy {
+  return strategy === 'listen' || strategy === 'custom' ? strategy : 'node';
 }
 
-// Orchestrators.
-// resolveAddr picks the host that goes into share/QR links. The default
-// `node` strategy keeps the previous node-address-first behavior for
-// node-managed inbounds; other strategies let a row prefer its listen address
-// or a custom endpoint.
-export function resolveAddr(inbound: Inbound, hostOverride: string, fallbackHostname: string): string {
+// ShareHostFields is the subset of an inbound resolveShareHost needs, so callers
+// holding only a lightweight projection (e.g. the clients page InboundOption)
+// can pick the same host as the full-inbound share/QR path.
+export interface ShareHostFields {
+  listen?: string;
+  shareAddr?: string;
+  shareAddrStrategy?: string;
+}
+
+// resolveShareHost picks the host that goes into share/QR links, the browser-side
+// analog of the backend resolveInboundAddress. hostOverride is the hosting node's
+// address (empty for this panel's own inbounds); fallbackHostname is the
+// already-resolved panel/public host used as the last resort — kept verbatim when
+// it fails normalization (e.g. an underscore intranet hostname) so the last
+// resort never degrades to an empty host.
+export function resolveShareHost(
+  fields: ShareHostFields,
+  hostOverride: string,
+  fallbackHostname: string,
+): string {
   const nodeAddr = normalizeShareHost(hostOverride);
-  const listenAddr = shareableListen(inbound);
-  const customAddr = normalizeShareHost(inbound.shareAddr ?? '');
-  const fallbackAddr = normalizeShareHost(fallbackHostname);
-  switch (shareAddrStrategy(inbound)) {
+  const listenAddr = shareableListenFrom(fields.listen ?? '');
+  const customAddr = normalizeShareHost(fields.shareAddr ?? '');
+  const fallbackAddr = normalizeShareHost(fallbackHostname) || fallbackHostname.trim();
+  switch (normalizeShareAddrStrategy(fields.shareAddrStrategy)) {
     case 'listen':
       return listenAddr || nodeAddr || fallbackAddr;
     case 'custom':
@@ -1003,6 +1014,15 @@ export function resolveAddr(inbound: Inbound, hostOverride: string, fallbackHost
   }
 }
 
+// Orchestrators.
+// resolveAddr picks the host that goes into share/QR links. The default
+// `node` strategy keeps the previous node-address-first behavior for
+// node-managed inbounds; other strategies let a row prefer its listen address
+// or a custom endpoint.
+export function resolveAddr(inbound: Inbound, hostOverride: string, fallbackHostname: string): string {
+  return resolveShareHost(inbound, hostOverride, fallbackHostname);
+}
+
 // A loopback browser host means the panel was reached through a tunnel (e.g.
 // SSH-forwarded 127.0.0.1/localhost), so it can never be a shareable link host.
 function isLoopbackHost(host: string): boolean {

+ 21 - 8
frontend/src/lib/xray/outbound-form-adapter.ts

@@ -1,7 +1,9 @@
 import { XHttpXmuxSchema } from '@/schemas/protocols/stream/xhttp';
+import { OutboundDomainStrategySchema } from '@/schemas/protocols/outbound';
 import { normalizeStreamSettingsForWire } from '@/lib/xray/stream-wire-normalize';
 import { Wireguard } from '@/utils';
 import type { Sniffing, SniffingDest } from '@/schemas/primitives';
+import type { OutboundDomainStrategy } from '@/schemas/protocols/outbound';
 
 import type {
   DnsOutboundFormSettings,
@@ -56,6 +58,16 @@ function asPort(value: unknown, fallback: number): number {
   return n;
 }
 
+// xray-core matches targetStrategy/domainStrategy case-insensitively;
+// normalize the wire value to the canonical spelling or '' (= AsIs).
+function targetStrategyFromWire(value: unknown): OutboundDomainStrategy | '' {
+  const s = asString(value);
+  if (!s) return '';
+  return OutboundDomainStrategySchema.options.find(
+    (v) => v.toLowerCase() === s.toLowerCase(),
+  ) ?? '';
+}
+
 const SNIFFING_DEST_VALUES: readonly SniffingDest[] = ['http', 'tls', 'quic', 'fakedns'];
 
 const SNIFFING_DEFAULT: Sniffing = {
@@ -285,14 +297,9 @@ function freedomFromWire(raw: Raw): FreedomOutboundFormSettings {
     && typeof raw.fragment === 'object'
     && Object.keys(fragment).length > 0;
   return {
-    domainStrategy: ((): FreedomOutboundFormSettings['domainStrategy'] => {
-      const allowed = [
-        'AsIs', 'UseIP', 'UseIPv4', 'UseIPv6', 'UseIPv6v4', 'UseIPv4v6',
-        'ForceIP', 'ForceIPv6v4', 'ForceIPv6', 'ForceIPv4v6', 'ForceIPv4',
-      ];
-      const s = asString(raw.domainStrategy);
-      return (allowed.includes(s) ? s : '') as FreedomOutboundFormSettings['domainStrategy'];
-    })(),
+    domainStrategy: targetStrategyFromWire(
+      asString(raw.targetStrategy) || asString(raw.domainStrategy),
+    ),
     redirect: asString(raw.redirect),
     userLevel: asNumber(raw.userLevel, 0),
     proxyProtocol: ((): FreedomOutboundFormSettings['proxyProtocol'] => {
@@ -374,6 +381,7 @@ export interface RawOutboundRow {
   tag?: string;
   protocol?: string;
   sendThrough?: string;
+  targetStrategy?: string;
   settings?: unknown;
   streamSettings?: unknown;
   mux?: unknown;
@@ -401,6 +409,7 @@ export function rawOutboundToFormValues(raw: RawOutboundRow): OutboundFormValues
   const settings = asObject(raw.settings);
   const tag = asString(raw.tag);
   const sendThrough = asString(raw.sendThrough);
+  const targetStrategy = targetStrategyFromWire(raw.targetStrategy);
   const mux = muxFromWire(raw.mux);
   const hasStream = raw.streamSettings
     && typeof raw.streamSettings === 'object'
@@ -430,6 +439,7 @@ export function rawOutboundToFormValues(raw: RawOutboundRow): OutboundFormValues
     ...typed,
     tag,
     sendThrough,
+    targetStrategy,
     mux,
     streamSettings,
   };
@@ -543,6 +553,8 @@ function hysteriaToWire(s: HysteriaOutboundFormSettings) {
 }
 
 function freedomToWire(s: FreedomOutboundFormSettings) {
+  // The strategy is emitted under the legacy domainStrategy key: new cores
+  // fall back to it when targetStrategy is absent, old cores only know it.
   // Legacy semantics: emit fragment only when the user actually populated
   // at least one of the four sub-fields. Defaults like packets='1-3' alone
   // are not enough — the modal's Fragment Switch sets all four together.
@@ -672,6 +684,7 @@ export function formValuesToWirePayload(values: OutboundFormValues): WireOutboun
     settings,
   };
   if (values.tag) result.tag = values.tag;
+  if (values.targetStrategy) result.targetStrategy = values.targetStrategy;
 
   // streamSettings emission gates on canEnableStream — non-stream protocols
   // still emit just `sockopt` if that key is present (legacy behavior).

+ 6 - 0
frontend/src/pages/clients/ClientInfoModal.tsx

@@ -308,6 +308,12 @@ export default function ClientInfoModal({
                   <td>{t('pages.inbounds.updatedAt')}</td>
                   <td><Tag>{dateLabel(client.updatedAt)}</Tag></td>
                 </tr>
+                {client.group && (
+                  <tr>
+                    <td>{t('pages.clients.group')}</td>
+                    <td><Tag color="geekblue">{client.group}</Tag></td>
+                  </tr>
+                )}
                 {client.comment && (
                   <tr>
                     <td>{t('pages.clients.comment')}</td>

+ 2 - 2
frontend/src/pages/clients/wireguardConfig.ts

@@ -1,5 +1,5 @@
 import { formatInboundLabel } from '@/lib/inbounds/label';
-import { preferPublicHost } from '@/lib/xray/inbound-link';
+import { preferPublicHost, resolveShareHost } from '@/lib/xray/inbound-link';
 import type { ClientRecord, InboundOption } from '@/hooks/useClients';
 
 export function isWireguardClient(client: ClientRecord | null | undefined): boolean {
@@ -22,7 +22,7 @@ export function buildWireguardClientConfig(
   host = window.location.hostname,
   publicHost = '',
 ): string {
-  const endpointHost = preferPublicHost(host, publicHost);
+  const endpointHost = resolveShareHost(inbound ?? {}, inbound?.nodeAddress ?? '', preferPublicHost(host, publicHost));
   const address = client.allowedIPs || '10.0.0.2/32';
   const endpoint = `${endpointHost}:${inbound?.port || ''}`;
   const inboundName = inbound ? formatInboundLabel(inbound.tag, inbound.remark) : '';

+ 1 - 1
frontend/src/pages/groups/GroupAddClientsModal.tsx

@@ -50,7 +50,7 @@ export default function GroupAddClientsModal({
     if (!open) return;
     setSelectedEmails([]);
     setSearch('');
-  }, [open, rows]);
+  }, [open]);
 
   const filteredRows = useMemo(() => {
     const q = search.trim().toLowerCase();

+ 1 - 1
frontend/src/pages/groups/GroupRemoveClientsModal.tsx

@@ -48,7 +48,7 @@ export default function GroupRemoveClientsModal({
     if (!open) return;
     setSelectedEmails([]);
     setSearch('');
-  }, [open, rows]);
+  }, [open]);
 
   const filteredRows = useMemo(() => {
     const q = search.trim().toLowerCase();

+ 9 - 0
frontend/src/pages/xray/outbounds/OutboundFormModal.tsx

@@ -39,6 +39,7 @@ import {
   NETWORK_OPTIONS,
   PROTOCOL_OPTIONS,
   SERVER_PROTOCOLS,
+  TARGET_STRATEGY_OPTIONS,
 } from './outbound-form-constants';
 import {
   applyNetworkChange,
@@ -394,6 +395,14 @@ export default function OutboundFormModal({
                       <Input placeholder={t('pages.xray.outboundForm.localIpPlaceholder')} />
                     </Form.Item>
 
+                    <Form.Item
+                      label={t('pages.xray.outbound.targetStrategy')}
+                      name="targetStrategy"
+                      tooltip={t('pages.xray.outboundForm.targetStrategyHint')}
+                    >
+                      <Select allowClear placeholder="AsIs" options={TARGET_STRATEGY_OPTIONS} />
+                    </Form.Item>
+
                     {SERVER_PROTOCOLS.has(protocol) && <ServerTarget />}
                     {protocol === 'vmess' && <VmessFields />}
                     {protocol === 'vless' && <VlessFields />}

+ 5 - 0
frontend/src/pages/xray/outbounds/outbound-form-constants.ts

@@ -7,6 +7,7 @@ import {
   USERS_SECURITY,
   UTLS_FINGERPRINT,
 } from '@/schemas/primitives';
+import { OutboundDomainStrategySchema } from '@/schemas/protocols/outbound';
 import { SSMethodSchema } from '@/schemas/protocols/shared/shadowsocks';
 
 export const PROTOCOL_OPTIONS = Object.values(Protocols).map((p) => ({ value: p, label: p }));
@@ -20,6 +21,10 @@ export const ADDRESS_PORT_STRATEGY_OPTIONS = Object.values(Address_Port_Strategy
   value: v,
   label: v,
 }));
+export const TARGET_STRATEGY_OPTIONS = OutboundDomainStrategySchema.options.map((v) => ({
+  value: v,
+  label: v,
+}));
 
 // canEnableMux mirrors the adapter's helper but lives here so the modal
 // can show/hide the Mux section without going through the adapter.

+ 7 - 0
frontend/src/schemas/client.ts

@@ -54,6 +54,13 @@ export const InboundOptionSchema = z.object({
   wgDns: z.string().optional(),
   // Hosting node id; absent/null for this panel's own inbounds (#4997).
   nodeId: z.number().nullable().optional(),
+  // Share-host resolution inputs, mirroring the backend resolveInboundAddress so
+  // the clients page picks the same WireGuard endpoint host as the subscription:
+  // the hosting node address, the inbound listen, and its share-address strategy.
+  nodeAddress: z.string().optional(),
+  listen: z.string().optional(),
+  shareAddr: z.string().optional(),
+  shareAddrStrategy: z.string().optional(),
 }).loose();
 
 export const InboundOptionsSchema = z.array(InboundOptionSchema);

+ 4 - 2
frontend/src/schemas/forms/outbound-form.ts

@@ -219,11 +219,13 @@ export const OutboundStreamFormSchema = NetworkSettingsSchema
   .and(StreamExtrasSchema);
 export type OutboundStreamFormValues = z.infer<typeof OutboundStreamFormSchema>;
 
-// Top-level form base: identity (tag, sendThrough), then the per-protocol
-// settings DU, then the stream sub-form, then mux.
+// Top-level form base: identity (tag, sendThrough, targetStrategy), then
+// the per-protocol settings DU, then the stream sub-form, then mux.
+// targetStrategy '' means AsIs (omitted from wire).
 export const OutboundFormBaseSchema = z.object({
   tag: z.string().default(''),
   sendThrough: z.string().default(''),
+  targetStrategy: z.union([OutboundDomainStrategySchema, z.literal('')]).default(''),
   streamSettings: OutboundStreamFormSchema.optional(),
   mux: MuxFormSchema.default({
     enabled: false,

+ 48 - 0
frontend/src/test/outbound-form-adapter.test.ts

@@ -420,6 +420,54 @@ describe('outbound-form-adapter: round-trip', () => {
   });
 });
 
+describe('outbound-form-adapter: targetStrategy', () => {
+  it('round-trips a top-level targetStrategy', () => {
+    const back = formValuesToWirePayload(rawOutboundToFormValues({
+      protocol: 'vless',
+      settings: { address: 's', port: 443, id: '11111111-2222-4333-8444-555555555555', flow: '', encryption: 'none' },
+      targetStrategy: 'ForceIPv6v4',
+    }));
+    expect(back.targetStrategy).toBe('ForceIPv6v4');
+  });
+
+  it('normalizes wire case to the canonical spelling (core matches case-insensitively)', () => {
+    const form = rawOutboundToFormValues({
+      protocol: 'freedom',
+      settings: {},
+      targetStrategy: 'useipv4v6',
+    });
+    expect(form.targetStrategy).toBe('UseIPv4v6');
+  });
+
+  it('omits targetStrategy when unset and drops unknown values', () => {
+    const unset = formValuesToWirePayload(rawOutboundToFormValues({
+      protocol: 'freedom',
+      settings: {},
+    }));
+    expect(unset).not.toHaveProperty('targetStrategy');
+
+    const invalid = formValuesToWirePayload(rawOutboundToFormValues({
+      protocol: 'freedom',
+      settings: {},
+      targetStrategy: 'UseIPv5',
+    }));
+    expect(invalid).not.toHaveProperty('targetStrategy');
+  });
+
+  it('freedom prefers settings.targetStrategy over domainStrategy and emits the legacy key', () => {
+    const form = rawOutboundToFormValues({
+      protocol: 'freedom',
+      settings: { targetStrategy: 'UseIPv6', domainStrategy: 'UseIPv4' },
+    });
+    if (form.protocol === 'freedom') {
+      expect(form.settings.domainStrategy).toBe('UseIPv6');
+    }
+    const back = formValuesToWirePayload(form);
+    expect(back.settings).toMatchObject({ domainStrategy: 'UseIPv6' });
+    expect(back.settings).not.toHaveProperty('targetStrategy');
+  });
+});
+
 describe('outbound-form-adapter: xhttp xmux toggle', () => {
   const xmuxWire = {
     protocol: 'vless',

+ 37 - 0
frontend/src/test/wireguard-client-config.test.ts

@@ -52,4 +52,41 @@ describe('buildWireguardClientConfig', () => {
     const cfg = buildWireguardClientConfig({ ...client, preSharedKey: undefined }, inbound, 'example.com', '');
     expect(cfg).not.toContain('PresharedKey');
   });
+
+  it('uses the hosting node address as the endpoint host for node-managed inbounds', () => {
+    const cfg = buildWireguardClientConfig(client, { ...inbound, nodeAddress: 'node.example.net' }, 'master.example.com', '');
+    expect(cfg).toContain('Endpoint = node.example.net:51820');
+    expect(cfg).not.toContain('master.example.com');
+  });
+
+  it('falls back to the panel host when the node address is blank', () => {
+    const cfg = buildWireguardClientConfig(client, { ...inbound, nodeAddress: '   ' }, 'master.example.com', '');
+    expect(cfg).toContain('Endpoint = master.example.com:51820');
+  });
+
+  it('honors the custom share-address strategy over the node address', () => {
+    const cfg = buildWireguardClientConfig(
+      client,
+      { ...inbound, nodeAddress: 'node.example.net', shareAddrStrategy: 'custom', shareAddr: 'vpn.example.com' },
+      'master.example.com',
+      '',
+    );
+    expect(cfg).toContain('Endpoint = vpn.example.com:51820');
+  });
+
+  it('honors the listen share-address strategy over the node address', () => {
+    const cfg = buildWireguardClientConfig(
+      client,
+      { ...inbound, nodeAddress: 'node.example.net', shareAddrStrategy: 'listen', listen: '198.51.100.7' },
+      'master.example.com',
+      '',
+    );
+    expect(cfg).toContain('Endpoint = 198.51.100.7:51820');
+  });
+
+  it('keeps a panel hostname that fails share-host normalization instead of emitting an empty endpoint', () => {
+    const cfg = buildWireguardClientConfig(client, { ...inbound, listen: '0.0.0.0' }, 'wg_gw.corp.lan', '');
+    expect(cfg).toContain('Endpoint = wg_gw.corp.lan:51820');
+    expect(cfg).not.toContain('Endpoint = :51820');
+  });
 });

+ 3 - 3
install.sh

@@ -101,13 +101,13 @@ install_base() {
             apt-get update && apt-get install -y -q cron curl tar tzdata socat ca-certificates openssl
             ;;
         fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
-            dnf -y update && dnf install -y -q cronie curl tar tzdata socat ca-certificates openssl
+            dnf makecache -y && dnf install -y -q cronie curl tar tzdata socat ca-certificates openssl
             ;;
         centos)
             if [[ "${VERSION_ID}" =~ ^7 ]]; then
-                yum -y update && yum install -y cronie curl tar tzdata socat ca-certificates openssl
+                yum makecache -y && yum install -y cronie curl tar tzdata socat ca-certificates openssl
             else
-                dnf -y update && dnf install -y -q cronie curl tar tzdata socat ca-certificates openssl
+                dnf makecache -y && dnf install -y -q cronie curl tar tzdata socat ca-certificates openssl
             fi
             ;;
         arch | manjaro | parch)

+ 2 - 2
internal/sub/json_service.go

@@ -216,7 +216,7 @@ func (s *SubJsonService) getConfig(subReq *SubService, inbound *model.Inbound, c
 		case "trojan", "shadowsocks":
 			newOutbounds = append(newOutbounds, s.genServer(subReq, inbound, streamSettings, client, jsonMux(mux, hostMux)))
 		case "hysteria":
-			newOutbounds = append(newOutbounds, s.genHy(subReq, inbound, newStream, client, jsonMux(mux, hostMux)))
+			newOutbounds = append(newOutbounds, s.genHy(inbound, newStream, client, jsonMux(mux, hostMux)))
 		}
 
 		newOutbounds = append(newOutbounds, s.defaultOutbounds...)
@@ -473,7 +473,7 @@ func (s *SubJsonService) genServer(subReq *SubService, inbound *model.Inbound, s
 	return result
 }
 
-func (s *SubJsonService) genHy(subReq *SubService, inbound *model.Inbound, newStream map[string]any, client model.Client, mux string) json_util.RawMessage {
+func (s *SubJsonService) genHy(inbound *model.Inbound, newStream map[string]any, client model.Client, mux string) json_util.RawMessage {
 	outbound := Outbound{}
 
 	outbound.Protocol = string(inbound.Protocol)

+ 45 - 22
internal/web/service/inbound.go

@@ -305,24 +305,39 @@ type InboundOption struct {
 	// Hosting node; nil for this panel's own inbounds. Lets the clients
 	// page map a node filter onto inbound IDs (#4997).
 	NodeId *int `json:"nodeId,omitempty"`
+	// Share-host resolution inputs, mirroring the subscription's
+	// resolveInboundAddress so the clients page renders a node-managed WireGuard
+	// Endpoint that points at the node, not the master panel. NodeAddress is the
+	// hosting node's externally reachable address (empty for this panel's own
+	// inbounds); Listen and ShareAddrStrategy/ShareAddr feed the same
+	// node→listen→custom fallback the share/QR links already use.
+	NodeAddress       string `json:"nodeAddress,omitempty"`
+	Listen            string `json:"listen,omitempty"`
+	ShareAddr         string `json:"shareAddr,omitempty"`
+	ShareAddrStrategy string `json:"shareAddrStrategy,omitempty"`
 }
 
 func (s *InboundService) GetInboundOptions(userId int) ([]InboundOption, error) {
 	db := database.GetDB()
 	var rows []struct {
-		Id             int    `gorm:"column:id"`
-		Remark         string `gorm:"column:remark"`
-		Tag            string `gorm:"column:tag"`
-		Protocol       string `gorm:"column:protocol"`
-		Port           int    `gorm:"column:port"`
-		StreamSettings string `gorm:"column:stream_settings"`
-		Settings       string `gorm:"column:settings"`
-		NodeId         *int   `gorm:"column:node_id"`
+		Id                int    `gorm:"column:id"`
+		Remark            string `gorm:"column:remark"`
+		Tag               string `gorm:"column:tag"`
+		Protocol          string `gorm:"column:protocol"`
+		Port              int    `gorm:"column:port"`
+		StreamSettings    string `gorm:"column:stream_settings"`
+		Settings          string `gorm:"column:settings"`
+		Listen            string `gorm:"column:listen"`
+		ShareAddr         string `gorm:"column:share_addr"`
+		ShareAddrStrategy string `gorm:"column:share_addr_strategy"`
+		NodeId            *int   `gorm:"column:node_id"`
+		NodeAddress       string `gorm:"column:node_address"`
 	}
 	err := db.Table("inbounds").
-		Select("id, remark, tag, protocol, port, stream_settings, settings, node_id").
-		Where("user_id = ?", userId).
-		Order("id ASC").
+		Select("inbounds.id, inbounds.remark, inbounds.tag, inbounds.protocol, inbounds.port, inbounds.stream_settings, inbounds.settings, inbounds.listen, inbounds.share_addr, inbounds.share_addr_strategy, inbounds.node_id, COALESCE(nodes.address, '') AS node_address").
+		Joins("LEFT JOIN nodes ON nodes.id = inbounds.node_id").
+		Where("inbounds.user_id = ?", userId).
+		Order("inbounds.id ASC").
 		Scan(&rows).Error
 	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
 		return nil, err
@@ -330,18 +345,26 @@ func (s *InboundService) GetInboundOptions(userId int) ([]InboundOption, error)
 	out := make([]InboundOption, 0, len(rows))
 	for _, r := range rows {
 		wgPublicKey, wgMtu, wgDns := inboundWireguardHints(r.Protocol, r.Settings)
+		shareAddrStrategy := r.ShareAddrStrategy
+		if shareAddrStrategy == "node" {
+			shareAddrStrategy = ""
+		}
 		out = append(out, InboundOption{
-			Id:             r.Id,
-			Remark:         r.Remark,
-			Tag:            r.Tag,
-			Protocol:       r.Protocol,
-			Port:           r.Port,
-			TlsFlowCapable: inboundCanEnableTlsFlow(r.Protocol, r.StreamSettings, r.Settings),
-			SsMethod:       inboundShadowsocksMethod(r.Protocol, r.Settings),
-			WgPublicKey:    wgPublicKey,
-			WgMtu:          wgMtu,
-			WgDns:          wgDns,
-			NodeId:         r.NodeId,
+			Id:                r.Id,
+			Remark:            r.Remark,
+			Tag:               r.Tag,
+			Protocol:          r.Protocol,
+			Port:              r.Port,
+			TlsFlowCapable:    inboundCanEnableTlsFlow(r.Protocol, r.StreamSettings, r.Settings),
+			SsMethod:          inboundShadowsocksMethod(r.Protocol, r.Settings),
+			WgPublicKey:       wgPublicKey,
+			WgMtu:             wgMtu,
+			WgDns:             wgDns,
+			NodeId:            r.NodeId,
+			NodeAddress:       r.NodeAddress,
+			Listen:            r.Listen,
+			ShareAddr:         r.ShareAddr,
+			ShareAddrStrategy: shareAddrStrategy,
 		})
 	}
 	return out, nil

+ 89 - 0
internal/web/service/inbound_options_node_address_test.go

@@ -0,0 +1,89 @@
+package service
+
+import (
+	"testing"
+
+	"github.com/mhsanaei/3x-ui/v3/internal/database"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+)
+
+// TestGetInboundOptions_NodeAddress verifies that a node-managed inbound carries
+// its hosting node's externally reachable address, while this panel's own
+// inbounds report an empty NodeAddress. The clients page uses it as the
+// WireGuard endpoint host so a copied config points at the node, not the master.
+func TestGetInboundOptions_NodeAddress(t *testing.T) {
+	setupConflictDB(t)
+
+	node := &model.Node{Name: "de-fra-1", Address: "node.example.net", Port: 2053, Enable: true}
+	if err := database.GetDB().Create(node).Error; err != nil {
+		t.Fatalf("create node: %v", err)
+	}
+
+	nodeInbound := &model.Inbound{
+		UserId:   1,
+		Tag:      "in-51820-udp",
+		Enable:   true,
+		Listen:   "0.0.0.0",
+		Port:     51820,
+		Protocol: model.WireGuard,
+		Settings: `{"clients":[],"secretKey":"QGVlb2dXc1ZTWGw0ZXBzZndsWmtMaUM5MUlNYjBHWFdYbz0="}`,
+		NodeID:   &node.Id,
+	}
+	localInbound := &model.Inbound{
+		UserId:            1,
+		Tag:               "in-443-tcp",
+		Enable:            true,
+		Listen:            "0.0.0.0",
+		Port:              443,
+		Protocol:          model.VLESS,
+		StreamSettings:    `{"network":"tcp"}`,
+		Settings:          `{"clients":[]}`,
+		ShareAddrStrategy: "custom",
+		ShareAddr:         "vpn.example.com",
+	}
+	if err := database.GetDB().Create(nodeInbound).Error; err != nil {
+		t.Fatalf("create node inbound: %v", err)
+	}
+	if err := database.GetDB().Create(localInbound).Error; err != nil {
+		t.Fatalf("create local inbound: %v", err)
+	}
+
+	svc := &InboundService{}
+	options, err := svc.GetInboundOptions(1)
+	if err != nil {
+		t.Fatalf("GetInboundOptions: %v", err)
+	}
+
+	byID := make(map[int]InboundOption, len(options))
+	for _, o := range options {
+		byID[o.Id] = o
+	}
+
+	got, ok := byID[nodeInbound.Id]
+	if !ok {
+		t.Fatalf("node inbound %d missing from options", nodeInbound.Id)
+	}
+	if got.NodeAddress != "node.example.net" {
+		t.Fatalf("node inbound NodeAddress = %q, want node.example.net", got.NodeAddress)
+	}
+	if got.Listen != "0.0.0.0" {
+		t.Fatalf("node inbound Listen = %q, want 0.0.0.0", got.Listen)
+	}
+	if got.ShareAddrStrategy != "" {
+		t.Fatalf("node inbound ShareAddrStrategy = %q, want empty (the default node strategy is elided so omitempty drops it)", got.ShareAddrStrategy)
+	}
+
+	local, ok := byID[localInbound.Id]
+	if !ok {
+		t.Fatalf("local inbound %d missing from options", localInbound.Id)
+	}
+	if local.NodeAddress != "" {
+		t.Fatalf("local inbound NodeAddress = %q, want empty", local.NodeAddress)
+	}
+	if local.ShareAddrStrategy != "custom" {
+		t.Fatalf("local inbound ShareAddrStrategy = %q, want custom", local.ShareAddrStrategy)
+	}
+	if local.ShareAddr != "vpn.example.com" {
+		t.Fatalf("local inbound ShareAddr = %q, want vpn.example.com", local.ShareAddr)
+	}
+}

+ 5 - 3
internal/web/translation/ar-EG.json

@@ -1206,7 +1206,7 @@
       "subListen": "IP الاستماع",
       "subListenDesc": "عنوان IP لخدمة الاشتراك. (سيبه فاضي عشان يستمع على كل الـ IPs)",
       "subPort": "بورت الاستماع",
-      "subPortDesc": "رقم البورت لخدمة الاشتراك. (لازم يكون بورت فاضي)",
+      "subPortDesc": "رقم البورت لخدمة الاشتراك. (لازم يكون بورت فاضي). كمان بيتحسب منه رابط الاشتراك/الـ QR اللي بيظهر في اللوحة لو حقل «مسار البروكسي العكسي» تحت فاضي — لو الاشتراك بيتفتح من ورا بروكسي عكسي على بورت مختلف، املا «مسار البروكسي العكسي» بدل كده.",
       "subCertPath": "مسار المفتاح العام",
       "subCertPathDesc": "مسار ملف المفتاح العام لخدمة الاشتراك. (يبدأ بـ '/')",
       "subKeyPath": "مسار المفتاح الخاص",
@@ -1214,13 +1214,13 @@
       "subPath": "مسار URI",
       "subPathDesc": "مسار URI لخدمة الاشتراك. (يبدأ بـ '/' وبينتهي بـ '/')",
       "subDomain": "دومين الاستماع",
-      "subDomainDesc": "اسم الدومين لخدمة الاشتراك. (سيبه فاضي عشان يستمع على كل الدومينات والـ IPs)",
+      "subDomainDesc": "اسم الدومين لخدمة الاشتراك. (سيبه فاضي عشان يستمع على كل الدومينات والـ IPs). كمان بيتستخدم كدومين افتراضي لرابط الاشتراك اللي بيظهر لو حقل «مسار البروكسي العكسي» فاضي — املا «مسار البروكسي العكسي» لو اللوحة والاشتراك بيتفتحوا من دومينات مختلفة (زي لما يكونوا ورا بروكسي عكسي).",
       "subUpdates": "فترات التحديث",
       "subUpdatesDesc": "فترات تحديث رابط الاشتراك في تطبيقات العملاء. (الوحدة: ساعة)",
       "subEncrypt": "تشفير",
       "subEncryptDesc": "المحتوى اللي هيترجع من خدمة الاشتراك هيكون مشفر بـ Base64.",
       "subURI": "مسار البروكسي العكسي",
-      "subURIDesc": "مسار URI لرابط الاشتراك عشان تستخدمه ورا البروكسي.",
+      "subURIDesc": "الرابط الأساسي الكامل (scheme://domain[:port]/path/) لرابط الاشتراك وكود الـQR، بيتستخدم بدل دومين الاستماع/بورت الاستماع. املا الحقل ده لو الاشتراك بيتفتح من ورا بروكسي عكسي أو على دومين/بورت مختلف عن اللي فوق.",
       "externalTrafficInformEnable": "تنبيه الترافيك الخارجي",
       "externalTrafficInformEnableDesc": "إخطار واجهة API خارجية بكل تحديث لحركة المرور.",
       "externalTrafficInformURI": "مسار تنبيه الترافيك الخارجي",
@@ -1549,6 +1549,7 @@
         "localIpPlaceholder": "IP محلي",
         "dialerProxyPlaceholder": "اختر مخرجًا لتمرير الاتصال عبره",
         "dialerProxyHint": "وجّه هذا المخرج عبر مخرج آخر (حسب الوسم) لبناء سلسلة بروكسي. اتركه فارغًا للاتصال المباشر.",
+        "targetStrategyHint": "كيفية حلّ نطاق الوجهة قبل الاتصال: AsIs (الافتراضي) يرسله كما هو، UseIP… يحلّه مع الرجوع عند الفشل، ForceIP… يشترط نجاح الحلّ.",
         "addressRequired": "العنوان مطلوب",
         "portRequired": "المنفذ مطلوب",
         "optional": "اختياري",
@@ -1618,6 +1619,7 @@
         "accountInfo": "معلومات الحساب",
         "outboundStatus": "حالة المخرج",
         "sendThrough": "أرسل من خلال",
+        "targetStrategy": "استراتيجية الوجهة",
         "test": "اختبار",
         "testResult": "نتيجة الاختبار",
         "testing": "جاري اختبار الاتصال...",

+ 5 - 3
internal/web/translation/en-US.json

@@ -1324,7 +1324,7 @@
       "subListen": "Listen IP",
       "subListenDesc": "The IP address for the subscription service. (leave blank to listen on all IPs)",
       "subPort": "Listen Port",
-      "subPortDesc": "The port number for the subscription service. (must be an unused port)",
+      "subPortDesc": "The port number for the subscription service. (must be an unused port). Also used to build the subscription link/QR shown in the panel when \"Reverse Proxy URI\" below is empty — if the subscription is reached through a reverse proxy on a different port, set \"Reverse Proxy URI\" instead.",
       "subCertPath": "Public Key Path",
       "subCertPathDesc": "The public key file path for the subscription service. (begins with ‘/‘)",
       "subKeyPath": "Private Key Path",
@@ -1332,13 +1332,13 @@
       "subPath": "URI Path",
       "subPathDesc": "The URI path for the subscription service. (begins with ‘/‘ and concludes with ‘/‘)",
       "subDomain": "Listen Domain",
-      "subDomainDesc": "The domain name for the subscription service. (leave blank to listen on all domains and IPs)",
+      "subDomainDesc": "The domain name for the subscription service. (leave blank to listen on all domains and IPs). Also used as the fallback domain for the displayed subscription link when \"Reverse Proxy URI\" is empty — set \"Reverse Proxy URI\" if the panel and the subscription are reached through different domains (e.g. behind a reverse proxy).",
       "subUpdates": "Update Intervals",
       "subUpdatesDesc": "The update intervals of the subscription URL in the client apps. (unit: hour)",
       "subEncrypt": "Encode",
       "subEncryptDesc": "The returned content of subscription service will be Base64 encoded.",
       "subURI": "Reverse Proxy URI",
-      "subURIDesc": "The URI path of the subscription URL for use behind proxies.",
+      "subURIDesc": "The full base URL (scheme://domain[:port]/path/) for the subscription link and QR code, used instead of Listen Domain/Listen Port. Set this whenever the subscription is reached through a reverse proxy or a domain/port different from the ones above.",
       "externalTrafficInformEnable": "External Traffic Inform",
       "externalTrafficInformEnableDesc": "Inform external API on every traffic update.",
       "externalTrafficInformURI": "External Traffic Inform URI",
@@ -1665,6 +1665,7 @@
         "localIpPlaceholder": "local IP",
         "dialerProxyPlaceholder": "Select an outbound to chain through",
         "dialerProxyHint": "Dial this outbound through another outbound (by tag) to build a proxy chain. Leave empty to connect directly.",
+        "targetStrategyHint": "How the destination domain is resolved before connecting: AsIs (default) sends it unresolved, UseIP… resolves with fallback, ForceIP… requires successful resolution.",
         "addressRequired": "Address is required",
         "portRequired": "Port is required",
         "optional": "optional",
@@ -1734,6 +1735,7 @@
         "accountInfo": "Account Information",
         "outboundStatus": "Outbound Status",
         "sendThrough": "Send Through",
+        "targetStrategy": "Target Strategy",
         "test": "Test",
         "testResult": "Test Result",
         "testing": "Testing connection...",

+ 5 - 3
internal/web/translation/es-ES.json

@@ -1206,7 +1206,7 @@
       "subListen": "Listening IP",
       "subListenDesc": "Dejar en blanco por defecto para monitorear todas las IPs.",
       "subPort": "Puerto de Suscripción",
-      "subPortDesc": "El número de puerto para el servicio de suscripción debe estar sin usar en el servidor.",
+      "subPortDesc": "El número de puerto para el servicio de suscripción debe estar sin usar en el servidor. También se usa para construir el enlace/QR de suscripción mostrado en el panel cuando «URI de proxy inverso» está vacío — si la suscripción se accede a través de un proxy inverso en otro puerto, configure «URI de proxy inverso» en su lugar.",
       "subCertPath": "Ruta del Archivo de Clave Pública del Certificado de Suscripción",
       "subCertPathDesc": "Complete con una ruta absoluta que comience con '/'",
       "subKeyPath": "Ruta del Archivo de Clave Privada del Certificado de Suscripción",
@@ -1214,13 +1214,13 @@
       "subPath": "Ruta URI",
       "subPathDesc": "Debe empezar con '/' y terminar con '/'",
       "subDomain": "Dominio de Escucha",
-      "subDomainDesc": "Dejar en blanco por defecto para monitorear todos los dominios e IPs.",
+      "subDomainDesc": "Dejar en blanco por defecto para monitorear todos los dominios e IPs. También se usa como dominio de reserva para el enlace de suscripción mostrado cuando «URI de proxy inverso» está vacío — configure «URI de proxy inverso» si el panel y la suscripción se acceden por dominios diferentes (por ejemplo, detrás de un proxy inverso).",
       "subUpdates": "Intervalos de Actualización de Suscripción",
       "subUpdatesDesc": "Horas de intervalo entre actualizaciones en la aplicación del cliente.",
       "subEncrypt": "Codificar",
       "subEncryptDesc": "Encriptar las configuraciones devueltas en la suscripción.",
       "subURI": "URI de proxy inverso",
-      "subURIDesc": "Cambiar el URI base de la URL de suscripción para usar detrás de los servidores proxy",
+      "subURIDesc": "La URL base completa (scheme://dominio[:puerto]/ruta/) para el enlace de suscripción y el código QR, usada en lugar de Dominio de Escucha/Puerto de Suscripción. Configúrela cuando la suscripción se acceda a través de un proxy inverso o un dominio/puerto distinto a los anteriores.",
       "externalTrafficInformEnable": "Informe de tráfico externo",
       "externalTrafficInformEnableDesc": "Informar a una API externa en cada actualización de tráfico.",
       "externalTrafficInformURI": "URI de información de tráfico externo",
@@ -1549,6 +1549,7 @@
         "localIpPlaceholder": "IP local",
         "dialerProxyPlaceholder": "Selecciona una salida para encadenar",
         "dialerProxyHint": "Conecta esta salida a través de otra salida (por etiqueta) para crear una cadena de proxy. Déjalo vacío para conectar directamente.",
+        "targetStrategyHint": "Cómo se resuelve el dominio de destino antes de conectar: AsIs (predeterminado) lo envía sin resolver, UseIP… resuelve con respaldo, ForceIP… exige resolución.",
         "addressRequired": "La dirección es obligatoria",
         "portRequired": "El puerto es obligatorio",
         "optional": "opcional",
@@ -1618,6 +1619,7 @@
         "accountInfo": "Información de la Cuenta",
         "outboundStatus": "Estado de Salida",
         "sendThrough": "Enviar a través de",
+        "targetStrategy": "Estrategia de destino",
         "test": "Probar",
         "testResult": "Resultado de la prueba",
         "testing": "Probando conexión...",

+ 5 - 3
internal/web/translation/fa-IR.json

@@ -1208,7 +1208,7 @@
       "subListen": "آدرس آی‌پی",
       "subListenDesc": "آدرس آی‌پی برای سرویس سابسکریپشن. برای گوش دادن به‌تمام آی‌پی‌ها خالی‌بگذارید",
       "subPort": "پورت",
-      "subPortDesc": "شماره پورت برای سرویس سابسکریپشن. باید پورت استفاده نشده‌باشد",
+      "subPortDesc": "شماره پورت برای سرویس سابسکریپشن. باید پورت استفاده‌نشده‌باشد. همچنین وقتی «مسیر پراکسی معکوس» خالی باشد، برای ساخت لینک/QR سابسکریپشن نمایش‌داده‌شده در پنل استفاده می‌شود — اگر سابسکریپشن از پشت یک پراکسی معکوس روی پورت دیگری در دسترس است، به جای این، «مسیر پراکسی معکوس» را پر کنید.",
       "subCertPath": "مسیر کلید عمومی",
       "subCertPathDesc": "مسیر فایل کلیدعمومی برای سرویس سابیکریپشن. با '/' شروع‌می‌شود",
       "subKeyPath": "مسیر کلید خصوصی",
@@ -1216,13 +1216,13 @@
       "subPath": "مسیر URI",
       "subPathDesc": "برای سرویس سابسکریپشن. با '/' شروع‌ و با '/' خاتمه‌ می‌یابد URI مسیر",
       "subDomain": "نام دامنه",
-      "subDomainDesc": "آدرس دامنه برای سرویس سابسکریپشن. برای گوش دادن به تمام دامنه‌ها و آی‌پی‌ها خالی‌بگذارید‌",
+      "subDomainDesc": "آدرس دامنه برای سرویس سابسکریپشن. برای گوش دادن به تمام دامنه‌ها و آی‌پی‌ها خالی‌بگذارید. همچنین وقتی «مسیر پراکسی معکوس» خالی باشد، به عنوان دامنه پیشفرض لینک سابسکریپشن نمایش‌داده‌شده استفاده می‌شود — اگر پنل و سابسکریپشن از دامنه‌های متفاوتی در دسترس هستند (مثلاً پشت یک پراکسی معکوس)، «مسیر پراکسی معکوس» را پر کنید.",
       "subUpdates": "فاصله بروزرسانی‌ سابسکریپشن",
       "subUpdatesDesc": "(فاصله مابین بروزرسانی در برنامه‌های کاربری. (واحد: ساعت",
       "subEncrypt": "انکود",
       "subEncryptDesc": "کدگذاری خواهدشد Base64 محتوای برگشتی سرویس سابسکریپشن برپایه",
       "subURI": "پروکسی معکوس URI مسیر",
-      "subURIDesc": "سابسکریپشن را برای استفاده در پشت پراکسی‌ها تغییر می‌دهد URI مسیر",
+      "subURIDesc": "آدرس پایه کامل (scheme://domain[:port]/path/) برای لینک سابسکریپشن و کد QR، به جای نام دامنه/پورت استفاده می‌شود. هر وقت سابسکریپشن از پشت یک پراکسی معکوس یا روی دامنه/پورت متفاوتی نسبت به موارد بالا در دسترس است، این را پر کنید.",
       "externalTrafficInformEnable": "اطلاع رسانی خارجی مصرف ترافیک",
       "externalTrafficInformEnableDesc": "به API خارجی در هر به‌روزرسانی ترافیک اطلاع بده.",
       "externalTrafficInformURI": "لینک اطلاع رسانی خارجی مصرف ترافیک",
@@ -1549,6 +1549,7 @@
         "localIpPlaceholder": "IP محلی",
         "dialerProxyPlaceholder": "یک خروجی برای زنجیره کردن انتخاب کنید",
         "dialerProxyHint": "این خروجی را از طریق خروجی دیگری (با تگ) برقرار کن تا یک زنجیره پروکسی ساخته شود. برای اتصال مستقیم خالی بگذار.",
+        "targetStrategyHint": "نحوه تبدیل دامنه مقصد پیش از اتصال: AsIs (پیش‌فرض) آن را بدون تغییر می‌فرستد، UseIP… با امکان بازگشت تبدیل می‌کند، ForceIP… تبدیل موفق را الزامی می‌کند.",
         "addressRequired": "آدرس الزامی است",
         "portRequired": "پورت الزامی است",
         "optional": "اختیاری",
@@ -1618,6 +1619,7 @@
         "accountInfo": "اطلاعات حساب",
         "outboundStatus": "وضعیت خروجی",
         "sendThrough": "ارسال با",
+        "targetStrategy": "استراتژی مقصد",
         "test": "تست",
         "testResult": "نتیجه تست",
         "testing": "در حال تست اتصال...",

+ 5 - 3
internal/web/translation/id-ID.json

@@ -1206,7 +1206,7 @@
       "subListen": "IP Pendengar",
       "subListenDesc": "Alamat IP untuk layanan langganan. (biarkan kosong untuk mendengarkan semua IP)",
       "subPort": "Port Pendengar",
-      "subPortDesc": "Nomor port untuk layanan langganan. (harus menjadi port yang tidak digunakan)",
+      "subPortDesc": "Nomor port untuk layanan langganan. (harus menjadi port yang tidak digunakan). Juga digunakan untuk membangun tautan/QR langganan yang ditampilkan di panel ketika \"URI Proxy Terbalik\" di bawah kosong — jika langganan diakses melalui reverse proxy pada port yang berbeda, atur \"URI Proxy Terbalik\" sebagai gantinya.",
       "subCertPath": "Path Kunci Publik",
       "subCertPathDesc": "Path berkas kunci publik untuk layanan langganan. (dimulai dengan ‘/‘)",
       "subKeyPath": "Path Kunci Privat",
@@ -1214,13 +1214,13 @@
       "subPath": "Path URI",
       "subPathDesc": "URI path untuk layanan langganan. (dimulai dengan ‘/‘ dan diakhiri dengan ‘/‘)",
       "subDomain": "Domain Pendengar",
-      "subDomainDesc": "Nama domain untuk layanan langganan. (biarkan kosong untuk mendengarkan semua domain dan IP)",
+      "subDomainDesc": "Nama domain untuk layanan langganan. (biarkan kosong untuk mendengarkan semua domain dan IP). Juga digunakan sebagai domain cadangan untuk tautan langganan yang ditampilkan ketika \"URI Proxy Terbalik\" kosong — atur \"URI Proxy Terbalik\" jika panel dan langganan diakses melalui domain yang berbeda (misalnya di belakang reverse proxy).",
       "subUpdates": "Interval Pembaruan",
       "subUpdatesDesc": "Interval pembaruan URL langganan dalam aplikasi klien. (unit: jam)",
       "subEncrypt": "Encode",
       "subEncryptDesc": "Konten yang dikembalikan dari layanan langganan akan dienkripsi Base64.",
       "subURI": "URI Proxy Terbalik",
-      "subURIDesc": "Path URI dari URL langganan untuk digunakan di belakang proxy.",
+      "subURIDesc": "URL dasar lengkap (scheme://domain[:port]/path/) untuk tautan langganan dan kode QR, digunakan sebagai pengganti Domain/Port Pendengar. Atur ini kapan pun langganan diakses melalui reverse proxy atau domain/port yang berbeda dari yang di atas.",
       "externalTrafficInformEnable": "Informasikan API eksternal pada setiap pembaruan lalu lintas.",
       "externalTrafficInformEnableDesc": "Beritahu API eksternal setiap kali ada pembaruan trafik.",
       "externalTrafficInformURI": "Lalu Lintas Eksternal Menginformasikan URI",
@@ -1549,6 +1549,7 @@
         "localIpPlaceholder": "IP lokal",
         "dialerProxyPlaceholder": "Pilih outbound untuk dirantai",
         "dialerProxyHint": "Hubungkan outbound ini melalui outbound lain (berdasarkan tag) untuk membuat rantai proxy. Kosongkan untuk terhubung langsung.",
+        "targetStrategyHint": "Cara domain tujuan diresolusi sebelum terhubung: AsIs (default) mengirim apa adanya, UseIP… meresolusi dengan fallback, ForceIP… wajib berhasil diresolusi.",
         "addressRequired": "Alamat wajib diisi",
         "portRequired": "Port wajib diisi",
         "optional": "opsional",
@@ -1618,6 +1619,7 @@
         "accountInfo": "Informasi Akun",
         "outboundStatus": "Status Keluar",
         "sendThrough": "Kirim Melalui",
+        "targetStrategy": "Strategi Target",
         "test": "Tes",
         "testResult": "Hasil Tes",
         "testing": "Menguji koneksi...",

+ 5 - 3
internal/web/translation/ja-JP.json

@@ -1206,7 +1206,7 @@
       "subListen": "監視IP",
       "subListenDesc": "サブスクリプションサービスが監視するIPアドレス(空白にするとすべてのIPを監視)",
       "subPort": "監視ポート",
-      "subPortDesc": "サブスクリプションサービスが監視するポート番号(使用されていないポートである必要があります)",
+      "subPortDesc": "サブスクリプションサービスが監視するポート番号(使用されていないポートである必要があります)。「リバースプロキシURI」が空の場合、パネルに表示されるサブスクリプションリンク/QRコードの生成にも使われます — サブスクリプションが別のポートのリバースプロキシ経由でアクセスされる場合は、代わりに「リバースプロキシURI」を設定してください。",
       "subCertPath": "公開鍵パス",
       "subCertPathDesc": "サブスクリプションサービスで使用する公開鍵ファイルのパス('/'で始まる)",
       "subKeyPath": "秘密鍵パス",
@@ -1214,13 +1214,13 @@
       "subPath": "URI パス",
       "subPathDesc": "サブスクリプションサービスで使用するURIパス('/'で始まり、'/'で終わる)",
       "subDomain": "監視ドメイン",
-      "subDomainDesc": "サブスクリプションサービスが監視するドメイン(空白にするとすべてのドメインとIPを監視)",
+      "subDomainDesc": "サブスクリプションサービスが監視するドメイン(空白にするとすべてのドメインとIPを監視)。「リバースプロキシURI」が空の場合、表示されるサブスクリプションリンクのフォールバックドメインとしても使われます — パネルとサブスクリプションが異なるドメイン(例: リバースプロキシの背後)でアクセスされる場合は「リバースプロキシURI」を設定してください。",
       "subUpdates": "更新間隔",
       "subUpdatesDesc": "クライアントアプリケーションでサブスクリプションURLの更新間隔(単位:時間)",
       "subEncrypt": "エンコード",
       "subEncryptDesc": "サブスクリプションサービスが返す内容をBase64エンコードする",
       "subURI": "リバースプロキシURI",
-      "subURIDesc": "プロキシ後ろのサブスクリプションURLのURIパスに使用する",
+      "subURIDesc": "サブスクリプションリンクとQRコードに使われる完全なベースURL(scheme://domain[:port]/path/)で、監視ドメイン/監視ポートの代わりに使用されます。サブスクリプションがリバースプロキシ経由、または上記と異なるドメイン/ポートでアクセスされる場合に設定してください。",
       "externalTrafficInformEnable": "外部トラフィック情報",
       "externalTrafficInformEnableDesc": "トラフィック更新ごとに外部 API に通知。",
       "externalTrafficInformURI": "外部トラフィック通知 URI",
@@ -1549,6 +1549,7 @@
         "localIpPlaceholder": "ローカル IP",
         "dialerProxyPlaceholder": "経由するアウトバウンドを選択",
         "dialerProxyHint": "このアウトバウンドを別のアウトバウンド(タグ指定)経由で接続し、プロキシチェーンを構成します。直接接続する場合は空のままにします。",
+        "targetStrategyHint": "接続前に宛先ドメインをどう解決するか:AsIs(既定)はそのまま送信、UseIP… は解決を試み失敗時はフォールバック、ForceIP… は解決必須。",
         "addressRequired": "アドレスは必須です",
         "portRequired": "ポートは必須です",
         "optional": "任意",
@@ -1618,6 +1619,7 @@
         "accountInfo": "アカウント情報",
         "outboundStatus": "アウトバウンドステータス",
         "sendThrough": "送信経路",
+        "targetStrategy": "ターゲット解決戦略",
         "test": "テスト",
         "testResult": "テスト結果",
         "testing": "接続をテスト中...",

+ 5 - 3
internal/web/translation/pt-BR.json

@@ -1206,7 +1206,7 @@
       "subListen": "IP de Escuta",
       "subListenDesc": "O endereço IP para o serviço de assinatura. (deixe em branco para escutar em todos os IPs)",
       "subPort": "Porta de Escuta",
-      "subPortDesc": "O número da porta para o serviço de assinatura. (deve ser uma porta não usada)",
+      "subPortDesc": "O número da porta para o serviço de assinatura. (deve ser uma porta não usada). Também é usada para construir o link/QR de assinatura exibido no painel quando \"URI de Proxy Reverso\" abaixo estiver vazio — se a assinatura for acessada por um proxy reverso em outra porta, configure \"URI de Proxy Reverso\" em vez disso.",
       "subCertPath": "Caminho da Chave Pública",
       "subCertPathDesc": "O caminho do arquivo de chave pública para o serviço de assinatura. (começa com ‘/‘)",
       "subKeyPath": "Caminho da Chave Privada",
@@ -1214,13 +1214,13 @@
       "subPath": "Caminho URI",
       "subPathDesc": "O caminho URI para o serviço de assinatura. (começa com ‘/‘ e termina com ‘/‘)",
       "subDomain": "Domínio de Escuta",
-      "subDomainDesc": "O nome de domínio para o serviço de assinatura. (deixe em branco para escutar em todos os domínios e IPs)",
+      "subDomainDesc": "O nome de domínio para o serviço de assinatura. (deixe em branco para escutar em todos os domínios e IPs). Também é usado como domínio de fallback para o link de assinatura exibido quando \"URI de Proxy Reverso\" estiver vazio — configure \"URI de Proxy Reverso\" se o painel e a assinatura forem acessados por domínios diferentes (por exemplo, atrás de um proxy reverso).",
       "subUpdates": "Intervalos de Atualização",
       "subUpdatesDesc": "Os intervalos de atualização da URL de assinatura nos aplicativos de cliente. (unidade: hora)",
       "subEncrypt": "Codificar",
       "subEncryptDesc": "O conteúdo retornado pelo serviço de assinatura será codificado em Base64.",
       "subURI": "URI de Proxy Reverso",
-      "subURIDesc": "O caminho URI da URL de assinatura para uso por trás de proxies.",
+      "subURIDesc": "A URL base completa (scheme://dominio[:porta]/caminho/) para o link de assinatura e o código QR, usada em vez de Domínio/Porta de Escuta. Configure isso sempre que a assinatura for acessada por um proxy reverso ou um domínio/porta diferente dos acima.",
       "externalTrafficInformEnable": "Informações de tráfego externo",
       "externalTrafficInformEnableDesc": "Informar API externa a cada atualização de tráfego.",
       "externalTrafficInformURI": "URI de informação de tráfego externo",
@@ -1549,6 +1549,7 @@
         "localIpPlaceholder": "IP local",
         "dialerProxyPlaceholder": "Selecione uma saída para encadear",
         "dialerProxyHint": "Conecte esta saída através de outra saída (por tag) para criar uma cadeia de proxy. Deixe vazio para conectar diretamente.",
+        "targetStrategyHint": "Como o domínio de destino é resolvido antes de conectar: AsIs (padrão) envia sem resolver, UseIP… resolve com fallback, ForceIP… exige resolução.",
         "addressRequired": "Endereço é obrigatório",
         "portRequired": "Porta é obrigatória",
         "optional": "opcional",
@@ -1618,6 +1619,7 @@
         "accountInfo": "Informações da Conta",
         "outboundStatus": "Status de Saída",
         "sendThrough": "Enviar Através de",
+        "targetStrategy": "Estratégia de destino",
         "test": "Testar",
         "testResult": "Resultado do teste",
         "testing": "Testando conexão...",

+ 5 - 3
internal/web/translation/ru-RU.json

@@ -1206,7 +1206,7 @@
       "subListen": "Прослушивание IP",
       "subListenDesc": "Оставьте пустым по умолчанию, чтобы отслеживать все IP-адреса",
       "subPort": "Порт подписки",
-      "subPortDesc": "Номер порта для обслуживания службы подписки не должен использоваться на сервере",
+      "subPortDesc": "Номер порта для обслуживания службы подписки не должен использоваться на сервере. Также используется для построения ссылки подписки в панели, если поле «URI обратного прокси» ниже пустое — если подписка доступна через reverse-proxy на другом порту, укажите «URI обратного прокси».",
       "subCertPath": "Путь к файлу публичного ключа сертификата подписки",
       "subCertPathDesc": "Введите полный путь, начинающийся с '/'",
       "subKeyPath": "Путь к файлу приватного ключа сертификата подписки",
@@ -1214,13 +1214,13 @@
       "subPath": "URI-путь",
       "subPathDesc": "Должен начинаться с '/' и заканчиваться на '/'",
       "subDomain": "Домен прослушивания",
-      "subDomainDesc": "Оставьте пустым по умолчанию, чтобы слушать все домены и IP-адреса",
+      "subDomainDesc": "Оставьте пустым по умолчанию, чтобы слушать все домены и IP-адреса. Также используется как домен по умолчанию для отображаемой ссылки подписки, если поле «URI обратного прокси» пустое — заполните «URI обратного прокси», если панель и подписка доступны на разных доменах (например, за reverse-proxy).",
       "subUpdates": "Интервалы обновления подписки",
       "subUpdatesDesc": "Интервал между обновлениями в клиентском приложении (в часах)",
       "subEncrypt": "Кодировать",
       "subEncryptDesc": "Шифровать возвращенные конфиги в подписке",
       "subURI": "URI обратного прокси",
-      "subURIDesc": "Изменить базовый URI URL-адреса подписки для использования за прокси-серверами",
+      "subURIDesc": "Полный базовый URL (schema://домен[:порт]/путь/) для ссылки подписки и QR-кода вместо «Домена прослушивания»/«Порта подписки». Заполните, если подписка доступна через reverse-proxy или на другом домене/порту.",
       "externalTrafficInformEnable": "Информация о внешнем трафике",
       "externalTrafficInformEnableDesc": "Уведомлять внешний API при каждом обновлении трафика.",
       "externalTrafficInformURI": "URI информации о внешнем трафике",
@@ -1549,6 +1549,7 @@
         "localIpPlaceholder": "локальный IP",
         "dialerProxyPlaceholder": "Выберите исходящее для цепочки",
         "dialerProxyHint": "Подключайте это исходящее через другое исходящее (по тегу), чтобы построить цепочку прокси. Оставьте пустым для прямого подключения.",
+        "targetStrategyHint": "Как разрешается домен назначения перед подключением: AsIs (по умолчанию) — отправляется как есть, UseIP… — разрешение с откатом, ForceIP… — требуется успешное разрешение.",
         "addressRequired": "Адрес обязателен",
         "portRequired": "Порт обязателен",
         "optional": "опционально",
@@ -1618,6 +1619,7 @@
         "accountInfo": "Информация об учетной записи",
         "outboundStatus": "Статус исходящего подключения",
         "sendThrough": "Отправить через",
+        "targetStrategy": "Стратегия назначения",
         "test": "Тест",
         "testResult": "Результат теста",
         "testing": "Тестирование соединения...",

+ 5 - 3
internal/web/translation/tr-TR.json

@@ -1206,7 +1206,7 @@
       "subListen": "Dinleme IP",
       "subListenDesc": "Abonelik hizmeti için IP adresi. (tüm IP'leri dinlemek için boş bırakın)",
       "subPort": "Dinleme Portu",
-      "subPortDesc": "Abonelik hizmeti için port numarası. (kullanılmayan bir port olmalıdır)",
+      "subPortDesc": "Abonelik hizmeti için port numarası. (kullanılmayan bir port olmalıdır). Ayrıca, aşağıdaki \"Ters Proxy URI\" boşsa panelde gösterilen abonelik bağlantısı/QR kodunu oluşturmak için de kullanılır — abonelik farklı bir portta ters proxy üzerinden erişiliyorsa bunun yerine \"Ters Proxy URI\"yi ayarlayın.",
       "subCertPath": "Genel Anahtar Yolu",
       "subCertPathDesc": "Abonelik hizmeti için genel anahtar dosya yolu. ('/' ile başlar)",
       "subKeyPath": "Özel Anahtar Yolu",
@@ -1214,13 +1214,13 @@
       "subPath": "URI Yolu",
       "subPathDesc": "Abonelik hizmeti için URI yolu. ('/' ile başlar ve '/' ile biter)",
       "subDomain": "Dinleme Alan Adı",
-      "subDomainDesc": "Abonelik hizmeti için alan adı. (tüm alan adlarını ve IP'leri dinlemek için boş bırakın)",
+      "subDomainDesc": "Abonelik hizmeti için alan adı. (tüm alan adlarını ve IP'leri dinlemek için boş bırakın). Ayrıca, \"Ters Proxy URI\" boşsa gösterilen abonelik bağlantısı için yedek alan adı olarak da kullanılır — panel ve abonelik farklı alan adları üzerinden erişiliyorsa (örneğin bir ters proxy arkasında) \"Ters Proxy URI\"yi ayarlayın.",
       "subUpdates": "Güncelleme Aralıkları",
       "subUpdatesDesc": "İstemci uygulamalarındaki abonelik URL'sinin güncellenme aralığı. (birim: saat)",
       "subEncrypt": "Kodla",
       "subEncryptDesc": "Abonelik hizmetinin döndürülen içeriğini Base64 ile şifreler.",
       "subURI": "Ters Proxy URI",
-      "subURIDesc": "Proxy arkasında kullanılacak abonelik URL'sinin URI yolu.",
+      "subURIDesc": "Abonelik bağlantısı ve QR kodu için Dinleme Alan Adı/Dinleme Portu yerine kullanılan tam temel URL (scheme://alanadi[:port]/yol/). Abonelik bir ters proxy üzerinden veya yukarıdakilerden farklı bir alan adı/port üzerinden erişildiğinde bunu ayarlayın.",
       "externalTrafficInformEnable": "Harici Trafik Bilgisi",
       "externalTrafficInformEnableDesc": "Her trafik güncellemesinde harici API'yi bilgilendirir.",
       "externalTrafficInformURI": "Harici Trafik Bilgisi URI'si",
@@ -1549,6 +1549,7 @@
         "localIpPlaceholder": "yerel IP",
         "dialerProxyPlaceholder": "Zincirlemek için bir giden bağlantı seçin",
         "dialerProxyHint": "Bir proxy zinciri oluşturmak için bu giden bağlantıyı başka bir giden bağlantı (etikete göre) üzerinden bağlayın. Doğrudan bağlanmak için boş bırakın.",
+        "targetStrategyHint": "Bağlanmadan önce hedef alan adının nasıl çözümleneceği: AsIs (varsayılan) olduğu gibi gönderir, UseIP… çözümler ve başarısızsa geri döner, ForceIP… çözümleme zorunludur.",
         "addressRequired": "Adres zorunludur",
         "portRequired": "Port zorunludur",
         "optional": "opsiyonel",
@@ -1618,6 +1619,7 @@
         "accountInfo": "Hesap Bilgileri",
         "outboundStatus": "Giden Bağlantı Durumu",
         "sendThrough": "Üzerinden Gönder",
+        "targetStrategy": "Hedef Stratejisi",
         "test": "Test",
         "testResult": "Test Sonucu",
         "testing": "Bağlantı test ediliyor...",

+ 5 - 3
internal/web/translation/uk-UA.json

@@ -1206,7 +1206,7 @@
       "subListen": "Слухати IP",
       "subListenDesc": "IP-адреса для служби підписки. (залиште порожнім, щоб слухати всі IP-адреси)",
       "subPort": "Слухати порт",
-      "subPortDesc": "Номер порту для служби підписки. (має бути невикористаний порт)",
+      "subPortDesc": "Номер порту для служби підписки. (має бути невикористаний порт). Також використовується для побудови посилання/QR підписки, що показується в панелі, коли поле «URI зворотного проксі» нижче порожнє — якщо підписка доступна через зворотний проксі на іншому порту, вкажіть замість цього «URI зворотного проксі».",
       "subCertPath": "Шлях відкритого ключа",
       "subCertPathDesc": "Шлях до файлу відкритого ключа для служби підписки. (починається з ‘/‘)",
       "subKeyPath": "Шлях приватного ключа",
@@ -1214,13 +1214,13 @@
       "subPath": "URI-шлях",
       "subPathDesc": "Шлях URI для служби підписки. (починається з ‘/‘ і закінчується ‘/‘)",
       "subDomain": "Домен прослуховування",
-      "subDomainDesc": "Ім'я домену для служби підписки. (залиште порожнім, щоб слухати всі домени та IP-адреси)",
+      "subDomainDesc": "Ім'я домену для служби підписки. (залиште порожнім, щоб слухати всі домени та IP-адреси). Також використовується як резервний домен для показаного посилання підписки, коли «URI зворотного проксі» порожнє — вкажіть «URI зворотного проксі», якщо панель і підписка доступні через різні домени (наприклад, за зворотним проксі).",
       "subUpdates": "Інтервали оновлення",
       "subUpdatesDesc": "Інтервали оновлення URL-адреси підписки в клієнтських програмах. (одиниця: година)",
       "subEncrypt": "Кодувати",
       "subEncryptDesc": "Повернений вміст послуги підписки матиме кодування Base64.",
       "subURI": "URI зворотного проксі",
-      "subURIDesc": "URI до URL-адреси підписки для використання за проксі.",
+      "subURIDesc": "Повна базова URL-адреса (scheme://домен[:порт]/шлях/) для посилання підписки та QR-коду, використовується замість Домену/Порту прослуховування. Вкажіть це, якщо підписка доступна через зворотний проксі або на іншому домені/порту, ніж вказано вище.",
       "externalTrafficInformEnable": "Інформація про зовнішній трафік",
       "externalTrafficInformEnableDesc": "Повідомляти зовнішній API про кожне оновлення трафіку.",
       "externalTrafficInformURI": "Інформаційний URI зовнішнього трафіку",
@@ -1549,6 +1549,7 @@
         "localIpPlaceholder": "локальний IP",
         "dialerProxyPlaceholder": "Виберіть вихідний для ланцюжка",
         "dialerProxyHint": "Підключайте цей вихідний через інший вихідний (за тегом), щоб побудувати ланцюжок проксі. Залиште порожнім для прямого підключення.",
+        "targetStrategyHint": "Як розвʼязується домен призначення перед підключенням: AsIs (типово) — надсилається як є, UseIP… — розвʼязання з відкатом, ForceIP… — розвʼязання обовʼязкове.",
         "addressRequired": "Адреса обов'язкова",
         "portRequired": "Порт обов'язковий",
         "optional": "опційно",
@@ -1618,6 +1619,7 @@
         "accountInfo": "Інформація про обліковий запис",
         "outboundStatus": "Статус виходу",
         "sendThrough": "Надіслати через",
+        "targetStrategy": "Стратегія призначення",
         "test": "Тест",
         "testResult": "Результат тесту",
         "testing": "Тестування з'єднання...",

+ 5 - 3
internal/web/translation/vi-VN.json

@@ -1206,7 +1206,7 @@
       "subListen": "Listening IP",
       "subListenDesc": "Mặc định để trống để nghe tất cả các IP",
       "subPort": "Cổng gói đăng ký",
-      "subPortDesc": "Số cổng dịch vụ đăng ký phải chưa được sử dụng trên máy chủ",
+      "subPortDesc": "Số cổng dịch vụ đăng ký phải chưa được sử dụng trên máy chủ. Cũng được dùng để tạo liên kết/QR đăng ký hiển thị trên bảng điều khiển khi \"URI proxy trung gian\" bên dưới để trống — nếu đăng ký được truy cập qua proxy trung gian trên một cổng khác, hãy đặt \"URI proxy trung gian\" thay thế.",
       "subCertPath": "Đường dẫn file chứng chỉ gói đăng ký",
       "subCertPathDesc": "Điền vào đường dẫn đầy đủ (bắt đầu với '/')",
       "subKeyPath": "Đường dẫn file khóa của chứng chỉ gói đăng ký",
@@ -1214,13 +1214,13 @@
       "subPath": "Đường dẫn URI",
       "subPathDesc": "Phải bắt đầu và kết thúc bằng '/'",
       "subDomain": "Tên miền con",
-      "subDomainDesc": "Mặc định để trống để nghe tất cả các tên miền và IP",
+      "subDomainDesc": "Mặc định để trống để nghe tất cả các tên miền và IP. Cũng được dùng làm tên miền dự phòng cho liên kết đăng ký hiển thị khi \"URI proxy trung gian\" để trống — hãy đặt \"URI proxy trung gian\" nếu bảng điều khiển và đăng ký được truy cập qua các tên miền khác nhau (ví dụ: đứng sau proxy trung gian).",
       "subUpdates": "Khoảng thời gian cập nhật gói đăng ký",
       "subUpdatesDesc": "Số giờ giữa các cập nhật trong ứng dụng khách",
       "subEncrypt": "Mã hóa",
       "subEncryptDesc": "Mã hóa các cấu hình được trả về trong gói đăng ký",
       "subURI": "URI proxy trung gian",
-      "subURIDesc": "Thay đổi URI cơ sở của URL gói đăng ký để sử dụng cho proxy trung gian",
+      "subURIDesc": "URL cơ sở đầy đủ (scheme://tênmiền[:cổng]/đường-dẫn/) cho liên kết đăng ký và mã QR, dùng thay cho Tên miền/Cổng gói đăng ký. Hãy đặt giá trị này khi đăng ký được truy cập qua proxy trung gian hoặc một tên miền/cổng khác với các mục trên.",
       "externalTrafficInformEnable": "Thông báo giao thông bên ngoài",
       "externalTrafficInformEnableDesc": "Thông báo API ngoài mỗi khi cập nhật lưu lượng.",
       "externalTrafficInformURI": "URI thông báo lưu lượng truy cập bên ngoài",
@@ -1549,6 +1549,7 @@
         "localIpPlaceholder": "IP nội bộ",
         "dialerProxyPlaceholder": "Chọn một outbound để nối chuỗi",
         "dialerProxyHint": "Kết nối outbound này qua một outbound khác (theo tag) để tạo chuỗi proxy. Để trống để kết nối trực tiếp.",
+        "targetStrategyHint": "Cách phân giải tên miền đích trước khi kết nối: AsIs (mặc định) gửi nguyên trạng, UseIP… phân giải kèm dự phòng, ForceIP… bắt buộc phân giải thành công.",
         "addressRequired": "Địa chỉ là bắt buộc",
         "portRequired": "Cổng là bắt buộc",
         "optional": "tùy chọn",
@@ -1618,6 +1619,7 @@
         "accountInfo": "Thông tin tài khoản",
         "outboundStatus": "Trạng thái đầu ra",
         "sendThrough": "Gửi qua",
+        "targetStrategy": "Chiến lược đích",
         "test": "Kiểm tra",
         "testResult": "Kết quả kiểm tra",
         "testing": "Đang kiểm tra kết nối...",

+ 5 - 3
internal/web/translation/zh-CN.json

@@ -1206,7 +1206,7 @@
       "subListen": "监听 IP",
       "subListenDesc": "订阅服务监听的 IP 地址(留空表示监听所有 IP)",
       "subPort": "监听端口",
-      "subPortDesc": "订阅服务监听的端口号(必须是未使用的端口)",
+      "subPortDesc": "订阅服务监听的端口号(必须是未使用的端口)。当下方「反向代理 URI」为空时,也会用来生成面板中显示的订阅链接/二维码——如果订阅是通过反向代理的其他端口访问的,请改为设置「反向代理 URI」。",
       "subCertPath": "公钥路径",
       "subCertPathDesc": "订阅服务使用的公钥文件路径(以 '/' 开头)",
       "subKeyPath": "私钥路径",
@@ -1214,13 +1214,13 @@
       "subPath": "URI 路径",
       "subPathDesc": "订阅服务使用的 URI 路径(以 '/' 开头,以 '/' 结尾)",
       "subDomain": "监听域名",
-      "subDomainDesc": "订阅服务监听的域名(留空表示监听所有域名和 IP)",
+      "subDomainDesc": "订阅服务监听的域名(留空表示监听所有域名和 IP)。当「反向代理 URI」为空时,也会作为显示的订阅链接的回退域名——如果面板和订阅通过不同的域名访问(例如位于反向代理之后),请设置「反向代理 URI」。",
       "subUpdates": "更新间隔",
       "subUpdatesDesc": "客户端应用中订阅 URL 的更新间隔(单位:小时)",
       "subEncrypt": "编码",
       "subEncryptDesc": "订阅服务返回的内容将采用 Base64 编码",
       "subURI": "反向代理 URI",
-      "subURIDesc": "用于代理后面的订阅 URL 的 URI 路径",
+      "subURIDesc": "用于订阅链接和二维码的完整基础 URL(scheme://域名[:端口]/路径/),会替代监听域名/监听端口使用。当订阅通过反向代理访问,或使用与上面不同的域名/端口访问时,请设置此项。",
       "externalTrafficInformEnable": "外部交通通知",
       "externalTrafficInformEnableDesc": "每次流量更新时通知外部 API。",
       "externalTrafficInformURI": "外部流量通知 URI",
@@ -1549,6 +1549,7 @@
         "localIpPlaceholder": "本地 IP",
         "dialerProxyPlaceholder": "选择要串联的出站",
         "dialerProxyHint": "让此出站通过另一个出站(按标签)拨号,以建立代理链。留空则直接连接。",
+        "targetStrategyHint": "连接前如何解析目标域名:AsIs(默认)原样发送,UseIP… 解析失败时回退,ForceIP… 必须解析成功。",
         "addressRequired": "地址为必填项",
         "portRequired": "端口为必填项",
         "optional": "可选",
@@ -1618,6 +1619,7 @@
         "accountInfo": "帐户信息",
         "outboundStatus": "出站状态",
         "sendThrough": "发送通过",
+        "targetStrategy": "目标解析策略",
         "test": "测试",
         "testResult": "测试结果",
         "testing": "正在测试连接...",

+ 5 - 3
internal/web/translation/zh-TW.json

@@ -1206,7 +1206,7 @@
       "subListen": "監聽 IP",
       "subListenDesc": "訂閱服務監聽的 IP 地址(留空表示監聽所有 IP)",
       "subPort": "監聽埠",
-      "subPortDesc": "訂閱服務監聽的埠號(必須是未使用的埠)",
+      "subPortDesc": "訂閱服務監聽的埠號(必須是未使用的埠)。當下方「反向代理 URI」為空時,也會用來產生面板中顯示的訂閱連結/QR 碼——如果訂閱是透過反向代理的其他埠存取的,請改為設定「反向代理 URI」。",
       "subCertPath": "公鑰路徑",
       "subCertPathDesc": "訂閱服務使用的公鑰檔案路徑(以 '/' 開頭)",
       "subKeyPath": "私鑰路徑",
@@ -1214,13 +1214,13 @@
       "subPath": "URI 路徑",
       "subPathDesc": "訂閱服務使用的 URI 路徑(以 '/' 開頭,以 '/' 結尾)",
       "subDomain": "監聽域名",
-      "subDomainDesc": "訂閱服務監聽的域名(留空表示監聽所有域名和 IP)",
+      "subDomainDesc": "訂閱服務監聽的域名(留空表示監聽所有域名和 IP)。當「反向代理 URI」為空時,也會作為顯示的訂閱連結的備援域名——如果面板和訂閱透過不同的域名存取(例如位於反向代理之後),請設定「反向代理 URI」。",
       "subUpdates": "更新間隔",
       "subUpdatesDesc": "客戶端應用中訂閱 URL 的更新間隔(單位:小時)",
       "subEncrypt": "編碼",
       "subEncryptDesc": "訂閱服務返回的內容將採用 Base64 編碼",
       "subURI": "反向代理 URI",
-      "subURIDesc": "用於代理後面的訂閱 URL 的 URI 路徑",
+      "subURIDesc": "用於訂閱連結和 QR 碼的完整基礎 URL(scheme://域名[:埠]/路徑/),會取代監聽域名/監聽埠使用。當訂閱透過反向代理存取,或使用與上面不同的域名/埠存取時,請設定此項。",
       "externalTrafficInformEnable": "外部交通通知",
       "externalTrafficInformEnableDesc": "每次流量更新時通知外部 API。",
       "externalTrafficInformURI": "外部流量通知 URI",
@@ -1549,6 +1549,7 @@
         "localIpPlaceholder": "本地 IP",
         "dialerProxyPlaceholder": "選擇要串接的出站",
         "dialerProxyHint": "讓此出站透過另一個出站(以標籤指定)連線,以建立代理鏈。留空則直接連線。",
+        "targetStrategyHint": "連線前如何解析目標網域:AsIs(預設)原樣傳送,UseIP… 解析失敗時回退,ForceIP… 必須解析成功。",
         "addressRequired": "地址為必填",
         "portRequired": "連接埠為必填",
         "optional": "選用",
@@ -1618,6 +1619,7 @@
         "accountInfo": "帳戶資訊",
         "outboundStatus": "出站狀態",
         "sendThrough": "傳送通過",
+        "targetStrategy": "目標解析策略",
         "test": "測試",
         "testResult": "測試結果",
         "testing": "正在測試連接...",

+ 1 - 1
update.sh

@@ -187,7 +187,7 @@ install_base() {
             ;;
         centos)
             if [[ "${VERSION_ID}" =~ ^7 ]]; then
-                yum -y update > /dev/null 2>&1 && yum install -y -q cronie curl tar tzdata socat openssl > /dev/null 2>&1
+                yum makecache -y > /dev/null 2>&1 && yum install -y -q cronie curl tar tzdata socat openssl > /dev/null 2>&1
             else
                 dnf makecache -y > /dev/null 2>&1 && dnf install -y -q cronie curl tar tzdata socat openssl > /dev/null 2>&1
             fi

+ 9 - 9
x-ui.sh

@@ -1583,13 +1583,13 @@ ssl_cert_issue_for_ip() {
             apt-get update > /dev/null 2>&1 && apt-get install socat -y > /dev/null 2>&1
             ;;
         fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
-            dnf -y update > /dev/null 2>&1 && dnf -y install socat > /dev/null 2>&1
+            dnf makecache -y > /dev/null 2>&1 && dnf -y install socat > /dev/null 2>&1
             ;;
         centos)
             if [[ "${VERSION_ID}" =~ ^7 ]]; then
-                yum -y update > /dev/null 2>&1 && yum -y install socat > /dev/null 2>&1
+                yum makecache -y > /dev/null 2>&1 && yum -y install socat > /dev/null 2>&1
             else
-                dnf -y update > /dev/null 2>&1 && dnf -y install socat > /dev/null 2>&1
+                dnf makecache -y > /dev/null 2>&1 && dnf -y install socat > /dev/null 2>&1
             fi
             ;;
         arch | manjaro | parch)
@@ -1749,13 +1749,13 @@ ssl_cert_issue() {
             apt-get update > /dev/null 2>&1 && apt-get install socat -y > /dev/null 2>&1
             ;;
         fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
-            dnf -y update > /dev/null 2>&1 && dnf -y install socat > /dev/null 2>&1
+            dnf makecache -y > /dev/null 2>&1 && dnf -y install socat > /dev/null 2>&1
             ;;
         centos)
             if [[ "${VERSION_ID}" =~ ^7 ]]; then
-                yum -y update > /dev/null 2>&1 && yum -y install socat > /dev/null 2>&1
+                yum makecache -y > /dev/null 2>&1 && yum -y install socat > /dev/null 2>&1
             else
-                dnf -y update > /dev/null 2>&1 && dnf -y install socat > /dev/null 2>&1
+                dnf makecache -y > /dev/null 2>&1 && dnf -y install socat > /dev/null 2>&1
             fi
             ;;
         arch | manjaro | parch)
@@ -2286,14 +2286,14 @@ setup_fail2ban_iplimit() {
                 apt-get update && apt-get install fail2ban nftables -y
                 ;;
             fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
-                dnf -y update && dnf -y install fail2ban nftables
+                dnf makecache -y && dnf -y install fail2ban nftables
                 ;;
             centos)
                 if [[ "${VERSION_ID}" =~ ^7 ]]; then
-                    yum update -y && yum install epel-release -y
+                    yum makecache -y && yum install epel-release -y
                     yum -y install fail2ban nftables
                 else
-                    dnf -y update && dnf -y install fail2ban nftables
+                    dnf makecache -y && dnf -y install fail2ban nftables
                 fi
                 ;;
             arch | manjaro | parch)