Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/features/15141-core-config-beside.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **feat(proxy-subscription):** generate the local proxy-core config beside the adopted file, opt-in per subscription ([#15141](https://github.com/diegosouzapw/OmniRoute/pull/15141)) — thanks @maxmad64bis
18 changes: 17 additions & 1 deletion docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3323,6 +3323,13 @@ paths:
localCoreEndpoint:
type: string
nullable: true
coreConfigPath:
type: string
nullable: true
description: >-
Absolute adopted sing-box config path (lowercase .json).
Empty means no generation; each sync writes
`<path>.generated` beside it, never into it.
updateIntervalMinutes:
type: integer
default: 60
Expand Down Expand Up @@ -3369,7 +3376,7 @@ paths:
summary: Update a proxy subscription
description: >-
Partial update — only fields present in the body are changed
(name/url/mode/ruleProviders/localCoreEndpoint/updateIntervalMinutes/enabled).
(name/url/mode/ruleProviders/localCoreEndpoint/coreConfigPath/updateIntervalMinutes/enabled).
security:
- ManagementSessionAuth: []
parameters:
Expand All @@ -3395,6 +3402,9 @@ paths:
localCoreEndpoint:
type: string
nullable: true
coreConfigPath:
type: string
nullable: true
updateIntervalMinutes:
type: integer
enabled:
Expand Down Expand Up @@ -15033,6 +15043,12 @@ components:
localCoreEndpoint:
type: string
nullable: true
coreConfigPath:
type: string
nullable: true
description: >-
Absolute adopted sing-box config path (lowercase .json).
Empty means no generation.
updateIntervalMinutes:
type: integer
lastFetchedAt:
Expand Down
34 changes: 34 additions & 0 deletions docs/routing/PROXY_CORE_CONFIG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
title: "Local Proxy-Core Configuration"
---

# Local proxy-core configuration (opt-in)

This subscription field generates a local-core config file. Empty (default)
means no generation: sync behaves exactly as before.

- Set **Core config file** to the absolute path of the adopted sing-box
config (lowercase `.json` extension, e.g.
`/var/lib/omniroute/sing-box/config.json`).
- Each sync renders the model and writes `<path>.generated` **beside** the
adopted file with an atomic, symlink-refusing write (mode 0600). The
adopted file itself is never modified here; replacing it is a separate,
verified step.
- Only sections whose tags start with `omniroute-` are owned: owned entries
are removed then replaced, everything else (`dns`, `log`,
`experimental/clash_api`, user rules and outbounds, unknown keys) is kept
verbatim. Only sing-box is rendered so far.
- Refused without writing: unparseable JSON, a non-object root, or a file
with neither an `inbounds` nor an `outbounds` array. Nodes whose source
isn't sing-box-shaped are skipped with a reason, never converted.
- Failures and refusals surface as a sync warning and never fail the sync.
When an earlier warning already owns the sync status, generation is
skipped and the skip is logged server-side. Removing the subscription or
changing its path leaves any previously written `.generated` file in
place; adopting the generated file is part of the verified replacement
step.
- The last switch line shows the refusal motive that fired it; live switch
activity is also visible in the application log under [SelectorControl].
- Without any selector group the model stays empty: nodes are neither
rendered nor listed. The switch reason shows as a generic label (quota,
unreachable, transport, slow).
2 changes: 1 addition & 1 deletion skills/omni-inference/references/endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ curl https://localhost:20128/api/v1/management/proxy-subscriptions/{id} \

Update a proxy subscription

Partial update — only fields present in the body are changed (name/url/mode/ruleProviders/localCoreEndpoint/updateIntervalMinutes/enabled).
Partial update — only fields present in the body are changed (name/url/mode/ruleProviders/localCoreEndpoint/coreConfigPath/updateIntervalMinutes/enabled).

```bash
curl -X PATCH https://localhost:20128/api/v1/management/proxy-subscriptions/{id} \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ interface SubscriptionRecord {
localCoreEndpoint: string | null;
updateIntervalMinutes: number;
controlUrl: string | null;
coreConfigPath: string | null;
hasControlSecret: boolean;
selectorMinGapSeconds: number;
selectorLastSwitchAt: string | null;
selectorLastSwitchResult: string | null;
selectorLastSwitchMember: string | null;
selectorLastSwitchKind: string | null;
lastFetchedAt: string | null;
status: "ok" | "error" | "empty";
error: string | null;
Expand All @@ -41,6 +43,7 @@ type FormState = {
ruleProviders: string[];
localCoreEndpoint: string;
controlUrl: string;
coreConfigPath: string;
controlSecret: string;
selectorMinGapSeconds: number;
updateIntervalMinutes: number;
Expand All @@ -54,6 +57,7 @@ const EMPTY_FORM: FormState = {
ruleProviders: [],
localCoreEndpoint: "",
controlUrl: "",
coreConfigPath: "",
controlSecret: "",
selectorMinGapSeconds: 60,
updateIntervalMinutes: 60,
Expand Down Expand Up @@ -99,6 +103,22 @@ export default function SubscriptionTab() {

const t = useTranslations("settings");

// Map a stored refusal kind to a generic display label. Unknown future
// kinds fall back to plain English so the UI never shows a raw code.
const switchKindLabel = useCallback(
(kind: string | null): string | null => {
if (!kind) return null;
try {
const label = t(`proxySubscription.switchKind.${kind}`);
if (label && !label.includes("switchKind.")) return label;
} catch {
// fall through to the generic fallback below
}
return kind.replace(/_/g, " ");
},
[t]
);

// Resolve a subscription `error` value into a localized message. Values are
// either a `{ code, detail? }` JSON (user-facing, i18n'd) or a plain
// diagnostic string (technical fetch/sync errors) shown verbatim.
Expand Down Expand Up @@ -183,6 +203,7 @@ export default function SubscriptionTab() {
ruleProviders: sub.ruleProviders ?? [],
localCoreEndpoint: sub.localCoreEndpoint ?? "",
controlUrl: sub.controlUrl ?? "",
coreConfigPath: sub.coreConfigPath ?? "",
controlSecret: "",
selectorMinGapSeconds: sub.selectorMinGapSeconds ?? 60,
updateIntervalMinutes: sub.updateIntervalMinutes,
Expand All @@ -207,6 +228,7 @@ export default function SubscriptionTab() {
ruleProviders: form.mode === "rule" ? form.ruleProviders : null,
localCoreEndpoint: form.localCoreEndpoint.trim() || null,
controlUrl: form.controlUrl.trim() || null,
coreConfigPath: form.coreConfigPath.trim() || null,
selectorMinGapSeconds: Number(form.selectorMinGapSeconds) || 60,
updateIntervalMinutes: Number(form.updateIntervalMinutes) || 60,
enabled: form.enabled,
Expand Down Expand Up @@ -298,11 +320,13 @@ export default function SubscriptionTab() {
const member = sub.selectorLastSwitchMember
? ` · ${t("proxySubscription.lastSwitchMember")}: ${sub.selectorLastSwitchMember}`
: "";
const kindLabel = switchKindLabel(sub.selectorLastSwitchKind);
const kind = kindLabel ? ` · ${t("proxySubscription.lastSwitchKind")}: ${kindLabel}` : "";
const next = nextSwitchLabel(sub);
const nextText = next === "now" ? t("proxySubscription.nextSwitchNow") : next;
return `${t("proxySubscription.lastSwitch")}: ${sub.selectorLastSwitchAt}${result}${member} · ${t("proxySubscription.nextSwitch")}: ${nextText}`;
return `${t("proxySubscription.lastSwitch")}: ${sub.selectorLastSwitchAt}${result}${member}${kind} · ${t("proxySubscription.nextSwitch")}: ${nextText}`;
},
[t]
[t, switchKindLabel]
);

const statusBadge: Record<SubscriptionRecord["status"], string> = {
Expand Down Expand Up @@ -472,6 +496,20 @@ export default function SubscriptionTab() {
: t("proxySubscription.controlSecretNotSet")}
</span>
</label>

<label className="flex flex-col gap-1 text-sm">
<span className="text-text-muted">{t("proxySubscription.coreConfigPath")}</span>
<input
className="rounded border border-border bg-surface px-2 py-1.5 text-text outline-none focus:border-primary"
value={form.coreConfigPath}
onChange={(e) => setForm({ ...form, coreConfigPath: e.target.value })}
placeholder={t("proxySubscription.coreConfigPathPlaceholder")}
inputMode="url"
/>
<span className="text-xs text-text-muted">
{t("proxySubscription.coreConfigPathDesc")}
</span>
</label>
</div>

<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
Expand Down
2 changes: 1 addition & 1 deletion src/app/docs/lib/openapi.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +603,7 @@ export const OPENAPI_ENDPOINTS: OpenApiEndpoint[] = [
path: "/api/v1/management/proxy-subscriptions/{id}",
method: "PATCH",
summary: "Update a proxy subscription",
description: "Partial update — only fields present in the body are changed (name/url/mode/ruleProviders/localCoreEndpoint/updateIntervalMinutes/enabled).",
description: "Partial update — only fields present in the body are changed (name/url/mode/ruleProviders/localCoreEndpoint/coreConfigPath/updateIntervalMinutes/enabled).",
tag: "Proxy Subscriptions",
tags: ["Proxy Subscriptions"],
requiresAuth: true,
Expand Down
15 changes: 13 additions & 2 deletions src/i18n/messages/am.json
Original file line number Diff line number Diff line change
Expand Up @@ -6873,7 +6873,8 @@
"LOCAL_CORE_ENDPOINT_INVALID": "የአካባቢው proxy-core መዳረሻ ነጥብ ልክ አይደለም፤ የSS/VMess/Trojan/VLESS ኖዶች ችላ ተብለዋል።",
"NEEDS_CORE_NOT_CONFIGURED": "ይህ ምዝገባ የአካባቢ proxy core (SS/VMess/Trojan/VLESS) የሚያስፈልጋቸው ኖዶች አሉት፤ የlocal-core SOCKS5 መዳረሻ ነጥቡን እስኪያዋቅሩ ድረስ አይዘዋወሩም።",
"NO_USABLE_NODES": "ምዝገባው ጥቅም ላይ የሚውሉ ኖዶችን አላመጣም (http/https/socks5 ወይም የአካባቢ core መዳረሻ ነጥብ ያላቸው ኖዶች)።",
"SELECTOR_SWITCH_FAILED": "የፕሮክሲ-ኮር መቆጣጠሪያ ጥሪ አልተሳካም; የቀደመው መስመር ንቁ ሆኖ ይቆያል። የመቆጣጠሪያ URL እና ሚስጥር ያረጋግጡ፣ ከዚያ ያድሱ።"
"SELECTOR_SWITCH_FAILED": "የፕሮክሲ-ኮር መቆጣጠሪያ ጥሪ አልተሳካም; የቀደመው መስመር ንቁ ሆኖ ይቆያል። የመቆጣጠሪያ URL እና ሚስጥር ያረጋግጡ፣ ከዚያ ያድሱ።",
"CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh."
},
"description": "የፕሮክሲ ምዝገባ አገናኝዎን ይለጥፉ። አንዴ ከነቃ፣ ትራፊክ በዓለም አቀፍ ወይም በደንብ (የተወሰነ Provider) ሁነታ በፕሮክሲ ስብስቡ በኩል ይዘዋወራል። የምዝገባ ኖዶች በራስ-ሰር ወደ ፕሮክሲ ስብስቡ ይመሳሰላሉ፣ እንዲሁም ነባሮቹን የየጊዜው ማጣሪያ፣ የጤና ፍተሻ እና ፍሳሽ መከላከያ ዘዴዎች እንደገና ይጠቀማሉ።",
"addSubscription": "ምዝገባ ያክሉ",
Expand Down Expand Up @@ -6959,7 +6960,17 @@
"lastSwitchNever": "ገና ምንም መቀያየር አልተመዘገበም",
"lastSwitchMember": "የሚያገለግል አባል",
"nextSwitch": "ቀጣይ የሚቻል መቀያየር",
"nextSwitchNow": "አሁን"
"nextSwitchNow": "አሁን",
"coreConfigPath": "Core config file",
"coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json",
"coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes '<path>'.generated beside the adopted file, never into it.",
"lastSwitchKind": "Switch reason",
"switchKind": {
"ip_quota_429": "quota",
"proxy_unreachable": "unreachable",
"transport": "transport",
"slow": "slow"
}
},
"bulkHealthcheck": "የጅምላ ጤና ምርመራ",
"bulkHealthcheckDesc": "የትኞቹ እንደሚሰሩ ለማወቅ ሁሉንም የተዋቀሩ ፕሮክሲዎች በዒላማ URL ላይ ይሞክሩ።",
Expand Down
15 changes: 13 additions & 2 deletions src/i18n/messages/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -6873,7 +6873,8 @@
"LOCAL_CORE_ENDPOINT_INVALID": "نقطة نهاية نواة الوكيل المحلي غير صالحة؛ تم تجاهل عقد SS/VMess/Trojan/VLESS.",
"NEEDS_CORE_NOT_CONFIGURED": "يحتوي هذا الاشتراك على عقد تحتاج إلى نواة وكيل محلي (SS/VMess/Trojan/VLESS)؛ لن يتم توجيهها حتى تقوم بتكوين نقطة نهاية SOCKS5 للنواة المحلية.",
"NO_USABLE_NODES": "لم ينتج عن الاشتراك أي عقد صالحة للاستخدام (http/https/socks5 أو عقد تحتوي على نقطة نهاية للنواة المحلية).",
"SELECTOR_SWITCH_FAILED": "فشل استدعاء التحكم في نواة الوكيل؛ ويظل المسار السابق نشطًا. تحقق من عنوان URL والسر الخاص بالتحكم، ثم حدّث."
"SELECTOR_SWITCH_FAILED": "فشل استدعاء التحكم في نواة الوكيل؛ ويظل المسار السابق نشطًا. تحقق من عنوان URL والسر الخاص بالتحكم، ثم حدّث.",
"CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh."
},
"description": "الصق رابط اشتراك الوكيل الخاص بك. بمجرد التمكين، يتم توجيه حركة المرور عبر تجمع الوكيل في الوضع العام أو وضع القاعدة (الموفر المحدد). تتم مزامنة عقد الاشتراك تلقائيًا في تجمع الوكيل وإعادة استخدام آليات الاستقصاء والفحص الصحي ومكافحة التسرب الموجودة.",
"addSubscription": "إضافة الاشتراك",
Expand Down Expand Up @@ -6959,7 +6960,17 @@
"lastSwitchNever": "لم يُسجَّل أي تبديل بعد",
"lastSwitchMember": "العضو الذي يخدم",
"nextSwitch": "التبديل التالي الممكن",
"nextSwitchNow": "الآن"
"nextSwitchNow": "الآن",
"coreConfigPath": "Core config file",
"coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json",
"coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes '<path>'.generated beside the adopted file, never into it.",
"lastSwitchKind": "Switch reason",
"switchKind": {
"ip_quota_429": "quota",
"proxy_unreachable": "unreachable",
"transport": "transport",
"slow": "slow"
}
},
"bulkHealthcheck": "الفحص الصحي بالجملة",
"bulkHealthcheckDesc": "اختبر جميع الوكلاء الذين تم تكوينهم مقابل عنوان URL المستهدف للعثور على الوكلاء الذين يعملون.",
Expand Down
15 changes: 13 additions & 2 deletions src/i18n/messages/az.json
Original file line number Diff line number Diff line change
Expand Up @@ -6873,7 +6873,8 @@
"LOCAL_CORE_ENDPOINT_INVALID": "Yerli proksi-nüvə son nöqtəsi keçərsizdir; SS/VMess/Trojan/VLESS qovşaqlarına məhəl qoyulmadı.",
"NEEDS_CORE_NOT_CONFIGURED": "Bu abunəlikdə yerli proksi-nüvəyə (SS/VMess/Trojan/VLESS) ehtiyacı olan qovşaqlar var; yerli nüvənin SOCKS5 son nöqtəsini konfiqurasiya edənə qədər onlar yönləndirilməyəcək.",
"NO_USABLE_NODES": "Abunəlik heç bir istifadəyə yararlı qovşaq vermədi (http/https/socks5 və ya yerli nüvə son nöqtəsi olan qovşaqlar).",
"SELECTOR_SWITCH_FAILED": "Proxy-core idarəetmə çağırışı uğursuz oldu; əvvəlki marşrut aktiv qalır. İdarəetmə URL-ni və sirri yoxlayıb yeniləyin."
"SELECTOR_SWITCH_FAILED": "Proxy-core idarəetmə çağırışı uğursuz oldu; əvvəlki marşrut aktiv qalır. İdarəetmə URL-ni və sirri yoxlayıb yeniləyin.",
"CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh."
},
"description": "Proksi abunə linkinizi yapışdırın. Aktivləşdirildikdən sonra trafik qlobal və ya qayda (müəyyən edilmiş Provayder) rejimində proksi hovuzu vasitəsilə yönləndirilir. Abunə qovşaqları avtomatik olaraq proksi hovuzuna sinxronlaşdırılır və mövcud sorğu, sağlamlıq yoxlanışı və sızma əleyhinə mexanizmlərdən yenidən istifadə edir.",
"addSubscription": "Abunəlik əlavə edin",
Expand Down Expand Up @@ -6959,7 +6960,17 @@
"lastSwitchNever": "Hələ qeydə alınmış keçid yoxdur",
"lastSwitchMember": "Xidmət edən üzv",
"nextSwitch": "Növbəti mümkün keçid",
"nextSwitchNow": "İndi"
"nextSwitchNow": "İndi",
"coreConfigPath": "Core config file",
"coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json",
"coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes '<path>'.generated beside the adopted file, never into it.",
"lastSwitchKind": "Switch reason",
"switchKind": {
"ip_quota_429": "quota",
"proxy_unreachable": "unreachable",
"transport": "transport",
"slow": "slow"
}
},
"bulkHealthcheck": "Toplu sağlamlıq yoxlaması",
"bulkHealthcheckDesc": "Hansılarının işlədiyini müəyyən etmək üçün bütün konfiqurasiya edilmiş proksiləri hədəf URL-də sınaqdan keçirin.",
Expand Down
Loading
Loading