mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 03:05:02 +03:00
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.
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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(<GeodataSection active onBusy={vi.fn()} onClose={vi.fn()} />);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 للصفحة",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "در حال نصب، لطفا صفحه را رفرش نکنید",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "インストール中、このページをリロードしないでください",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Установка в процессе. Не обновляйте страницу",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Інсталяція триває, будь ласка, не оновлюйте цю сторінку",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "安装中,请勿刷新此页面",
|
||||
|
||||
@@ -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": "安裝中,請勿重新整理此頁面",
|
||||
|
||||
Reference in New Issue
Block a user