From 7982794291661342eb58e7394be12fc84aed06fa Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Sun, 1 Nov 2026 14:55:41 +0100 Subject: [PATCH] fix(xray): refuse geodata settings xray-core 26.10.29 refuses at startup xray-core 26.10.29 (#7267) checks a scheduled geodata section when the core starts: at most 100 assets, every file a .dat, a cron it can parse and an @every interval of at least a minute. A refusal there stops the core and every inbound with it, and the panel's own file-name pattern still let any extension through. The geodata editor now requires a .dat file name, and the template save path applies the same checks the core does. The invalid-file hint names the .dat requirement in all 14 locales. --- frontend/src/pages/index/GeodataSection.tsx | 4 +-- frontend/src/test/geodata-section.test.tsx | 29 +++++++++++++++ internal/web/service/xray_setting.go | 37 +++++++++++++++++++ internal/web/service/xray_setting_test.go | 39 +++++++++++++++++++++ internal/web/translation/ar-EG.json | 2 +- internal/web/translation/en-US.json | 2 +- internal/web/translation/es-ES.json | 2 +- internal/web/translation/fa-IR.json | 2 +- internal/web/translation/id-ID.json | 2 +- internal/web/translation/ja-JP.json | 2 +- internal/web/translation/pt-BR.json | 2 +- internal/web/translation/ru-RU.json | 2 +- internal/web/translation/sk-SK.json | 2 +- internal/web/translation/tr-TR.json | 2 +- internal/web/translation/uk-UA.json | 2 +- internal/web/translation/vi-VN.json | 2 +- internal/web/translation/zh-CN.json | 2 +- internal/web/translation/zh-TW.json | 2 +- 18 files changed, 121 insertions(+), 16 deletions(-) diff --git a/frontend/src/pages/index/GeodataSection.tsx b/frontend/src/pages/index/GeodataSection.tsx index 4af726925..94f94f531 100644 --- a/frontend/src/pages/index/GeodataSection.tsx +++ b/frontend/src/pages/index/GeodataSection.tsx @@ -19,8 +19,8 @@ interface GeodataSectionProps { } const DEFAULT_CRON = '0 4 * * *'; -// Xray resolves `file` inside its asset directory; plain file names only. -const FILE_NAME_PATTERN = /^[A-Za-z0-9._-]+$/; +// Xray resolves `file` inside its asset directory; since 26.10.29 a scheduled one must be .dat. +const FILE_NAME_PATTERN = /^[A-Za-z0-9._-]+\.dat$/i; function fileNameFromUrl(url: string): string { try { diff --git a/frontend/src/test/geodata-section.test.tsx b/frontend/src/test/geodata-section.test.tsx index 8c11ffb53..6b7e842f9 100644 --- a/frontend/src/test/geodata-section.test.tsx +++ b/frontend/src/test/geodata-section.test.tsx @@ -76,4 +76,33 @@ describe('GeodataSection', () => { } }); }); + + // xray-core 26.10.29 refuses a scheduled asset whose file is not .dat when it starts. + it('refuses to save a scheduled asset whose file is not .dat', async () => { + const post = vi.spyOn(HttpUtil, 'post').mockResolvedValue( + new Msg( + true, + '', + JSON.stringify({ + xraySetting: { + outbounds: [], + geodata: { cron: '0 4 * * *', assets: [CUSTOM_SOURCE] }, + }, + geodataSources: [], + }), + ), + ); + const user = userEvent.setup(); + + render(); + + const file = await screen.findByDisplayValue(CUSTOM_SOURCE.file); + await user.clear(file); + await user.type(file, 'geosite_custom.mmdb'); + await user.click(screen.getByRole('button', { name: 'Save & Restart Xray' })); + + expect(await screen.findByText(/File names must be/)).toBeTruthy(); + expect(screen.queryByText('Save geodata settings?')).toBeNull(); + expect(post).toHaveBeenCalledTimes(1); + }); }); diff --git a/internal/web/service/xray_setting.go b/internal/web/service/xray_setting.go index 79d49c081..048a75f3e 100644 --- a/internal/web/service/xray_setting.go +++ b/internal/web/service/xray_setting.go @@ -5,9 +5,13 @@ import ( "encoding/base64" "encoding/json" "net/netip" + "path/filepath" "slices" "strconv" "strings" + "time" + + "github.com/robfig/cron/v3" "github.com/mhsanaei/3x-ui/v3/internal/amneziawg" "github.com/mhsanaei/3x-ui/v3/internal/database" @@ -81,6 +85,9 @@ func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error { if err := checkTemplateLuaScripts(xrayConfig); err != nil { return err } + if err := checkTemplateGeodata([]byte(xrayConfig.Geodata)); err != nil { + return err + } for _, inbound := range xrayConfig.InboundConfigs { if err := validateFinalMaskSalamander(string(inbound.StreamSettings)); err != nil { return common.NewError("xray template inbound \""+inbound.Tag+"\":", err) @@ -158,6 +165,36 @@ func checkTemplateLuaScripts(cfg *xray.Config) error { return nil } +// checkTemplateGeodata applies the checks xray-core 26.10.29 (#7267) runs on a scheduled geodata +// section only when the core starts, where a refusal stops every inbound. +func checkTemplateGeodata(raw []byte) error { + var geodata struct { + Cron string `json:"cron"` + Assets []struct { + File string `json:"file"` + } `json:"assets"` + } + if len(raw) == 0 || json.Unmarshal(raw, &geodata) != nil || geodata.Cron == "" { + return nil + } + if len(geodata.Assets) > 100 { + return common.NewError("xray template geodata: xray-core supports at most 100 assets.") + } + for _, asset := range geodata.Assets { + if !strings.EqualFold(filepath.Ext(asset.File), ".dat") { + return common.NewError(`xray template geodata: file extension must be .dat, not "` + asset.File + `".`) + } + } + schedule, err := cron.ParseStandard(geodata.Cron) + if err != nil { + return common.NewError("xray template geodata: invalid geodata cron:", err) + } + if interval, ok := schedule.(cron.ConstantDelaySchedule); ok && interval.Delay < time.Minute { + return common.NewError("xray template geodata: the cron interval must be at least 1 minute.") + } + return nil +} + // shouldSkipLegacyUnencryptedOutboundRejection keeps an unencrypted outbound the running core still // allows: any address before 26.7.11, a legacy-private IP before 26.10.29; unknown versions stay strict. func shouldSkipLegacyUnencryptedOutboundRejection(coreVersion string, outbound []byte, err error) bool { diff --git a/internal/web/service/xray_setting_test.go b/internal/web/service/xray_setting_test.go index 20ba90fe9..58b79897a 100644 --- a/internal/web/service/xray_setting_test.go +++ b/internal/web/service/xray_setting_test.go @@ -3,6 +3,7 @@ package service import ( "encoding/json" "errors" + "strconv" "strings" "testing" @@ -289,6 +290,44 @@ func TestCheckXrayConfig_RefusesLuaScriptsTheCoreRefuses(t *testing.T) { } } +// xray-core 26.10.29 (#7267) refuses these geodata settings when the core starts, stopping every inbound. +func TestCheckXrayConfig_RefusesGeodataTheCoreRefuses(t *testing.T) { + const outbounds = `"outbounds":[{"protocol":"freedom","tag":"direct"}]` + geodata := func(cron string, files ...string) string { + assets := make([]string, len(files)) + for i, f := range files { + assets[i] = `{"url":"https://example.com/` + f + `","file":"` + f + `"}` + } + return `{"geodata":{"cron":"` + cron + `","assets":[` + strings.Join(assets, ",") + `]},` + outbounds + `}` + } + tooMany := make([]string, 101) + for i := range tooMany { + tooMany[i] = "geo" + strconv.Itoa(i) + ".dat" + } + tests := []struct{ name, template, want string }{ + {"file that is not .dat", geodata("0 4 * * *", "custom.mmdb"), "file extension must be .dat"}, + {"more than 100 assets", geodata("0 4 * * *", tooMany...), "at most 100 assets"}, + {"cron the core cannot parse", geodata("every day", "custom.dat"), "invalid geodata cron"}, + {"interval under a minute", geodata("@every 30s", "custom.dat"), "at least 1 minute"}, + {"upper-case extension on a daily cron", geodata("0 4 * * *", "Custom.DAT"), ""}, + {"no cron leaves the assets unscheduled", geodata("", "custom.mmdb"), ""}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := (&XraySettingService{}).CheckXrayConfig(tc.template) + if tc.want == "" { + if err != nil { + t.Fatalf("CheckXrayConfig = %v, want the geodata the core accepts kept", err) + } + return + } + if err == nil || !strings.Contains(err.Error(), tc.want) { + t.Fatalf("CheckXrayConfig = %v, want one containing %q", err, tc.want) + } + }) + } +} + // A template saved under 26.10.10 can carry those scripts; the build drops them so the core still // starts, and the vendored loader accepts the routing and dns sections it then emits. func TestGetXrayConfig_DropsLuaScriptsTheCoreRefuses(t *testing.T) { diff --git a/internal/web/translation/ar-EG.json b/internal/web/translation/ar-EG.json index 58d2c5e4c..4aee49632 100644 --- a/internal/web/translation/ar-EG.json +++ b/internal/web/translation/ar-EG.json @@ -238,7 +238,7 @@ "geodataConfirmTitle": "حفظ إعدادات geodata؟", "geodataConfirmContent": "سيتم تحديث قالب إعدادات Xray وإعادة تشغيل Xray.", "geodataInvalidUrl": "كل ملف يحتاج إلى رابط HTTPS.", - "geodataInvalidFile": "يجب أن يكون اسم الملف اسماً بسيطاً مثل geosite_custom.dat (بدون مسارات).", + "geodataInvalidFile": "يجب أن يكون اسم الملف اسماً بسيطاً بامتداد ‎.dat مثل geosite_custom.dat (بدون مسارات).", "geodataInvalidCron": "يجب أن يتكون cron من 5 حقول، مثل 0 4 * * *", "geodataEmpty": "لا توجد ملفات مهيأة. في قواعد التوجيه يُشار إلى الملفات بالشكل ext:geosite_custom.dat:category.", "dontRefresh": "التثبيت شغال، متعملش Refresh للصفحة", diff --git a/internal/web/translation/en-US.json b/internal/web/translation/en-US.json index 38a3c675e..28320955c 100644 --- a/internal/web/translation/en-US.json +++ b/internal/web/translation/en-US.json @@ -238,7 +238,7 @@ "geodataConfirmTitle": "Save geodata settings?", "geodataConfirmContent": "This updates the Xray config template and restarts Xray.", "geodataInvalidUrl": "Each file needs an HTTPS URL.", - "geodataInvalidFile": "File names must be plain names like geosite_custom.dat (no paths).", + "geodataInvalidFile": "File names must be plain .dat names like geosite_custom.dat (no paths).", "geodataInvalidCron": "Cron must have 5 fields, e.g. 0 4 * * *", "geodataEmpty": "No files configured. Reference files in routing rules as ext:geosite_custom.dat:category.", "dontRefresh": "Installation is in progress, please do not refresh this page", diff --git a/internal/web/translation/es-ES.json b/internal/web/translation/es-ES.json index fbcc76191..f9cbf2208 100644 --- a/internal/web/translation/es-ES.json +++ b/internal/web/translation/es-ES.json @@ -238,7 +238,7 @@ "geodataConfirmTitle": "¿Guardar la configuración de geodata?", "geodataConfirmContent": "Se actualizará la plantilla de configuración de Xray y se reiniciará Xray.", "geodataInvalidUrl": "Cada archivo necesita una URL HTTPS.", - "geodataInvalidFile": "El nombre de archivo debe ser simple, p. ej. geosite_custom.dat (sin rutas).", + "geodataInvalidFile": "El nombre de archivo debe ser un nombre .dat simple, p. ej. geosite_custom.dat (sin rutas).", "geodataInvalidCron": "Cron debe tener 5 campos, p. ej. 0 4 * * *", "geodataEmpty": "No hay archivos configurados. En las reglas de enrutamiento se referencian como ext:geosite_custom.dat:category.", "dontRefresh": "La instalación está en progreso, por favor no actualices esta página.", diff --git a/internal/web/translation/fa-IR.json b/internal/web/translation/fa-IR.json index 6e790b04a..b07d71b8b 100644 --- a/internal/web/translation/fa-IR.json +++ b/internal/web/translation/fa-IR.json @@ -238,7 +238,7 @@ "geodataConfirmTitle": "تنظیمات geodata ذخیره شود؟", "geodataConfirmContent": "قالب پیکربندی Xray به‌روزرسانی و Xray ری‌استارت می‌شود.", "geodataInvalidUrl": "هر فایل به یک آدرس HTTPS نیاز دارد.", - "geodataInvalidFile": "نام فایل باید ساده باشد، مانند geosite_custom.dat (بدون مسیر).", + "geodataInvalidFile": "نام فایل باید یک نام ساده با پسوند ‎.dat باشد، مانند geosite_custom.dat (بدون مسیر).", "geodataInvalidCron": "Cron باید ۵ بخش داشته باشد، مثل 0 4 * * *", "geodataEmpty": "فایلی پیکربندی نشده است. در قوانین مسیریابی فایل‌ها به صورت ext:geosite_custom.dat:category استفاده می‌شوند.", "dontRefresh": "در حال نصب، لطفا صفحه را رفرش نکنید", diff --git a/internal/web/translation/id-ID.json b/internal/web/translation/id-ID.json index bc95dc0a7..2a2822290 100644 --- a/internal/web/translation/id-ID.json +++ b/internal/web/translation/id-ID.json @@ -238,7 +238,7 @@ "geodataConfirmTitle": "Simpan pengaturan geodata?", "geodataConfirmContent": "Templat konfigurasi Xray akan diperbarui dan Xray akan dimulai ulang.", "geodataInvalidUrl": "Setiap berkas memerlukan URL HTTPS.", - "geodataInvalidFile": "Nama berkas harus berupa nama sederhana, mis. geosite_custom.dat (tanpa path).", + "geodataInvalidFile": "Nama berkas harus berupa nama .dat sederhana, mis. geosite_custom.dat (tanpa path).", "geodataInvalidCron": "Cron harus terdiri dari 5 bagian, mis. 0 4 * * *", "geodataEmpty": "Belum ada berkas yang dikonfigurasi. Pada aturan routing, rujuk berkas sebagai ext:geosite_custom.dat:category.", "dontRefresh": "Instalasi sedang berlangsung, harap jangan menyegarkan halaman ini", diff --git a/internal/web/translation/ja-JP.json b/internal/web/translation/ja-JP.json index 7a7c8021d..12cd2af39 100644 --- a/internal/web/translation/ja-JP.json +++ b/internal/web/translation/ja-JP.json @@ -238,7 +238,7 @@ "geodataConfirmTitle": "geodata 設定を保存しますか?", "geodataConfirmContent": "Xray 設定テンプレートを更新し、Xray を再起動します。", "geodataInvalidUrl": "各ファイルには HTTPS URL が必要です。", - "geodataInvalidFile": "ファイル名はパスを含まない単純な名前にしてください(例: geosite_custom.dat)。", + "geodataInvalidFile": "ファイル名はパスを含まない単純な .dat 名にしてください(例: geosite_custom.dat)。", "geodataInvalidCron": "Cron は 5 フィールド必要です(例: 0 4 * * *)", "geodataEmpty": "ファイルが設定されていません。ルーティングルールでは ext:geosite_custom.dat:category の形式で参照します。", "dontRefresh": "インストール中、このページをリロードしないでください", diff --git a/internal/web/translation/pt-BR.json b/internal/web/translation/pt-BR.json index dba34991e..c24280532 100644 --- a/internal/web/translation/pt-BR.json +++ b/internal/web/translation/pt-BR.json @@ -238,7 +238,7 @@ "geodataConfirmTitle": "Salvar configurações de geodata?", "geodataConfirmContent": "O modelo de configuração do Xray será atualizado e o Xray será reiniciado.", "geodataInvalidUrl": "Cada arquivo precisa de uma URL HTTPS.", - "geodataInvalidFile": "O nome do arquivo deve ser simples, ex.: geosite_custom.dat (sem caminhos).", + "geodataInvalidFile": "O nome do arquivo deve ser um nome .dat simples, ex.: geosite_custom.dat (sem caminhos).", "geodataInvalidCron": "O cron deve ter 5 campos, ex.: 0 4 * * *", "geodataEmpty": "Nenhum arquivo configurado. Nas regras de roteamento, referencie como ext:geosite_custom.dat:category.", "dontRefresh": "Instalação em andamento, por favor não atualize a página", diff --git a/internal/web/translation/ru-RU.json b/internal/web/translation/ru-RU.json index 8ab6ca7f4..6b55abdb0 100644 --- a/internal/web/translation/ru-RU.json +++ b/internal/web/translation/ru-RU.json @@ -238,7 +238,7 @@ "geodataConfirmTitle": "Сохранить настройки geodata?", "geodataConfirmContent": "Шаблон конфигурации Xray будет обновлён, а Xray перезапущен.", "geodataInvalidUrl": "Для каждого файла нужен HTTPS URL.", - "geodataInvalidFile": "Имя файла должно быть простым, например geosite_custom.dat (без путей).", + "geodataInvalidFile": "Имя файла должно быть простым именем .dat, например geosite_custom.dat (без путей).", "geodataInvalidCron": "Cron должен содержать 5 полей, напр. 0 4 * * *", "geodataEmpty": "Файлы не настроены. В правилах маршрутизации файлы указываются как ext:geosite_custom.dat:category.", "dontRefresh": "Установка в процессе. Не обновляйте страницу", diff --git a/internal/web/translation/sk-SK.json b/internal/web/translation/sk-SK.json index 4d7a7cd73..3e11a3dc5 100644 --- a/internal/web/translation/sk-SK.json +++ b/internal/web/translation/sk-SK.json @@ -238,7 +238,7 @@ "geodataConfirmTitle": "Uložiť nastavenia geodát?", "geodataConfirmContent": "Zmení sa šablóna konfigurácie Xray a Xray sa reštartuje.", "geodataInvalidUrl": "Pre každý súbor zadajte odkaz HTTPS.", - "geodataInvalidFile": "Použite samotný názov súboru, napr. geosite_custom.dat, bez cesty.", + "geodataInvalidFile": "Použite samotný názov súboru .dat, napr. geosite_custom.dat, bez cesty.", "geodataInvalidCron": "Výraz cron musí mať 5 polí, napr. 0 4 * * *", "geodataEmpty": "Nie sú nastavené žiadne súbory. V pravidlách smerovania na ne odkazujte ako ext:geosite_custom.dat:category.", "dontRefresh": "Prebieha inštalácia. Neobnovujte stránku.", diff --git a/internal/web/translation/tr-TR.json b/internal/web/translation/tr-TR.json index 024822587..1b23d182b 100644 --- a/internal/web/translation/tr-TR.json +++ b/internal/web/translation/tr-TR.json @@ -238,7 +238,7 @@ "geodataConfirmTitle": "Geodata ayarları kaydedilsin mi?", "geodataConfirmContent": "Xray yapılandırma şablonu güncellenecek ve Xray yeniden başlatılacak.", "geodataInvalidUrl": "Her dosya için bir HTTPS URL gerekir.", - "geodataInvalidFile": "Dosya adı yalnızca dosya adı olmalıdır, örn. geosite_custom.dat (yol içermemeli).", + "geodataInvalidFile": "Dosya adı yalnızca .dat uzantılı bir dosya adı olmalıdır, örn. geosite_custom.dat (yol içermemeli).", "geodataInvalidCron": "Cron 5 alandan oluşmalıdır, örn. 0 4 * * *", "geodataEmpty": "Yapılandırılmış dosya yok. Yönlendirme kurallarında dosyalar ext:geosite_custom.dat:category olarak kullanılır.", "dontRefresh": "Kurulum devam ediyor, lütfen bu sayfayı yenilemeyin", diff --git a/internal/web/translation/uk-UA.json b/internal/web/translation/uk-UA.json index f21d413cd..4d69d7ae0 100644 --- a/internal/web/translation/uk-UA.json +++ b/internal/web/translation/uk-UA.json @@ -238,7 +238,7 @@ "geodataConfirmTitle": "Зберегти налаштування geodata?", "geodataConfirmContent": "Шаблон конфігурації Xray буде оновлено, а Xray перезапущено.", "geodataInvalidUrl": "Для кожного файлу потрібен HTTPS URL.", - "geodataInvalidFile": "Ім’я файлу має бути простим, напр. geosite_custom.dat (без шляхів).", + "geodataInvalidFile": "Ім’я файлу має бути простим ім’ям .dat, напр. geosite_custom.dat (без шляхів).", "geodataInvalidCron": "Cron має містити 5 полів, напр. 0 4 * * *", "geodataEmpty": "Файли не налаштовано. У правилах маршрутизації файли вказуються як ext:geosite_custom.dat:category.", "dontRefresh": "Інсталяція триває, будь ласка, не оновлюйте цю сторінку", diff --git a/internal/web/translation/vi-VN.json b/internal/web/translation/vi-VN.json index 57450713c..38f8c92ba 100644 --- a/internal/web/translation/vi-VN.json +++ b/internal/web/translation/vi-VN.json @@ -238,7 +238,7 @@ "geodataConfirmTitle": "Lưu cài đặt geodata?", "geodataConfirmContent": "Mẫu cấu hình Xray sẽ được cập nhật và Xray sẽ khởi động lại.", "geodataInvalidUrl": "Mỗi tệp cần một URL HTTPS.", - "geodataInvalidFile": "Tên tệp phải là tên đơn giản, ví dụ geosite_custom.dat (không chứa đường dẫn).", + "geodataInvalidFile": "Tên tệp phải là tên .dat đơn giản, ví dụ geosite_custom.dat (không chứa đường dẫn).", "geodataInvalidCron": "Cron phải có 5 trường, ví dụ 0 4 * * *", "geodataEmpty": "Chưa cấu hình tệp nào. Trong quy tắc định tuyến, tham chiếu tệp dạng ext:geosite_custom.dat:category.", "dontRefresh": "Đang tiến hành cài đặt, vui lòng không làm mới trang này.", diff --git a/internal/web/translation/zh-CN.json b/internal/web/translation/zh-CN.json index 0ef602822..66b40ca8f 100644 --- a/internal/web/translation/zh-CN.json +++ b/internal/web/translation/zh-CN.json @@ -238,7 +238,7 @@ "geodataConfirmTitle": "保存 geodata 设置?", "geodataConfirmContent": "将更新 Xray 配置模板并重启 Xray。", "geodataInvalidUrl": "每个文件都需要 HTTPS 地址。", - "geodataInvalidFile": "文件名必须是纯文件名,例如 geosite_custom.dat(不能包含路径)。", + "geodataInvalidFile": "文件名必须是纯 .dat 文件名,例如 geosite_custom.dat(不能包含路径)。", "geodataInvalidCron": "Cron 必须为 5 段,例如 0 4 * * *", "geodataEmpty": "尚未配置文件。路由规则中可通过 ext:geosite_custom.dat:category 引用文件。", "dontRefresh": "安装中,请勿刷新此页面", diff --git a/internal/web/translation/zh-TW.json b/internal/web/translation/zh-TW.json index e77b37ab0..348c0856a 100644 --- a/internal/web/translation/zh-TW.json +++ b/internal/web/translation/zh-TW.json @@ -238,7 +238,7 @@ "geodataConfirmTitle": "儲存 geodata 設定?", "geodataConfirmContent": "將更新 Xray 設定範本並重啟 Xray。", "geodataInvalidUrl": "每個檔案都需要 HTTPS 位址。", - "geodataInvalidFile": "檔案名稱必須是純檔名,例如 geosite_custom.dat(不能包含路徑)。", + "geodataInvalidFile": "檔案名稱必須是純 .dat 檔名,例如 geosite_custom.dat(不能包含路徑)。", "geodataInvalidCron": "Cron 必須為 5 段,例如 0 4 * * *", "geodataEmpty": "尚未設定檔案。路由規則中可透過 ext:geosite_custom.dat:category 引用檔案。", "dontRefresh": "安裝中,請勿重新整理此頁面",