Browse Source

feat(sub): make external subscription fetch User-Agent configurable (#6613)

* feat(sub): make external subscription fetch User-Agent configurable

Some providers reject fetches that do not send a known client User-Agent.
Expose externalSubUserAgent as a panel setting (default v2rayNG/1.8.5)
and use it when fetching client external subscription URLs.

Fixes #6383

* ci: retrigger frontend after npm registry maintenance

The frontend job failed solely on `npm audit` while registry.npmjs.org
returned 503 (Service Under Maintenance). Lint, typecheck, vitest, vite
build, and storybook all passed. Local `npm audit --omit=dev
--audit-level=high` now reports 0 vulnerabilities.

* fix(sub): fall back to the default UA when the DB is not initialised

externalSubUserAgent read the setting through SettingService.getSetting,
which calls Model() on database.GetDB() and panics on a nil *gorm.DB.
The fetch path's other DB read, service.ExternalSubscriptionHwid, already
treats a nil DB as unreachable and sends no header; the new UA lookup
did not, so any fetch before InitDB panicked instead of sending the
historical v2rayNG/1.8.5.

Production initialises the DB before the sub server starts, but the
internal/sub fetch tests run without one: under make test-go's
-shuffle=on, whenever one of them ran before the first InitDB test the
panic aborted the whole package. Reproduced deterministically with
go test -run '^TestDoFetchSubscriptionLinks_RejectsOversizedBody$'.

---------

Co-authored-by: mrchatam <[email protected]>
Co-authored-by: MHSanaei <[email protected]>
mrchatam 7 giờ trước cách đây
mục cha
commit
bd9ccde1f4

+ 8 - 0
docs/public/openapi.json

@@ -69,6 +69,9 @@
             "minimum": 0,
             "type": "integer"
           },
+          "externalSubUserAgent": {
+            "type": "string"
+          },
           "externalTrafficInformEnable": {
             "type": "boolean"
           },
@@ -522,6 +525,7 @@
           "discordMemory",
           "discordRunTime",
           "expireDiff",
+          "externalSubUserAgent",
           "externalTrafficInformEnable",
           "externalTrafficInformURI",
           "happLinkEnable",
@@ -704,6 +708,9 @@
             "minimum": 0,
             "type": "integer"
           },
+          "externalSubUserAgent": {
+            "type": "string"
+          },
           "externalTrafficInformEnable": {
             "type": "boolean"
           },
@@ -1181,6 +1188,7 @@
           "discordMemory",
           "discordRunTime",
           "expireDiff",
+          "externalSubUserAgent",
           "externalTrafficInformEnable",
           "externalTrafficInformURI",
           "happLinkEnable",

+ 8 - 0
frontend/public/openapi.json

@@ -69,6 +69,9 @@
             "minimum": 0,
             "type": "integer"
           },
+          "externalSubUserAgent": {
+            "type": "string"
+          },
           "externalTrafficInformEnable": {
             "type": "boolean"
           },
@@ -522,6 +525,7 @@
           "discordMemory",
           "discordRunTime",
           "expireDiff",
+          "externalSubUserAgent",
           "externalTrafficInformEnable",
           "externalTrafficInformURI",
           "happLinkEnable",
@@ -704,6 +708,9 @@
             "minimum": 0,
             "type": "integer"
           },
+          "externalSubUserAgent": {
+            "type": "string"
+          },
           "externalTrafficInformEnable": {
             "type": "boolean"
           },
@@ -1181,6 +1188,7 @@
           "discordMemory",
           "discordRunTime",
           "expireDiff",
+          "externalSubUserAgent",
           "externalTrafficInformEnable",
           "externalTrafficInformURI",
           "happLinkEnable",

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

@@ -13,6 +13,7 @@ export const EXAMPLES: Record<string, unknown> = {
     "discordMemory": 0,
     "discordRunTime": "",
     "expireDiff": 0,
+    "externalSubUserAgent": "",
     "externalTrafficInformEnable": false,
     "externalTrafficInformURI": "",
     "happLinkEnable": false,
@@ -163,6 +164,7 @@ export const EXAMPLES: Record<string, unknown> = {
     "discordMemory": 0,
     "discordRunTime": "",
     "expireDiff": 0,
+    "externalSubUserAgent": "",
     "externalTrafficInformEnable": false,
     "externalTrafficInformURI": "",
     "happLinkEnable": false,

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

@@ -43,6 +43,9 @@ export const SCHEMAS: Record<string, unknown> = {
         "minimum": 0,
         "type": "integer"
       },
+      "externalSubUserAgent": {
+        "type": "string"
+      },
       "externalTrafficInformEnable": {
         "type": "boolean"
       },
@@ -496,6 +499,7 @@ export const SCHEMAS: Record<string, unknown> = {
       "discordMemory",
       "discordRunTime",
       "expireDiff",
+      "externalSubUserAgent",
       "externalTrafficInformEnable",
       "externalTrafficInformURI",
       "happLinkEnable",
@@ -678,6 +682,9 @@ export const SCHEMAS: Record<string, unknown> = {
         "minimum": 0,
         "type": "integer"
       },
+      "externalSubUserAgent": {
+        "type": "string"
+      },
       "externalTrafficInformEnable": {
         "type": "boolean"
       },
@@ -1155,6 +1162,7 @@ export const SCHEMAS: Record<string, unknown> = {
       "discordMemory",
       "discordRunTime",
       "expireDiff",
+      "externalSubUserAgent",
       "externalTrafficInformEnable",
       "externalTrafficInformURI",
       "happLinkEnable",

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

@@ -21,6 +21,7 @@ export interface AllSetting {
   discordMemory: number;
   discordRunTime: string;
   expireDiff: number;
+  externalSubUserAgent: string;
   externalTrafficInformEnable: boolean;
   externalTrafficInformURI: string;
   happLinkEnable: boolean;
@@ -172,6 +173,7 @@ export interface AllSettingView {
   discordMemory: number;
   discordRunTime: string;
   expireDiff: number;
+  externalSubUserAgent: string;
   externalTrafficInformEnable: boolean;
   externalTrafficInformURI: string;
   happLinkEnable: boolean;

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

@@ -37,6 +37,7 @@ export const AllSettingSchema = z.object({
   discordMemory: z.number().int().min(0).max(100),
   discordRunTime: z.string(),
   expireDiff: z.number().int().min(0),
+  externalSubUserAgent: z.string(),
   externalTrafficInformEnable: z.boolean(),
   externalTrafficInformURI: z.string(),
   happLinkEnable: z.boolean(),
@@ -189,6 +190,7 @@ export const AllSettingViewSchema = z.object({
   discordMemory: z.number().int().min(0).max(100),
   discordRunTime: z.string(),
   expireDiff: z.number().int().min(0),
+  externalSubUserAgent: z.string(),
   externalTrafficInformEnable: z.boolean(),
   externalTrafficInformURI: z.string(),
   happLinkEnable: z.boolean(),

+ 1 - 0
frontend/src/models/setting.ts

@@ -66,6 +66,7 @@ export class AllSetting {
   restartXrayOnClientDisable = true;
   subCertFile = '';
   subKeyFile = '';
+  externalSubUserAgent = 'v2rayNG/1.8.5';
   subUpdates = 12;
   subEncrypt = true;
   subURI = '';

+ 20 - 0
frontend/src/pages/settings/SubscriptionGeneralTab.tsx

@@ -247,6 +247,26 @@ export default function SubscriptionGeneralTab({
                   onChange={onNumber((v) => updateSetting({ subUpdates: v }))}
                 />
               </SettingListItem>
+
+              <SettingListItem
+                paddings="small"
+                title={t('pages.settings.externalSubUserAgent')}
+                badge={
+                  <DefaultSettingTag
+                    settingKey="externalSubUserAgent"
+                    value={allSetting.externalSubUserAgent}
+                  />
+                }
+                description={t('pages.settings.externalSubUserAgentDesc')}
+              >
+                <Input
+                  value={allSetting.externalSubUserAgent}
+                  placeholder="v2rayNG/1.8.5"
+                  maxLength={512}
+                  allowClear
+                  onChange={(e) => updateSetting({ externalSubUserAgent: e.target.value })}
+                />
+              </SettingListItem>
             </>
           ),
         },

+ 1 - 0
frontend/src/schemas/setting.ts

@@ -72,6 +72,7 @@ export const AllSettingSchema = z
     restartXrayOnClientDisable: z.boolean().optional(),
     subCertFile: z.string().optional(),
     subKeyFile: z.string().optional(),
+    externalSubUserAgent: z.string().max(512).optional(),
     subUpdates: z.number().int().min(0).max(525600).optional(),
     subEncrypt: z.boolean().optional(),
     subURI: z.string().optional(),

+ 14 - 1
internal/sub/external_subscription.go

@@ -148,7 +148,7 @@ func doFetchSubscriptionLinks(rawURL string) ([]string, error) {
 		return nil, err
 	}
 	// Some providers gate the link body on a known client User-Agent.
-	req.Header.Set("User-Agent", "v2rayNG/1.8.5")
+	req.Header.Set("User-Agent", externalSubUserAgent())
 	// A 3x-ui donor with an HWID limit answers 404 when the header is empty (#6559).
 	if hwid := service.ExternalSubscriptionHwid(); hwid != "" {
 		req.Header.Set("X-HWID", hwid)
@@ -176,6 +176,19 @@ var (
 	errSubscriptionBodyTooLarge = &subError{"subscription response body exceeds size limit"}
 )
 
+// externalSubUserAgent returns the panel setting for external subscription
+// fetches, or the historical client UA when it is unset or the DB is unreachable.
+func externalSubUserAgent() string {
+	if database.GetDB() == nil {
+		return service.DefaultExternalSubUserAgent
+	}
+	ua, err := (&service.SettingService{}).GetExternalSubUserAgent()
+	if err != nil {
+		return service.DefaultExternalSubUserAgent
+	}
+	return ua
+}
+
 type subError struct{ msg string }
 
 func (e *subError) Error() string { return e.msg }

+ 55 - 0
internal/sub/external_subscription_test.go

@@ -4,6 +4,7 @@ import (
 	"errors"
 	"net/http"
 	"net/http/httptest"
+	"path/filepath"
 	"strconv"
 	"strings"
 	"sync"
@@ -298,3 +299,57 @@ func TestExpandEntryCacheHitWritesNothing(t *testing.T) {
 		t.Fatalf("cache hit wrote fetch status: %#v", after)
 	}
 }
+
+func TestFetchUsesConfiguredExternalSubUserAgent(t *testing.T) {
+	resetSubscriptionCache(t)
+	if err := database.InitDB(filepath.Join(t.TempDir(), "ua.db")); err != nil {
+		t.Fatalf("InitDB: %v", err)
+	}
+	t.Cleanup(func() { _ = database.CloseDB() })
+
+	const customUA = "Happ/4.2.1"
+	if err := database.GetDB().Create(&model.Setting{
+		Key:   "externalSubUserAgent",
+		Value: customUA,
+	}).Error; err != nil {
+		t.Fatalf("save setting: %v", err)
+	}
+
+	var gotUA string
+	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		gotUA = r.Header.Get("User-Agent")
+		_, _ = w.Write([]byte("vless://uuid@host:443?security=none#x"))
+	}))
+	defer srv.Close()
+
+	res := fetchSubscriptionLinks(srv.URL)
+	if res.err != nil {
+		t.Fatalf("fetch: %v", res.err)
+	}
+	if gotUA != customUA {
+		t.Fatalf("User-Agent = %q, want %q", gotUA, customUA)
+	}
+}
+
+func TestFetchFallsBackToDefaultExternalSubUserAgent(t *testing.T) {
+	resetSubscriptionCache(t)
+	if err := database.InitDB(filepath.Join(t.TempDir(), "ua-default.db")); err != nil {
+		t.Fatalf("InitDB: %v", err)
+	}
+	t.Cleanup(func() { _ = database.CloseDB() })
+
+	var gotUA string
+	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		gotUA = r.Header.Get("User-Agent")
+		_, _ = w.Write([]byte("vless://uuid@host:443?security=none#x"))
+	}))
+	defer srv.Close()
+
+	res := fetchSubscriptionLinks(srv.URL)
+	if res.err != nil {
+		t.Fatalf("fetch: %v", res.err)
+	}
+	if gotUA != "v2rayNG/1.8.5" {
+		t.Fatalf("User-Agent = %q, want default v2rayNG/1.8.5", gotUA)
+	}
+}

+ 1 - 0
internal/web/entity/entity.go

@@ -107,6 +107,7 @@ type AllSetting struct {
 	SubDomain                   string `json:"subDomain" form:"subDomain"`
 	SubCertFile                 string `json:"subCertFile" form:"subCertFile"`
 	SubKeyFile                  string `json:"subKeyFile" form:"subKeyFile"`
+	ExternalSubUserAgent        string `json:"externalSubUserAgent" form:"externalSubUserAgent"`
 	SubUpdates                  int    `json:"subUpdates" form:"subUpdates" validate:"gte=0,lte=525600"`
 	ExternalTrafficInformEnable bool   `json:"externalTrafficInformEnable" form:"externalTrafficInformEnable"`
 	ExternalTrafficInformURI    string `json:"externalTrafficInformURI" form:"externalTrafficInformURI"`

+ 13 - 0
internal/web/service/setting.go

@@ -36,6 +36,7 @@ var xrayTemplateConfig string
 
 const (
 	DefaultSubClashUserAgentRegex     = `(?i)(clash|mihomo)`
+	DefaultExternalSubUserAgent       = "v2rayNG/1.8.5"
 	DefaultSubJsonUserAgentRegex      = ``
 	DefaultRemarkTemplate             = "{{INBOUND}}-{{EMAIL}}|📊{{TRAFFIC_LEFT}}|⏳{{DAYS_LEFT}}D"
 	DefaultSubExpiredTemplate         = "⛔ {{EMAIL}} | Expired: {{EXPIRE_DATE}}"
@@ -146,6 +147,7 @@ var defaultValueMap = map[string]string{
 	"subDomain":                   "",
 	"subCertFile":                 "",
 	"subKeyFile":                  "",
+	"externalSubUserAgent":        DefaultExternalSubUserAgent,
 	"subUpdates":                  "12",
 	"subEncrypt":                  "true",
 	"subURI":                      "",
@@ -1052,6 +1054,17 @@ func (s *SettingService) GetSubUpdates() (string, error) {
 	return s.getString("subUpdates")
 }
 
+func (s *SettingService) GetExternalSubUserAgent() (string, error) {
+	value, err := s.getString("externalSubUserAgent")
+	if err != nil {
+		return DefaultExternalSubUserAgent, err
+	}
+	if value = strings.TrimSpace(value); value == "" {
+		return DefaultExternalSubUserAgent, nil
+	}
+	return value, nil
+}
+
 func (s *SettingService) GetSubEncrypt() (bool, error) {
 	return s.getBool("subEncrypt")
 }

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

@@ -1273,6 +1273,8 @@
       "subDomainDesc": "اسم الدومين لخدمة الاشتراك. (سيبه فاضي عشان يستمع على كل الدومينات والـ IPs). كمان بيتستخدم كدومين افتراضي لرابط الاشتراك اللي بيظهر لو حقل «مسار البروكسي العكسي» فاضي — املا «مسار البروكسي العكسي» لو اللوحة والاشتراك بيتفتحوا من دومينات مختلفة (زي لما يكونوا ورا بروكسي عكسي).",
       "subUpdates": "فترات التحديث",
       "subUpdatesDesc": "فترات تحديث رابط الاشتراك في تطبيقات العملاء. (الوحدة: ساعة)",
+      "externalSubUserAgent": "User-Agent للاشتراك الخارجي",
+      "externalSubUserAgentDesc": "ترويسة User-Agent عند جلب رابط الاشتراك الخارجي للعميل. اتركه فارغًا لاستخدام الافتراضي (v2rayNG/1.8.5). بعض المزودين يرفضون العملاء غير المعروفين.",
       "subEncrypt": "تشفير",
       "subEncryptDesc": "المحتوى اللي هيترجع من خدمة الاشتراك هيكون مشفر بـ Base64.",
       "subURI": "مسار البروكسي العكسي",

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

@@ -1395,6 +1395,8 @@
       "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)",
+      "externalSubUserAgent": "External subscription User-Agent",
+      "externalSubUserAgentDesc": "User-Agent header sent when the panel fetches a client external subscription URL. Leave blank to use the default (v2rayNG/1.8.5). Some providers reject unknown clients.",
       "subEncrypt": "Encode",
       "subEncryptDesc": "The returned content of subscription service will be Base64 encoded.",
       "subURI": "Reverse Proxy URI",

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

@@ -1273,6 +1273,8 @@
       "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.",
+      "externalSubUserAgent": "User-Agent de suscripción externa",
+      "externalSubUserAgentDesc": "Cabecera User-Agent al obtener la URL de suscripción externa del cliente. Déjelo vacío para usar el valor predeterminado (v2rayNG/1.8.5). Algunos proveedores rechazan clientes desconocidos.",
       "subEncrypt": "Codificar",
       "subEncryptDesc": "Encriptar las configuraciones devueltas en la suscripción.",
       "subURI": "URI de proxy inverso",

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

@@ -1277,6 +1277,8 @@
       "subDomainDesc": "آدرس دامنه برای سرویس سابسکریپشن. برای گوش دادن به تمام دامنه‌ها و آی‌پی‌ها خالی‌بگذارید. همچنین وقتی «مسیر پراکسی معکوس» خالی باشد، به عنوان دامنه پیش‌فرض لینک سابسکریپشن نمایش‌داده‌شده استفاده می‌شود — اگر پنل و سابسکریپشن از دامنه‌های متفاوتی در دسترس هستند (مثلاً پشت یک پراکسی معکوس)، «مسیر پراکسی معکوس» را پر کنید.",
       "subUpdates": "فاصله بروزرسانی‌ سابسکریپشن",
       "subUpdatesDesc": "(فاصله مابین بروزرسانی در برنامه‌های کاربری. (واحد: ساعت",
+      "externalSubUserAgent": "User-Agent اشتراک خارجی",
+      "externalSubUserAgentDesc": "هدر User-Agent هنگام واکشی URL اشتراک خارجی کلاینت. خالی بگذارید تا پیش‌فرض (v2rayNG/1.8.5) استفاده شود. بعضی ارائه‌دهنده‌ها کلاینت ناشناس را رد می‌کنند.",
       "subEncrypt": "انکود",
       "subEncryptDesc": "کدگذاری خواهدشد Base64 محتوای برگشتی سرویس سابسکریپشن برپایه",
       "subURI": "پروکسی معکوس URI مسیر",

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

@@ -1273,6 +1273,8 @@
       "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)",
+      "externalSubUserAgent": "User-Agent langganan eksternal",
+      "externalSubUserAgentDesc": "Header User-Agent saat panel mengambil URL langganan eksternal klien. Kosongkan untuk memakai bawaan (v2rayNG/1.8.5). Beberapa penyedia menolak klien yang tidak dikenal.",
       "subEncrypt": "Encode",
       "subEncryptDesc": "Konten yang dikembalikan dari layanan langganan akan dienkripsi Base64.",
       "subURI": "URI Proxy Terbalik",

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

@@ -1273,6 +1273,8 @@
       "subDomainDesc": "サブスクリプションサービスが監視するドメイン(空白にするとすべてのドメインとIPを監視)。「リバースプロキシURI」が空の場合、表示されるサブスクリプションリンクのフォールバックドメインとしても使われます — パネルとサブスクリプションが異なるドメイン(例: リバースプロキシの背後)でアクセスされる場合は「リバースプロキシURI」を設定してください。",
       "subUpdates": "更新間隔",
       "subUpdatesDesc": "クライアントアプリケーションでサブスクリプションURLの更新間隔(単位:時間)",
+      "externalSubUserAgent": "外部サブスクリプションの User-Agent",
+      "externalSubUserAgentDesc": "クライアントの外部サブスクリプション URL を取得するときに送る User-Agent。空欄の場合はデフォルト(v2rayNG/1.8.5)を使用します。一部のプロバイダーは未知のクライアントを拒否します。",
       "subEncrypt": "エンコード",
       "subEncryptDesc": "サブスクリプションサービスが返す内容をBase64エンコードする",
       "subURI": "リバースプロキシURI",

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

@@ -1273,6 +1273,8 @@
       "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)",
+      "externalSubUserAgent": "User-Agent de assinatura externa",
+      "externalSubUserAgentDesc": "Cabeçalho User-Agent ao buscar a URL de assinatura externa do cliente. Deixe em branco para usar o padrão (v2rayNG/1.8.5). Alguns provedores rejeitam clientes desconhecidos.",
       "subEncrypt": "Codificar",
       "subEncryptDesc": "O conteúdo retornado pelo serviço de assinatura será codificado em Base64.",
       "subURI": "URI de Proxy Reverso",

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

@@ -1273,6 +1273,8 @@
       "subDomainDesc": "Оставьте пустым по умолчанию, чтобы слушать все домены и IP-адреса. Также используется как домен по умолчанию для отображаемой ссылки подписки, если поле «URI обратного прокси» пустое — заполните «URI обратного прокси», если панель и подписка доступны на разных доменах (например, за reverse-proxy).",
       "subUpdates": "Интервалы обновления подписки",
       "subUpdatesDesc": "Интервал между обновлениями в клиентском приложении (в часах)",
+      "externalSubUserAgent": "User-Agent внешней подписки",
+      "externalSubUserAgentDesc": "Заголовок User-Agent при загрузке внешней подписки клиента. Оставьте пустым для значения по умолчанию (v2rayNG/1.8.5). Некоторые провайдеры отклоняют неизвестных клиентов.",
       "subEncrypt": "Кодировать",
       "subEncryptDesc": "Шифровать возвращенные конфиги в подписке",
       "subURI": "URI обратного прокси",

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

@@ -1273,6 +1273,8 @@
       "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)",
+      "externalSubUserAgent": "Harici abonelik User-Agent",
+      "externalSubUserAgentDesc": "Panel istemci harici abonelik URL’sini çekerken gönderilen User-Agent. Boş bırakılırsa varsayılan (v2rayNG/1.8.5) kullanılır. Bazı sağlayıcılar bilinmeyen istemcileri reddeder.",
       "subEncrypt": "Kodla",
       "subEncryptDesc": "Abonelik hizmetinin döndürülen içeriğini Base64 ile şifreler.",
       "subURI": "Ters Proxy URI",

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

@@ -1273,6 +1273,8 @@
       "subDomainDesc": "Ім'я домену для служби підписки. (залиште порожнім, щоб слухати всі домени та IP-адреси). Також використовується як резервний домен для показаного посилання підписки, коли «URI зворотного проксі» порожнє — вкажіть «URI зворотного проксі», якщо панель і підписка доступні через різні домени (наприклад, за зворотним проксі).",
       "subUpdates": "Інтервали оновлення",
       "subUpdatesDesc": "Інтервали оновлення URL-адреси підписки в клієнтських програмах. (одиниця: година)",
+      "externalSubUserAgent": "User-Agent зовнішньої підписки",
+      "externalSubUserAgentDesc": "Заголовок User-Agent під час завантаження зовнішньої підписки клієнта. Залиште порожнім для значення за замовчуванням (v2rayNG/1.8.5). Деякі провайдери відхиляють невідомих клієнтів.",
       "subEncrypt": "Кодувати",
       "subEncryptDesc": "Повернений вміст послуги підписки матиме кодування Base64.",
       "subURI": "URI зворотного проксі",

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

@@ -1273,6 +1273,8 @@
       "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",
+      "externalSubUserAgent": "User-Agent đăng ký bên ngoài",
+      "externalSubUserAgentDesc": "Header User-Agent khi panel tải URL đăng ký bên ngoài của client. Để trống để dùng mặc định (v2rayNG/1.8.5). Một số nhà cung cấp từ chối client lạ.",
       "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",

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

@@ -1273,6 +1273,8 @@
       "subDomainDesc": "订阅服务监听的域名(留空表示监听所有域名和 IP)。当「反向代理 URI」为空时,也会作为显示的订阅链接的回退域名——如果面板和订阅通过不同的域名访问(例如位于反向代理之后),请设置「反向代理 URI」。",
       "subUpdates": "更新间隔",
       "subUpdatesDesc": "客户端应用中订阅 URL 的更新间隔(单位:小时)",
+      "externalSubUserAgent": "外部订阅 User-Agent",
+      "externalSubUserAgentDesc": "面板拉取客户端外部订阅 URL 时发送的 User-Agent。留空则使用默认值(v2rayNG/1.8.5)。部分服务商会拒绝未知客户端。",
       "subEncrypt": "编码",
       "subEncryptDesc": "订阅服务返回的内容将采用 Base64 编码",
       "subURI": "反向代理 URI",

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

@@ -1273,6 +1273,8 @@
       "subDomainDesc": "訂閱服務監聽的域名(留空表示監聽所有域名和 IP)。當「反向代理 URI」為空時,也會作為顯示的訂閱連結的備援域名——如果面板和訂閱透過不同的域名存取(例如位於反向代理之後),請設定「反向代理 URI」。",
       "subUpdates": "更新間隔",
       "subUpdatesDesc": "客戶端應用中訂閱 URL 的更新間隔(單位:小時)",
+      "externalSubUserAgent": "外部訂閱 User-Agent",
+      "externalSubUserAgentDesc": "面板擷取用戶外部訂閱 URL 時傳送的 User-Agent。留空則使用預設值(v2rayNG/1.8.5)。部分服務商會拒絕未知用戶端。",
       "subEncrypt": "編碼",
       "subEncryptDesc": "訂閱服務返回的內容將採用 Base64 編碼",
       "subURI": "反向代理 URI",