Browse Source

feat(outbounds): pick dialerProxy from other outbound tags for proxy chaining

Turn the outbound sockopt dialerProxy free-text input into a searchable Select populated with the other outbound tags, so users can build a proxy chain (route one outbound through another) without typing tags by hand. The list excludes the current outbound, so self-reference cycles cannot be selected. A tooltip and placeholder explain the chaining concept. Adds dialerProxyPlaceholder and dialerProxyHint to all 13 locales.

Closes #4446
MHSanaei 1 day ago
parent
commit
7bc31dd194

+ 3 - 1
frontend/src/pages/xray/outbounds/OutboundFormModal.tsx

@@ -575,7 +575,9 @@ export default function OutboundFormModal({
 
                     {security === 'reality' && realityAllowed && <RealityForm />}
 
-                    {((streamAllowed && network) || !streamAllowed) && <SockoptForm form={form} />}
+                    {((streamAllowed && network) || !streamAllowed) && (
+                      <SockoptForm form={form} outboundTags={existingTags} />
+                    )}
 
                     <FinalMaskForm
                       name={['streamSettings', 'finalmask']}

+ 22 - 2
frontend/src/pages/xray/outbounds/transport/sockopt.tsx

@@ -7,7 +7,13 @@ import type { OutboundFormValues } from '@/schemas/forms/outbound-form';
 
 import { ADDRESS_PORT_STRATEGY_OPTIONS } from '../outbound-form-constants';
 
-export default function SockoptForm({ form }: { form: FormInstance<OutboundFormValues> }) {
+export default function SockoptForm({
+  form,
+  outboundTags = [],
+}: {
+  form: FormInstance<OutboundFormValues>;
+  outboundTags?: string[];
+}) {
   const { t } = useTranslation();
   return (
     <Form.Item shouldUpdate noStyle>
@@ -16,6 +22,14 @@ export default function SockoptForm({ form }: { form: FormInstance<OutboundFormV
           'streamSettings',
           'sockopt',
         ]);
+        const dialerProxy = (form.getFieldValue([
+          'streamSettings',
+          'sockopt',
+          'dialerProxy',
+        ]) ?? '') as string;
+        const dialerProxyOptions = Array.from(
+          new Set([...outboundTags, dialerProxy].filter(Boolean)),
+        ).map((tg) => ({ value: tg, label: tg }));
         return (
           <>
             <Form.Item label={t('pages.xray.outboundForm.sockopts')}>
@@ -34,8 +48,14 @@ export default function SockoptForm({ form }: { form: FormInstance<OutboundFormV
                 <Form.Item
                   label={t('pages.inbounds.form.dialerProxy')}
                   name={['streamSettings', 'sockopt', 'dialerProxy']}
+                  tooltip={t('pages.xray.outboundForm.dialerProxyHint')}
                 >
-                  <Input />
+                  <Select
+                    allowClear
+                    showSearch
+                    placeholder={t('pages.xray.outboundForm.dialerProxyPlaceholder')}
+                    options={dialerProxyOptions}
+                  />
                 </Form.Item>
                 <Form.Item
                   label={t('pages.xray.wireguard.domainStrategy')}

+ 2 - 0
web/translation/ar-EG.json

@@ -1206,6 +1206,8 @@
         "tagRequired": "الوسم مطلوب",
         "tagPlaceholder": "وسم-فريد",
         "localIpPlaceholder": "IP محلي",
+        "dialerProxyPlaceholder": "اختر مخرجًا لتمرير الاتصال عبره",
+        "dialerProxyHint": "وجّه هذا المخرج عبر مخرج آخر (حسب الوسم) لبناء سلسلة بروكسي. اتركه فارغًا للاتصال المباشر.",
         "addressRequired": "العنوان مطلوب",
         "portRequired": "المنفذ مطلوب",
         "optional": "اختياري",

+ 2 - 0
web/translation/en-US.json

@@ -1206,6 +1206,8 @@
         "tagRequired": "Tag is required",
         "tagPlaceholder": "unique-tag",
         "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.",
         "addressRequired": "Address is required",
         "portRequired": "Port is required",
         "optional": "optional",

+ 2 - 0
web/translation/es-ES.json

@@ -1206,6 +1206,8 @@
         "tagRequired": "La etiqueta es obligatoria",
         "tagPlaceholder": "etiqueta-única",
         "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.",
         "addressRequired": "La dirección es obligatoria",
         "portRequired": "El puerto es obligatorio",
         "optional": "opcional",

+ 2 - 0
web/translation/fa-IR.json

@@ -1206,6 +1206,8 @@
         "tagRequired": "تگ الزامی است",
         "tagPlaceholder": "تگ-منحصربه‌فرد",
         "localIpPlaceholder": "IP محلی",
+        "dialerProxyPlaceholder": "یک خروجی برای زنجیره کردن انتخاب کنید",
+        "dialerProxyHint": "این خروجی را از طریق خروجی دیگری (با تگ) برقرار کن تا یک زنجیره پروکسی ساخته شود. برای اتصال مستقیم خالی بگذار.",
         "addressRequired": "آدرس الزامی است",
         "portRequired": "پورت الزامی است",
         "optional": "اختیاری",

+ 2 - 0
web/translation/id-ID.json

@@ -1206,6 +1206,8 @@
         "tagRequired": "Tag wajib diisi",
         "tagPlaceholder": "tag-unik",
         "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.",
         "addressRequired": "Alamat wajib diisi",
         "portRequired": "Port wajib diisi",
         "optional": "opsional",

+ 2 - 0
web/translation/ja-JP.json

@@ -1206,6 +1206,8 @@
         "tagRequired": "タグは必須です",
         "tagPlaceholder": "一意のタグ",
         "localIpPlaceholder": "ローカル IP",
+        "dialerProxyPlaceholder": "経由するアウトバウンドを選択",
+        "dialerProxyHint": "このアウトバウンドを別のアウトバウンド(タグ指定)経由で接続し、プロキシチェーンを構成します。直接接続する場合は空のままにします。",
         "addressRequired": "アドレスは必須です",
         "portRequired": "ポートは必須です",
         "optional": "任意",

+ 2 - 0
web/translation/pt-BR.json

@@ -1206,6 +1206,8 @@
         "tagRequired": "A tag é obrigatória",
         "tagPlaceholder": "tag-única",
         "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.",
         "addressRequired": "Endereço é obrigatório",
         "portRequired": "Porta é obrigatória",
         "optional": "opcional",

+ 2 - 0
web/translation/ru-RU.json

@@ -1206,6 +1206,8 @@
         "tagRequired": "Тег обязателен",
         "tagPlaceholder": "уникальный-тег",
         "localIpPlaceholder": "локальный IP",
+        "dialerProxyPlaceholder": "Выберите исходящее для цепочки",
+        "dialerProxyHint": "Подключайте это исходящее через другое исходящее (по тегу), чтобы построить цепочку прокси. Оставьте пустым для прямого подключения.",
         "addressRequired": "Адрес обязателен",
         "portRequired": "Порт обязателен",
         "optional": "опционально",

+ 2 - 0
web/translation/tr-TR.json

@@ -1206,6 +1206,8 @@
         "tagRequired": "Etiket gereklidir",
         "tagPlaceholder": "benzersiz-etiket",
         "localIpPlaceholder": "yerel IP",
+        "dialerProxyPlaceholder": "Zincirlemek için bir giden seçin",
+        "dialerProxyHint": "Bir proxy zinciri oluşturmak için bu gideni başka bir giden üzerinden (etikete göre) bağlayın. Doğrudan bağlanmak için boş bırakın.",
         "addressRequired": "Adres gereklidir",
         "portRequired": "Port gereklidir",
         "optional": "opsiyonel",

+ 2 - 0
web/translation/uk-UA.json

@@ -1206,6 +1206,8 @@
         "tagRequired": "Тег обов'язковий",
         "tagPlaceholder": "унікальний-тег",
         "localIpPlaceholder": "локальний IP",
+        "dialerProxyPlaceholder": "Виберіть вихідний для ланцюжка",
+        "dialerProxyHint": "Підключайте цей вихідний через інший вихідний (за тегом), щоб побудувати ланцюжок проксі. Залиште порожнім для прямого підключення.",
         "addressRequired": "Адреса обов'язкова",
         "portRequired": "Порт обов'язковий",
         "optional": "опційно",

+ 2 - 0
web/translation/vi-VN.json

@@ -1206,6 +1206,8 @@
         "tagRequired": "Tag là bắt buộc",
         "tagPlaceholder": "tag-duy-nhất",
         "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.",
         "addressRequired": "Địa chỉ là bắt buộc",
         "portRequired": "Cổng là bắt buộc",
         "optional": "tùy chọn",

+ 2 - 0
web/translation/zh-CN.json

@@ -1206,6 +1206,8 @@
         "tagRequired": "标签为必填项",
         "tagPlaceholder": "唯一标签",
         "localIpPlaceholder": "本地 IP",
+        "dialerProxyPlaceholder": "选择要串联的出站",
+        "dialerProxyHint": "让此出站通过另一个出站(按标签)拨号,以建立代理链。留空则直接连接。",
         "addressRequired": "地址为必填项",
         "portRequired": "端口为必填项",
         "optional": "可选",

+ 2 - 0
web/translation/zh-TW.json

@@ -1206,6 +1206,8 @@
         "tagRequired": "標籤為必填",
         "tagPlaceholder": "唯一標籤",
         "localIpPlaceholder": "本地 IP",
+        "dialerProxyPlaceholder": "選擇要串接的出站",
+        "dialerProxyHint": "讓此出站透過另一個出站(以標籤指定)連線,以建立代理鏈。留空則直接連線。",
         "addressRequired": "地址為必填",
         "portRequired": "連接埠為必填",
         "optional": "選用",