Harden outbound tag allocation
This commit is contained in:
+331
-324
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PODKOP PLUS\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-10 05:05+0300\n"
|
||||
"PO-Revision-Date: 2026-07-10 05:05+0300\n"
|
||||
"POT-Creation-Date: 2026-07-10 15:25+0300\n"
|
||||
"PO-Revision-Date: 2026-07-10 15:25+0300\n"
|
||||
"Last-Translator: ushan0v\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
@@ -488,6 +488,9 @@ msgstr "Загружено"
|
||||
msgid "Drag rows to change priority. The rule at the top is checked first."
|
||||
msgstr "Перетаскивайте строки, чтобы изменить приоритет. Правило сверху проверяется первым."
|
||||
|
||||
msgid "Duplicate JSON outbound tag"
|
||||
msgstr "Такой тег JSON outbound уже используется"
|
||||
|
||||
msgid "Duplicate value"
|
||||
msgstr "Дублирующееся значение"
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export { bulkValidate } from './validators/bulkValidate';
|
||||
export { validateOutboundJson } from './validators/validateOutboundJson';
|
||||
export { validateProxyUrl } from './validators/validateProxyUrl';
|
||||
export { parseValueList } from './helpers/parseValueList';
|
||||
export { getProxyUrlName } from './helpers/getProxyUrlName';
|
||||
export { injectGlobalStyles } from './helpers/injectGlobalStyles';
|
||||
export { showToast } from './helpers/showToast';
|
||||
export { getClashUIUrl } from './helpers/getClashApiUrl';
|
||||
|
||||
@@ -832,10 +832,13 @@ function getOutboundDisplayName(
|
||||
entry: ClashProxyEntry | undefined,
|
||||
link: string,
|
||||
outboundMetadata?: Podkop.GetOutboundMetadata,
|
||||
preferMetadata = false,
|
||||
) {
|
||||
const metadataName = outboundMetadata?.names?.[code];
|
||||
|
||||
return (
|
||||
getProxyUrlName(link) ||
|
||||
outboundMetadata?.names?.[code] ||
|
||||
(preferMetadata ? metadataName : getProxyUrlName(link)) ||
|
||||
(preferMetadata ? getProxyUrlName(link) : metadataName) ||
|
||||
entry?.value?.name ||
|
||||
code
|
||||
);
|
||||
@@ -888,6 +891,7 @@ function buildUrlTestInfo({
|
||||
childEntry,
|
||||
link,
|
||||
outboundMetadata,
|
||||
subscriptionCopyableCodes.has(childCode),
|
||||
),
|
||||
latency: childEntry?.value?.history?.[0]?.delay || 0,
|
||||
type: childEntry?.value?.type || '',
|
||||
@@ -928,6 +932,7 @@ function buildPriorityInfo({
|
||||
cachedProxyLinks,
|
||||
outboundMetadata,
|
||||
subscriptionCopyableCodes,
|
||||
showDetectedCountries,
|
||||
}: {
|
||||
config: PriorityConfig;
|
||||
entry?: ClashProxyEntry;
|
||||
@@ -937,6 +942,7 @@ function buildPriorityInfo({
|
||||
cachedProxyLinks: Map<string, string>;
|
||||
outboundMetadata?: Podkop.GetOutboundMetadata;
|
||||
subscriptionCopyableCodes: Set<string>;
|
||||
showDetectedCountries: boolean;
|
||||
}): Podkop.PriorityInfo {
|
||||
const selectedCode = entry?.value.now || '';
|
||||
const cacheLevels = Array.isArray(groupCache?.levels)
|
||||
@@ -1001,12 +1007,16 @@ function buildPriorityInfo({
|
||||
childEntry,
|
||||
link,
|
||||
outboundMetadata,
|
||||
subscriptionCopyableCodes.has(childCode),
|
||||
),
|
||||
latency: childEntry?.value?.history?.[0]?.delay || 0,
|
||||
type: childEntry?.value?.type || '',
|
||||
selected: selectedCode === childCode,
|
||||
link,
|
||||
canCopyLink,
|
||||
country: showDetectedCountries
|
||||
? outboundMetadata?.countries?.[childCode]
|
||||
: undefined,
|
||||
levelIndex,
|
||||
levelName: level.displayName,
|
||||
levelId: level.id,
|
||||
@@ -1155,7 +1165,13 @@ function buildProxyGroupOutbounds(
|
||||
const displayName =
|
||||
priorityConfig?.displayName ||
|
||||
urlTestConfig?.displayName ||
|
||||
getOutboundDisplayName(code, item, link, outboundMetadata);
|
||||
getOutboundDisplayName(
|
||||
code,
|
||||
item,
|
||||
link,
|
||||
outboundMetadata,
|
||||
subscriptionCopyableCodes.has(code),
|
||||
);
|
||||
const isRuntimeUrlTest = isUrlTestProxyEntry(item);
|
||||
|
||||
return [
|
||||
@@ -1197,6 +1213,7 @@ function buildProxyGroupOutbounds(
|
||||
cachedProxyLinks,
|
||||
outboundMetadata,
|
||||
subscriptionCopyableCodes,
|
||||
showDetectedCountries: priorityConfig.showDetectedCountries,
|
||||
})
|
||||
: undefined,
|
||||
},
|
||||
|
||||
@@ -307,7 +307,11 @@ describe('getDashboardSections', () => {
|
||||
mocks.getConfigSections.mockResolvedValue([
|
||||
proxySection({ urltests: [], urltest_settings: undefined }),
|
||||
priorityGroup('pg_main'),
|
||||
priorityLevel('pl_de', { name: 'Germany', order: '0' }),
|
||||
priorityLevel('pl_de', {
|
||||
name: 'Germany',
|
||||
order: '0',
|
||||
detect_server_country: 'country_is',
|
||||
}),
|
||||
priorityLevel('pl_nl', {
|
||||
name: 'Netherlands',
|
||||
order: '1',
|
||||
@@ -322,11 +326,7 @@ describe('getDashboardSections', () => {
|
||||
'main-out': proxy('Selector', {
|
||||
name: 'main-out',
|
||||
now: 'main-priority-pg_main-out',
|
||||
all: [
|
||||
'main-1-out',
|
||||
'main-2-out',
|
||||
'main-priority-pg_main-out',
|
||||
],
|
||||
all: ['main-1-out', 'main-2-out', 'main-priority-pg_main-out'],
|
||||
}),
|
||||
'main-priority-pg_main-out': proxy('Selector', {
|
||||
name: 'main-priority-pg_main-out',
|
||||
@@ -343,7 +343,7 @@ describe('getDashboardSections', () => {
|
||||
'main-1-out': 'First cached',
|
||||
'main-2-out': 'Second cached',
|
||||
},
|
||||
countries: {},
|
||||
countries: { 'main-2-out': 'DE' },
|
||||
},
|
||||
priorityGroups: {
|
||||
'main-priority-pg_main-out': {
|
||||
@@ -403,6 +403,7 @@ describe('getDashboardSections', () => {
|
||||
levelIndex: item.levelIndex,
|
||||
levelName: item.levelName,
|
||||
selected: item.selected,
|
||||
country: item.country,
|
||||
})),
|
||||
).toEqual([
|
||||
{
|
||||
@@ -410,12 +411,14 @@ describe('getDashboardSections', () => {
|
||||
levelIndex: 0,
|
||||
levelName: 'Germany',
|
||||
selected: true,
|
||||
country: 'DE',
|
||||
},
|
||||
{
|
||||
code: 'main-1-out',
|
||||
levelIndex: 1,
|
||||
levelName: 'Netherlands',
|
||||
selected: false,
|
||||
country: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -434,11 +437,7 @@ describe('getDashboardSections', () => {
|
||||
'main-out': proxy('Selector', {
|
||||
name: 'main-out',
|
||||
now: 'main-priority-pg_main-out',
|
||||
all: [
|
||||
'main-1-out',
|
||||
'main-2-out',
|
||||
'main-priority-pg_main-out',
|
||||
],
|
||||
all: ['main-1-out', 'main-2-out', 'main-priority-pg_main-out'],
|
||||
}),
|
||||
'main-priority-pg_main-out': proxy('Selector', {
|
||||
name: 'main-priority-pg_main-out',
|
||||
@@ -982,8 +981,13 @@ describe('getDashboardSections', () => {
|
||||
]);
|
||||
mocks.fsRead.mockResolvedValue(
|
||||
JSON.stringify({
|
||||
outboundMetadata: {
|
||||
names: { 'main-2-out': 'Provider Subscription' },
|
||||
countries: {},
|
||||
},
|
||||
links: {
|
||||
'main-2-out': 'vless://00000000-0000-4000-8000-000000000002@example.com:443#Subscription',
|
||||
'main-2-out':
|
||||
'vless://00000000-0000-4000-8000-000000000002@example.com:443#Subscription',
|
||||
},
|
||||
linkRefs: {
|
||||
'main-2-out': {
|
||||
@@ -1004,6 +1008,7 @@ describe('getDashboardSections', () => {
|
||||
expect(subscriptionOutbound?.link).toBe(
|
||||
'vless://00000000-0000-4000-8000-000000000002@example.com:443#Subscription',
|
||||
);
|
||||
expect(subscriptionOutbound?.displayName).toBe('Provider Subscription');
|
||||
expect(subscriptionOutbound?.canCopyLink).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ const RESERVED_RUNTIME_TAGS = new Set([
|
||||
'dns-in',
|
||||
'service-mixed-in',
|
||||
'direct-out',
|
||||
'bypass-out',
|
||||
]);
|
||||
|
||||
export function allocateRuntimeTag(base: string, postfix: string): string {
|
||||
|
||||
@@ -710,7 +710,18 @@ function getDetectedCountryFlag(country?: string) {
|
||||
|
||||
function renderDetailsMemberName(member: Podkop.UrlTestMember) {
|
||||
const countryFlag = getDetectedCountryFlag(member.country);
|
||||
return countryFlag ? `${countryFlag} ${member.displayName}` : member.displayName;
|
||||
if (!countryFlag) {
|
||||
return member.displayName;
|
||||
}
|
||||
|
||||
return [
|
||||
E(
|
||||
'span',
|
||||
{ class: 'pdk_dashboard-page__urltest-details__country-badge' },
|
||||
countryFlag,
|
||||
),
|
||||
member.displayName,
|
||||
];
|
||||
}
|
||||
|
||||
function renderUrlTestSelectedValue(info: Podkop.UrlTestInfo) {
|
||||
|
||||
@@ -542,6 +542,17 @@ export const styles = `
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__country-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-right: 6px;
|
||||
padding: 2px 4px;
|
||||
border: 1px solid rgba(128, 128, 128, 0.25);
|
||||
border-radius: 4px;
|
||||
background: rgba(128, 128, 128, 0.15);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__priority-node {
|
||||
color: var(--text-color-high, #fff);
|
||||
font-weight: 600;
|
||||
|
||||
@@ -6,6 +6,7 @@ describe('runtimeTags', () => {
|
||||
it('uses the same direct section collision format as the backend generator', () => {
|
||||
expect(getOutboundTagBySection('proxy')).toBe('proxy-out');
|
||||
expect(getOutboundTagBySection('direct')).toBe('direct-out-1');
|
||||
expect(getOutboundTagBySection('bypass')).toBe('bypass-out-1');
|
||||
expect(getOutboundTagBySection('direct-1')).toBe('direct-1-out');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -56,6 +56,16 @@ describe('validateOutboundJson', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a tag already used by another JSON outbound', () => {
|
||||
const result = validateOutboundJson('{"type":"direct","tag":"duplicate"}', [
|
||||
'first',
|
||||
'duplicate',
|
||||
]);
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.message).toBe('Duplicate JSON outbound tag');
|
||||
});
|
||||
|
||||
it('requires server fields for common server outbound types', () => {
|
||||
const result = validateOutboundJson(
|
||||
'{"type":"socks","tag":"proxy","server":"127.0.0.1"}',
|
||||
|
||||
@@ -40,7 +40,10 @@ function validateOutbounds(value: unknown): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function validateOutboundJson(value: string): ValidationResult {
|
||||
export function validateOutboundJson(
|
||||
value: string,
|
||||
usedTags: string[] = [],
|
||||
): ValidationResult {
|
||||
const normalized = `${value || ''}`.trim();
|
||||
|
||||
if (!normalized.length) {
|
||||
@@ -67,6 +70,11 @@ export function validateOutboundJson(value: string): ValidationResult {
|
||||
return invalid(_('JSON outbound must contain a non-empty tag field'));
|
||||
}
|
||||
|
||||
const tag = parsed.tag.trim();
|
||||
if (usedTags.some((usedTag) => `${usedTag || ''}`.trim() === tag)) {
|
||||
return invalid(_('Duplicate JSON outbound tag'));
|
||||
}
|
||||
|
||||
const type = parsed.type.trim().toLowerCase();
|
||||
|
||||
if (
|
||||
|
||||
@@ -272,7 +272,7 @@ function validateServerPort(value) {
|
||||
function validateOutbounds(value) {
|
||||
return Array.isArray(value) && value.length > 0 && value.every((item) => nonEmptyString(item));
|
||||
}
|
||||
function validateOutboundJson(value) {
|
||||
function validateOutboundJson(value, usedTags = []) {
|
||||
const normalized = `${value || ""}`.trim();
|
||||
if (!normalized.length) {
|
||||
return invalid(_("JSON outbound cannot be empty"));
|
||||
@@ -292,6 +292,10 @@ function validateOutboundJson(value) {
|
||||
if (!nonEmptyString(parsed.tag)) {
|
||||
return invalid(_("JSON outbound must contain a non-empty tag field"));
|
||||
}
|
||||
const tag = parsed.tag.trim();
|
||||
if (usedTags.some((usedTag) => `${usedTag || ""}`.trim() === tag)) {
|
||||
return invalid(_("Duplicate JSON outbound tag"));
|
||||
}
|
||||
const type = parsed.type.trim().toLowerCase();
|
||||
if ((type === "selector" || type === "urltest") && !validateOutbounds(parsed.outbounds)) {
|
||||
return invalid(
|
||||
@@ -951,6 +955,19 @@ function parseValueList(value) {
|
||||
return value.split(/\n/).map((line) => line.split("//")[0].split("#")[0]).join(" ").split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
// src/helpers/getProxyUrlName.ts
|
||||
function getProxyUrlName(url) {
|
||||
try {
|
||||
const [_link, hash] = url.split("#");
|
||||
if (!hash) {
|
||||
return "";
|
||||
}
|
||||
return decodeURIComponent(hash);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// src/helpers/downloadAsTxt.ts
|
||||
function downloadAsTxt(text, filename) {
|
||||
const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
|
||||
@@ -1136,19 +1153,6 @@ async function executeShellCommand({
|
||||
}
|
||||
}
|
||||
|
||||
// src/helpers/getProxyUrlName.ts
|
||||
function getProxyUrlName(url) {
|
||||
try {
|
||||
const [_link, hash] = url.split("#");
|
||||
if (!hash) {
|
||||
return "";
|
||||
}
|
||||
return decodeURIComponent(hash);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// src/helpers/onMount.ts
|
||||
function getTarget(target) {
|
||||
if (typeof target === "string") {
|
||||
@@ -2456,7 +2460,8 @@ var RESERVED_RUNTIME_TAGS = /* @__PURE__ */ new Set([
|
||||
"tproxy6-in",
|
||||
"dns-in",
|
||||
"service-mixed-in",
|
||||
"direct-out"
|
||||
"direct-out",
|
||||
"bypass-out"
|
||||
]);
|
||||
function allocateRuntimeTag(base, postfix) {
|
||||
let suffix = 1;
|
||||
@@ -3666,8 +3671,9 @@ function getPriorityGroups(dashboardCache) {
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
function getOutboundDisplayName(code, entry, link, outboundMetadata) {
|
||||
return getProxyUrlName(link) || outboundMetadata?.names?.[code] || entry?.value?.name || code;
|
||||
function getOutboundDisplayName(code, entry, link, outboundMetadata, preferMetadata = false) {
|
||||
const metadataName = outboundMetadata?.names?.[code];
|
||||
return (preferMetadata ? metadataName : getProxyUrlName(link)) || (preferMetadata ? getProxyUrlName(link) : metadataName) || entry?.value?.name || code;
|
||||
}
|
||||
function buildUrlTestInfo({
|
||||
code,
|
||||
@@ -3697,7 +3703,8 @@ function buildUrlTestInfo({
|
||||
childCode,
|
||||
childEntry,
|
||||
link,
|
||||
outboundMetadata
|
||||
outboundMetadata,
|
||||
subscriptionCopyableCodes.has(childCode)
|
||||
),
|
||||
latency: childEntry?.value?.history?.[0]?.delay || 0,
|
||||
type: childEntry?.value?.type || "",
|
||||
@@ -3731,7 +3738,8 @@ function buildPriorityInfo({
|
||||
manualLinkByCode,
|
||||
cachedProxyLinks,
|
||||
outboundMetadata,
|
||||
subscriptionCopyableCodes
|
||||
subscriptionCopyableCodes,
|
||||
showDetectedCountries
|
||||
}) {
|
||||
const selectedCode = entry?.value.now || "";
|
||||
const cacheLevels = Array.isArray(groupCache?.levels) ? groupCache.levels : [];
|
||||
@@ -3773,13 +3781,15 @@ function buildPriorityInfo({
|
||||
childCode,
|
||||
childEntry,
|
||||
link,
|
||||
outboundMetadata
|
||||
outboundMetadata,
|
||||
subscriptionCopyableCodes.has(childCode)
|
||||
),
|
||||
latency: childEntry?.value?.history?.[0]?.delay || 0,
|
||||
type: childEntry?.value?.type || "",
|
||||
selected: selectedCode === childCode,
|
||||
link,
|
||||
canCopyLink,
|
||||
country: showDetectedCountries ? outboundMetadata?.countries?.[childCode] : void 0,
|
||||
levelIndex,
|
||||
levelName: level.displayName,
|
||||
levelId: level.id
|
||||
@@ -3879,7 +3889,13 @@ function buildProxyGroupOutbounds(section, proxies, outboundMetadata, urltestGro
|
||||
}
|
||||
const link = manualLinkByCode.get(code) || cachedProxyLinks.get(code) || "";
|
||||
const canCopyLink = isCopyableProxyLink(link) || subscriptionCopyableCodes.has(code);
|
||||
const displayName = priorityConfig?.displayName || urlTestConfig?.displayName || getOutboundDisplayName(code, item, link, outboundMetadata);
|
||||
const displayName = priorityConfig?.displayName || urlTestConfig?.displayName || getOutboundDisplayName(
|
||||
code,
|
||||
item,
|
||||
link,
|
||||
outboundMetadata,
|
||||
subscriptionCopyableCodes.has(code)
|
||||
);
|
||||
const isRuntimeUrlTest = isUrlTestProxyEntry(item);
|
||||
return [
|
||||
{
|
||||
@@ -3912,7 +3928,8 @@ function buildProxyGroupOutbounds(section, proxies, outboundMetadata, urltestGro
|
||||
manualLinkByCode,
|
||||
cachedProxyLinks,
|
||||
outboundMetadata,
|
||||
subscriptionCopyableCodes
|
||||
subscriptionCopyableCodes,
|
||||
showDetectedCountries: priorityConfig.showDetectedCountries
|
||||
}) : void 0
|
||||
}
|
||||
];
|
||||
@@ -6043,7 +6060,17 @@ function getDetectedCountryFlag(country) {
|
||||
}
|
||||
function renderDetailsMemberName(member) {
|
||||
const countryFlag = getDetectedCountryFlag(member.country);
|
||||
return countryFlag ? `${countryFlag} ${member.displayName}` : member.displayName;
|
||||
if (!countryFlag) {
|
||||
return member.displayName;
|
||||
}
|
||||
return [
|
||||
E(
|
||||
"span",
|
||||
{ class: "pdk_dashboard-page__urltest-details__country-badge" },
|
||||
countryFlag
|
||||
),
|
||||
member.displayName
|
||||
];
|
||||
}
|
||||
function renderUrlTestSelectedValue(info) {
|
||||
const selectedMember = info.outbounds.find((member) => member.selected);
|
||||
@@ -7396,6 +7423,17 @@ var styles = `
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__country-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-right: 6px;
|
||||
padding: 2px 4px;
|
||||
border: 1px solid rgba(128, 128, 128, 0.25);
|
||||
border-radius: 4px;
|
||||
background: rgba(128, 128, 128, 0.15);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__priority-node {
|
||||
color: var(--text-color-high, #fff);
|
||||
font-weight: 600;
|
||||
@@ -13741,6 +13779,7 @@ return baseclass.extend({
|
||||
bulkValidate,
|
||||
coreService,
|
||||
getClashUIUrl,
|
||||
getProxyUrlName,
|
||||
injectGlobalStyles,
|
||||
parseValueList,
|
||||
showToast,
|
||||
|
||||
@@ -376,6 +376,7 @@ let actionProvidersAvailabilityPromise = null;
|
||||
let actionProvidersAvailabilityLoader = null;
|
||||
const outboundNameChoicesCache = new Map();
|
||||
const outboundNameChoicesInflight = new Map();
|
||||
const outboundNameSourceOptions = new Map();
|
||||
const SECTION_CACHE_DIR = "/var/run/podkop-plus/section-cache";
|
||||
const COUNTRY_CODES =
|
||||
"AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG EH ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS YE YT ZA ZM ZW XK".split(
|
||||
@@ -1372,11 +1373,90 @@ function currentOutboundNameChoices(section_id, values) {
|
||||
};
|
||||
|
||||
(outboundNameChoicesCache.get(section_id) || []).forEach(append);
|
||||
currentDraftOutboundNames(section_id).forEach(append);
|
||||
normalizeDynamicListItems(values).forEach(append);
|
||||
|
||||
return result.sort((a, b) => a.label.localeCompare(b.label));
|
||||
}
|
||||
|
||||
function currentSourceOptionValues(section_id, optionName) {
|
||||
const liveValues = currentLiveDynamicListValues(section_id, optionName);
|
||||
if (liveValues != null) {
|
||||
return liveValues;
|
||||
}
|
||||
|
||||
const option = outboundNameSourceOptions.get(optionName);
|
||||
|
||||
if (option && typeof option.formvalue === "function") {
|
||||
try {
|
||||
const value = option.formvalue(section_id);
|
||||
if (value != null) {
|
||||
return normalizeDynamicListItems(value);
|
||||
}
|
||||
} catch (_error) {
|
||||
// Fall back to the LuCI UCI cache when the section widget is not mounted.
|
||||
}
|
||||
}
|
||||
|
||||
return getConfigListValues(section_id, optionName);
|
||||
}
|
||||
|
||||
function currentLiveDynamicListValues(section_id, optionName) {
|
||||
if (typeof document === "undefined") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const widget = document.getElementById(
|
||||
`cbid.${UCI_PACKAGE}.${section_id}.${optionName}`,
|
||||
);
|
||||
if (!widget) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const values = Array.from(
|
||||
widget.querySelectorAll('.item > input[type="hidden"]'),
|
||||
)
|
||||
.map((input) => `${input.value || ""}`.trim())
|
||||
.filter(Boolean);
|
||||
const pendingInput = widget.querySelector('.add-item > input[type="text"]');
|
||||
const pendingValue = `${(pendingInput && pendingInput.value) || ""}`.trim();
|
||||
|
||||
if (
|
||||
pendingValue &&
|
||||
!pendingInput.classList.contains("cbi-input-invalid") &&
|
||||
!values.includes(pendingValue)
|
||||
) {
|
||||
values.push(pendingValue);
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
function currentDraftOutboundNames(section_id) {
|
||||
const names = [];
|
||||
|
||||
currentSourceOptionValues(section_id, "selector_proxy_links").forEach(
|
||||
(value, index) => {
|
||||
const name = main.getProxyUrlName(`${value || ""}`);
|
||||
names.push(name || `${section_id}-${index + 1}-out`);
|
||||
},
|
||||
);
|
||||
currentSourceOptionValues(section_id, "interfaces").forEach((name) => {
|
||||
const normalized = `${name || ""}`.trim();
|
||||
if (normalized) {
|
||||
names.push(normalized);
|
||||
}
|
||||
});
|
||||
currentSourceOptionValues(section_id, "outbound_jsons").forEach((value) => {
|
||||
const tag = outboundJsonDisplayTag(value);
|
||||
if (tag) {
|
||||
names.push(tag);
|
||||
}
|
||||
});
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
function isDownloadThroughTargetSection(section, currentSectionId) {
|
||||
const sectionName = getUciSectionName(section);
|
||||
const action = (section && section.action) || "";
|
||||
@@ -3037,7 +3117,16 @@ function showOutboundJsonSettingsModal(
|
||||
jsonOption.modalonly = true;
|
||||
jsonOption.rmempty = false;
|
||||
jsonOption.validate = function (_itemId, value) {
|
||||
const validation = main.validateOutboundJson(`${value || ""}`);
|
||||
const usedTags =
|
||||
widget && widget.node
|
||||
? Array.from(widget.node.querySelectorAll(".item"))
|
||||
.filter((item) => item !== itemNode)
|
||||
.map((item) =>
|
||||
outboundJsonDisplayTag(dynamicListItemCurrentValue(item, "")),
|
||||
)
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
const validation = main.validateOutboundJson(`${value || ""}`, usedTags);
|
||||
return validation.valid ? true : validation.message;
|
||||
};
|
||||
configureTextareaOption(jsonOption);
|
||||
@@ -3063,13 +3152,17 @@ function showOutboundJsonSettingsModal(
|
||||
|
||||
function validateOutboundJsonItemsBeforeSave(_section_id, values) {
|
||||
const items = Array.isArray(values) ? values : values ? [values] : [];
|
||||
const tags = [];
|
||||
|
||||
for (const value of items) {
|
||||
const validation = main.validateOutboundJson(`${value || ""}`);
|
||||
const validation = main.validateOutboundJson(`${value || ""}`, tags);
|
||||
if (!validation.valid) {
|
||||
const tag = outboundJsonDisplayTag(value);
|
||||
return tag ? `${tag}: ${validation.message}` : validation.message;
|
||||
}
|
||||
|
||||
const tag = outboundJsonDisplayTag(value);
|
||||
tags.push(tag);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -6368,6 +6461,7 @@ function createSectionContent(section) {
|
||||
const validation = main.validateProxyUrl(value);
|
||||
return validation.valid ? true : validation.message;
|
||||
};
|
||||
outboundNameSourceOptions.set("selector_proxy_links", o);
|
||||
|
||||
o = section.taboption(
|
||||
"settings",
|
||||
@@ -6428,6 +6522,7 @@ function createSectionContent(section) {
|
||||
o.rmempty = true;
|
||||
o.modalonly = true;
|
||||
o.placeholder = _("Select a network interface");
|
||||
outboundNameSourceOptions.set("interfaces", o);
|
||||
|
||||
o = section.taboption(
|
||||
"settings",
|
||||
@@ -6453,6 +6548,7 @@ function createSectionContent(section) {
|
||||
const validation = main.validateOutboundJson(value);
|
||||
return validation.valid ? true : validation.message;
|
||||
};
|
||||
outboundNameSourceOptions.set("outbound_jsons", o);
|
||||
|
||||
o = section.taboption(
|
||||
"settings",
|
||||
|
||||
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PODKOP PLUS\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-10 05:05+0300\n"
|
||||
"PO-Revision-Date: 2026-07-10 05:05+0300\n"
|
||||
"POT-Creation-Date: 2026-07-10 15:25+0300\n"
|
||||
"PO-Revision-Date: 2026-07-10 15:25+0300\n"
|
||||
"Last-Translator: ushan0v\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
@@ -488,6 +488,9 @@ msgstr "Загружено"
|
||||
msgid "Drag rows to change priority. The rule at the top is checked first."
|
||||
msgstr "Перетаскивайте строки, чтобы изменить приоритет. Правило сверху проверяется первым."
|
||||
|
||||
msgid "Duplicate JSON outbound tag"
|
||||
msgstr "Такой тег JSON outbound уже используется"
|
||||
|
||||
msgid "Duplicate value"
|
||||
msgstr "Дублирующееся значение"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -78,7 +78,7 @@ config settings 'settings'
|
||||
# option label 'JSON outbound'
|
||||
# option enabled '1'
|
||||
# option action 'connection'
|
||||
# list outbound_jsons '{"type":"socks","server":"127.0.0.1","server_port":1080,"version":"5"}'
|
||||
# list outbound_jsons '{"type":"socks","tag":"Local SOCKS","server":"127.0.0.1","server_port":1080,"version":"5"}'
|
||||
# list community_lists 'russia_inside'
|
||||
|
||||
#config server 'server_json_inbound_example'
|
||||
|
||||
@@ -1102,15 +1102,20 @@ function validate_common_rule_references(section, context) {
|
||||
validate_plain_domain_ip_list_reference_value(value);
|
||||
}
|
||||
|
||||
function valid_outbound_json(value) {
|
||||
function outbound_json_object(value) {
|
||||
try {
|
||||
value = json(as_string(value));
|
||||
}
|
||||
catch (e) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
return type(value) == "object" && type(value.type) == "string";
|
||||
return type(value) == "object" ? value : null;
|
||||
}
|
||||
|
||||
function valid_outbound_json(value) {
|
||||
value = outbound_json_object(value);
|
||||
return value != null && type(value.type) == "string" && trim(as_string(value.type)) != "";
|
||||
}
|
||||
|
||||
function validate_outbound_json_rule(section) {
|
||||
@@ -1127,6 +1132,8 @@ function validate_outbound_json_rule(section) {
|
||||
function validate_outbound_json_values(section) {
|
||||
let name = section_name(section);
|
||||
let values = connections.outbound_jsons(section);
|
||||
let require_user_tag = length(list_option(section, "outbound_jsons")) > 0;
|
||||
let tags = {};
|
||||
|
||||
for (let value in values) {
|
||||
if (as_string(value) == "")
|
||||
@@ -1134,6 +1141,15 @@ function validate_outbound_json_values(section) {
|
||||
|
||||
if (!valid_outbound_json(value))
|
||||
fail_validation("Connection rule '" + name + "' must contain valid sing-box outbound JSON objects with a type field. Aborted.");
|
||||
|
||||
let outbound = outbound_json_object(value);
|
||||
let tag_name = trim(as_string(outbound.tag || ""));
|
||||
if (tag_name == "" && require_user_tag)
|
||||
fail_validation("Connection rule '" + name + "' has JSON outbound without a non-empty tag. Aborted.");
|
||||
if (tag_name != "" && tags[tag_name])
|
||||
fail_validation("Connection rule '" + name + "' has duplicate JSON outbound tag '" + tag_name + "'. Aborted.");
|
||||
if (tag_name != "")
|
||||
tags[tag_name] = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ const SB_FAKEIP_DNS_RULE_TAG = getenv("SB_FAKEIP_DNS_RULE_TAG") || "fakeip-dns-r
|
||||
const SB_FAKEIP_RULESET_DNS_RULE_TAG = getenv("SB_FAKEIP_RULESET_DNS_RULE_TAG") || "fakeip-ruleset-dns-rule-tag";
|
||||
const SB_SERVICE_FAKEIP_DNS_RULE_TAG = getenv("SB_SERVICE_FAKEIP_DNS_RULE_TAG") || "service-fakeip-dns-rule-tag";
|
||||
const SB_TPROXY_INBOUND_TAG = getenv("SB_TPROXY_INBOUND_TAG") || "tproxy-in";
|
||||
const SB_TPROXY_INBOUND6_TAG = getenv("SB_TPROXY_INBOUND6_TAG") || "tproxy6-in";
|
||||
const SB_DNS_INBOUND_TAG = getenv("SB_DNS_INBOUND_TAG") || "dns-in";
|
||||
const SB_SERVICE_MIXED_INBOUND_TAG = getenv("SB_SERVICE_MIXED_INBOUND_TAG") || "service-mixed-in";
|
||||
const SB_DIRECT_OUTBOUND_TAG = getenv("SB_DIRECT_OUTBOUND_TAG") || "direct-out";
|
||||
@@ -528,6 +529,7 @@ function default_reserved_runtime_tags() {
|
||||
SB_FAKEIP_RULESET_DNS_RULE_TAG,
|
||||
SB_SERVICE_FAKEIP_DNS_RULE_TAG,
|
||||
SB_TPROXY_INBOUND_TAG,
|
||||
SB_TPROXY_INBOUND6_TAG,
|
||||
SB_DNS_INBOUND_TAG,
|
||||
SB_SERVICE_MIXED_INBOUND_TAG,
|
||||
SB_DIRECT_OUTBOUND_TAG,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
let fs = require("fs");
|
||||
let uci_core = require("core.uci");
|
||||
let runtime_constants = require("singbox.constants");
|
||||
let validator_module = null;
|
||||
|
||||
const CONFIG_NAME = getenv("PODKOP_CONFIG_NAME") || "podkop-plus";
|
||||
@@ -18,18 +19,6 @@ const BYEDPI_RESPAWN_DELAY = getenv("BYEDPI_RESPAWN_DELAY") || "5";
|
||||
const BYEDPI_OPEN_FILES_LIMIT = getenv("BYEDPI_OPEN_FILES_LIMIT") || "4096";
|
||||
const BYEDPI_DEFAULT_CMD_OPTS = getenv("BYEDPI_DEFAULT_CMD_OPTS") || "-o 2 --auto=t,r,a,s -d 2";
|
||||
const SB_TPROXY_INBOUND_TAG = getenv("SB_TPROXY_INBOUND_TAG") || "tproxy-in";
|
||||
const RESERVED_RUNTIME_TAGS = [
|
||||
getenv("SB_DNS_SERVER_TAG") || "dns-server",
|
||||
getenv("SB_FAKEIP_DNS_SERVER_TAG") || "fakeip-server",
|
||||
getenv("SB_BOOTSTRAP_SERVER_TAG") || "bootstrap-dns-server",
|
||||
getenv("SB_FAKEIP_DNS_RULE_TAG") || "fakeip-dns-rule-tag",
|
||||
getenv("SB_FAKEIP_RULESET_DNS_RULE_TAG") || "fakeip-ruleset-dns-rule-tag",
|
||||
getenv("SB_SERVICE_FAKEIP_DNS_RULE_TAG") || "service-fakeip-dns-rule-tag",
|
||||
getenv("SB_TPROXY_INBOUND_TAG") || "tproxy-in",
|
||||
getenv("SB_DNS_INBOUND_TAG") || "dns-in",
|
||||
getenv("SB_SERVICE_MIXED_INBOUND_TAG") || "service-mixed-in",
|
||||
getenv("SB_DIRECT_OUTBOUND_TAG") || "direct-out"
|
||||
];
|
||||
|
||||
function as_string(value) {
|
||||
return value == null ? "" : "" + value;
|
||||
@@ -439,45 +428,8 @@ function restart_count() {
|
||||
return count;
|
||||
}
|
||||
|
||||
function str_last_index(value, needle) {
|
||||
value = as_string(value);
|
||||
needle = as_string(needle);
|
||||
if (needle == "")
|
||||
return length(value);
|
||||
|
||||
for (let i = length(value) - length(needle); i >= 0; i--)
|
||||
if (substr(value, i, length(needle)) == needle)
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
function tag_is_reserved(tag) {
|
||||
for (let value in RESERVED_RUNTIME_TAGS)
|
||||
if (tag == as_string(value))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function runtime_tag(base, postfix) {
|
||||
base = as_string(base);
|
||||
postfix = as_string(postfix);
|
||||
let candidate = base + "-" + postfix;
|
||||
let suffix = 1;
|
||||
|
||||
if (match(base, /-[0-9]/) != null) {
|
||||
let dash = str_last_index(base, "-");
|
||||
let parent = dash >= 0 ? substr(base, 0, dash) : base;
|
||||
if (tag_is_reserved(parent + "-" + postfix)) {
|
||||
candidate = base + "-" + suffix + "-" + postfix;
|
||||
suffix++;
|
||||
}
|
||||
}
|
||||
|
||||
while (tag_is_reserved(candidate)) {
|
||||
candidate = base + "-" + suffix + "-" + postfix;
|
||||
suffix++;
|
||||
}
|
||||
return candidate;
|
||||
return runtime_constants.tag(base, postfix);
|
||||
}
|
||||
|
||||
function read_json_file(path) {
|
||||
|
||||
@@ -3,23 +3,12 @@
|
||||
let fs = require("fs");
|
||||
let constants = require("core.constants");
|
||||
let uci_core = require("core.uci");
|
||||
let runtime_constants = require("singbox.constants");
|
||||
|
||||
const CONFIG_NAME = getenv("PODKOP_CONFIG_NAME") || constants.PODKOP_CONFIG_NAME || "podkop-plus";
|
||||
const LIB_DIR = getenv("PODKOP_LIB") || "/usr/lib/podkop-plus";
|
||||
const SB_TPROXY_INBOUND_TAG = getenv("SB_TPROXY_INBOUND_TAG") || constants.SB_TPROXY_INBOUND_TAG || "tproxy-in";
|
||||
const NFT_TABLE_NAME = getenv("NFT_TABLE_NAME") || constants.NFT_TABLE_NAME || "PodkopPlusTable";
|
||||
const RESERVED_RUNTIME_TAGS = [
|
||||
getenv("SB_DNS_SERVER_TAG") || constants.SB_DNS_SERVER_TAG || "dns-server",
|
||||
getenv("SB_FAKEIP_DNS_SERVER_TAG") || constants.SB_FAKEIP_DNS_SERVER_TAG || "fakeip-server",
|
||||
getenv("SB_BOOTSTRAP_SERVER_TAG") || constants.SB_BOOTSTRAP_SERVER_TAG || "bootstrap-dns-server",
|
||||
getenv("SB_FAKEIP_DNS_RULE_TAG") || constants.SB_FAKEIP_DNS_RULE_TAG || "fakeip-dns-rule-tag",
|
||||
getenv("SB_FAKEIP_RULESET_DNS_RULE_TAG") || constants.SB_FAKEIP_RULESET_DNS_RULE_TAG || "fakeip-ruleset-dns-rule-tag",
|
||||
getenv("SB_SERVICE_FAKEIP_DNS_RULE_TAG") || constants.SB_SERVICE_FAKEIP_DNS_RULE_TAG || "service-fakeip-dns-rule-tag",
|
||||
SB_TPROXY_INBOUND_TAG,
|
||||
getenv("SB_DNS_INBOUND_TAG") || constants.SB_DNS_INBOUND_TAG || "dns-in",
|
||||
getenv("SB_SERVICE_MIXED_INBOUND_TAG") || constants.SB_SERVICE_MIXED_INBOUND_TAG || "service-mixed-in",
|
||||
getenv("SB_DIRECT_OUTBOUND_TAG") || constants.SB_DIRECT_OUTBOUND_TAG || "direct-out"
|
||||
];
|
||||
|
||||
function as_string(value) {
|
||||
return value == null ? "" : "" + value;
|
||||
@@ -502,45 +491,8 @@ function start_runtime(cfg) {
|
||||
}
|
||||
}
|
||||
|
||||
function str_last_index(value, needle) {
|
||||
value = as_string(value);
|
||||
needle = as_string(needle);
|
||||
if (needle == "")
|
||||
return length(value);
|
||||
|
||||
for (let i = length(value) - length(needle); i >= 0; i--)
|
||||
if (substr(value, i, length(needle)) == needle)
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
function tag_is_reserved(tag) {
|
||||
for (let value in RESERVED_RUNTIME_TAGS)
|
||||
if (tag == as_string(value))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function runtime_tag(base, postfix) {
|
||||
base = as_string(base);
|
||||
postfix = as_string(postfix);
|
||||
let candidate = base + "-" + postfix;
|
||||
let suffix = 1;
|
||||
|
||||
if (match(base, /-[0-9]/) != null) {
|
||||
let dash = str_last_index(base, "-");
|
||||
let parent = dash >= 0 ? substr(base, 0, dash) : base;
|
||||
if (tag_is_reserved(parent + "-" + postfix)) {
|
||||
candidate = base + "-" + suffix + "-" + postfix;
|
||||
suffix++;
|
||||
}
|
||||
}
|
||||
|
||||
while (tag_is_reserved(candidate)) {
|
||||
candidate = base + "-" + suffix + "-" + postfix;
|
||||
suffix++;
|
||||
}
|
||||
return candidate;
|
||||
return runtime_constants.tag(base, postfix);
|
||||
}
|
||||
|
||||
function read_json_file(path) {
|
||||
|
||||
@@ -521,9 +521,11 @@ function rewrite_subscription_outbound_references(outbounds, tag_map) {
|
||||
if (type(outbound) != "object")
|
||||
continue;
|
||||
|
||||
let detour = as_string(outbound.detour || "");
|
||||
if (detour != "" && tag_map[detour])
|
||||
outbound.detour = tag_map[detour];
|
||||
for (let key in [ "detour", "default" ]) {
|
||||
let reference = as_string(outbound[key] || "");
|
||||
if (reference != "" && tag_map[reference])
|
||||
outbound[key] = tag_map[reference];
|
||||
}
|
||||
|
||||
if (type(outbound.outbounds) == "array") {
|
||||
let rewritten = [];
|
||||
@@ -570,6 +572,37 @@ function unique_tag(base, taken) {
|
||||
return base + "-overflow";
|
||||
}
|
||||
|
||||
function reserved_runtime_tag_set(outbounds) {
|
||||
let result = {};
|
||||
for (let tag_name in keys(object_or_empty(runtime_constants.RESERVED_TAGS)))
|
||||
result[tag_name] = true;
|
||||
|
||||
for (let outbound in array_or_empty(outbounds)) {
|
||||
if (type(outbound) != "object")
|
||||
continue;
|
||||
|
||||
let tag_name = as_string(outbound.tag || "");
|
||||
if (tag_name != "")
|
||||
result[tag_name] = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function assert_unique_outbound_tags(config) {
|
||||
let seen = {};
|
||||
for (let outbound in array_or_empty(config.outbounds)) {
|
||||
if (type(outbound) != "object")
|
||||
runtime_generate_unsupported("generated sing-box outbound is not an object");
|
||||
|
||||
let tag_name = as_string(outbound.tag || "");
|
||||
if (tag_name == "")
|
||||
runtime_generate_unsupported("generated sing-box outbound has an empty tag");
|
||||
if (seen[tag_name])
|
||||
runtime_generate_unsupported("generated sing-box config has duplicate outbound tag '" + tag_name + "'");
|
||||
seen[tag_name] = true;
|
||||
}
|
||||
}
|
||||
|
||||
function add_subscription_source_with_state(config, section, source_index, source_entry, taken, selector_tags, urltest_candidate_tags, state, show_metadata, include_urltest_groups, hide_urltest_group_outbounds, hide_detour_outbounds, node_prefix) {
|
||||
let section_name = section[".name"];
|
||||
let source_section = runtime_subscription.source_id(section_name, source_index);
|
||||
@@ -1827,30 +1860,77 @@ function parse_outbound_json(value) {
|
||||
return type(value) == "object" ? value : null;
|
||||
}
|
||||
|
||||
function add_json_connection_outbound(config, state, section, json_index, outbound_json, taken, selector_tags, urltest_candidate_tags) {
|
||||
let outbound = parse_outbound_json(outbound_json);
|
||||
if (outbound == null)
|
||||
runtime_generate_unsupported("JSON outbound is invalid");
|
||||
function rewrite_json_outbound_references(outbounds, tag_map) {
|
||||
for (let outbound in array_or_empty(outbounds)) {
|
||||
if (type(outbound) != "object")
|
||||
continue;
|
||||
|
||||
let display_name = as_string(outbound.remark || outbound.tag || ("JSON outbound " + json_index));
|
||||
let tag_name = connection_item_tag(section[".name"], "json", json_index);
|
||||
if (taken[tag_name])
|
||||
tag_name = unique_tag(tag_name, taken);
|
||||
taken[tag_name] = true;
|
||||
for (let key in [ "detour", "default" ]) {
|
||||
let reference = as_string(outbound[key] || "");
|
||||
if (reference != "" && tag_map[reference])
|
||||
outbound[key] = tag_map[reference];
|
||||
}
|
||||
|
||||
outbound.tag = tag_name;
|
||||
push(config.outbounds, outbound);
|
||||
push(selector_tags, tag_name);
|
||||
if (urltest_leaf_candidate_outbound(outbound))
|
||||
push(urltest_candidate_tags, tag_name);
|
||||
runtime_subscription.remember_outbound_metadata(state, tag_name, display_name, outbound);
|
||||
runtime_subscription.remember_urltest_group(state, tag_name, display_name, outbound);
|
||||
if (type(outbound.outbounds) == "array") {
|
||||
let rewritten = [];
|
||||
for (let tag_name in outbound.outbounds) {
|
||||
tag_name = as_string(tag_name);
|
||||
if (tag_name != "")
|
||||
push(rewritten, as_string(tag_map[tag_name] || tag_name));
|
||||
}
|
||||
outbound.outbounds = rewritten;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function prepare_json_connection_outbounds(section, taken) {
|
||||
let items = connections.outbound_jsons(section);
|
||||
let prepared = [];
|
||||
let outbounds = [];
|
||||
let tag_map = {};
|
||||
let legacy_tags = [];
|
||||
|
||||
for (let i = 0; i < length(items); i++) {
|
||||
let outbound = parse_outbound_json(items[i]);
|
||||
if (outbound == null)
|
||||
runtime_generate_unsupported("JSON outbound is invalid");
|
||||
|
||||
let display_name = trim(as_string(outbound.tag || ""));
|
||||
let legacy_tag = connection_item_tag(section[".name"], "json", i + 1);
|
||||
let base = display_name != "" ? display_name : legacy_tag;
|
||||
let tag_name = unique_tag(base, taken);
|
||||
taken[tag_name] = true;
|
||||
if (display_name != "" && !tag_map[display_name])
|
||||
tag_map[display_name] = tag_name;
|
||||
push(legacy_tags, [ legacy_tag, tag_name ]);
|
||||
|
||||
outbound.tag = tag_name;
|
||||
push(outbounds, outbound);
|
||||
push(prepared, {
|
||||
outbound,
|
||||
displayName: display_name != "" ? display_name : "JSON outbound " + (i + 1)
|
||||
});
|
||||
}
|
||||
|
||||
for (let entry in legacy_tags)
|
||||
if (!tag_map[entry[0]])
|
||||
tag_map[entry[0]] = entry[1];
|
||||
|
||||
rewrite_json_outbound_references(outbounds, tag_map);
|
||||
return prepared;
|
||||
}
|
||||
|
||||
function add_connection_json_outbounds(config, state, section, taken, selector_tags, urltest_candidate_tags) {
|
||||
let items = connections.outbound_jsons(section);
|
||||
for (let i = 0; i < length(items); i++)
|
||||
add_json_connection_outbound(config, state, section, i + 1, items[i], taken, selector_tags, urltest_candidate_tags);
|
||||
for (let item in prepare_json_connection_outbounds(section, taken)) {
|
||||
let outbound = item.outbound;
|
||||
let tag_name = outbound.tag;
|
||||
push(config.outbounds, outbound);
|
||||
push(selector_tags, tag_name);
|
||||
if (urltest_leaf_candidate_outbound(outbound))
|
||||
push(urltest_candidate_tags, tag_name);
|
||||
runtime_subscription.remember_outbound_metadata(state, tag_name, item.displayName, outbound);
|
||||
runtime_subscription.remember_urltest_group(state, tag_name, item.displayName, outbound);
|
||||
}
|
||||
}
|
||||
|
||||
function add_connections_outbound(config, section, taken) {
|
||||
@@ -2365,6 +2445,14 @@ function reserve_section_outbound_tags(sections, taken) {
|
||||
if (connections.is_connections_action(action) ||
|
||||
action == "byedpi" || action == "zapret" || action == "zapret2")
|
||||
taken[outbound_tag(section[".name"])] = true;
|
||||
|
||||
if (!connections.is_connections_action(action))
|
||||
continue;
|
||||
|
||||
for (let urltest_id in connections.urltests(section))
|
||||
taken[urltest_outbound_tag(section[".name"], urltest_id)] = true;
|
||||
for (let group_id in connections.priority_groups(section))
|
||||
taken[priority_outbound_tag(section[".name"], group_id)] = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2482,7 +2570,7 @@ function generate_config(output_path, service_address, mwan3_active) {
|
||||
runtime_generate_unsupported("no enabled sections");
|
||||
|
||||
let config = base_config(settings, service_address, { mwan3_active: cli_bool(mwan3_active) });
|
||||
let taken = { [runtime_constants.DIRECT_OUTBOUND_TAG]: true };
|
||||
let taken = reserved_runtime_tag_set(config.outbounds);
|
||||
reserve_section_outbound_tags(sections, taken);
|
||||
for (let server in servers)
|
||||
runtime_servers.add_server(config, server);
|
||||
@@ -2496,6 +2584,7 @@ function generate_config(output_path, service_address, mwan3_active) {
|
||||
for (let section in sections)
|
||||
add_mixed_proxy_for_section(config, section, service_address);
|
||||
|
||||
assert_unique_outbound_tags(config);
|
||||
strip_internal_fields(config);
|
||||
if (!write_json_file(output_path, config)) {
|
||||
warn("failed to write ", output_path, "\n");
|
||||
|
||||
@@ -2180,12 +2180,8 @@ function xray_transport_from_stream(stream) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function xray_display_name(original_tag, tag_counts, config_index) {
|
||||
function xray_display_name(original_tag) {
|
||||
original_tag = as_string(original_tag);
|
||||
if (original_tag == "")
|
||||
return "";
|
||||
if (int(tag_counts[original_tag] || 0) > 1)
|
||||
return original_tag + " #" + (config_index + 1);
|
||||
return original_tag;
|
||||
}
|
||||
|
||||
@@ -2488,21 +2484,7 @@ function xray_balancer_group_base(plan, display_name, config_index, config_count
|
||||
return xray_tag_base("urltest", config_index, config_count);
|
||||
}
|
||||
|
||||
function xray_tag_counts(configs) {
|
||||
let counts = {};
|
||||
for (let config in configs) {
|
||||
for (let outbound in array_or_empty(config.outbounds)) {
|
||||
if (type(outbound) != "object" || !xray_protocol_supported(outbound.protocol))
|
||||
continue;
|
||||
|
||||
let tag = xray_source_tag(outbound, lc(as_string(outbound.protocol || "server")));
|
||||
counts[tag] = (counts[tag] || 0) + 1;
|
||||
}
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
function xray_plan_config_outbounds(config, config_index, config_count, tag_counts, taken) {
|
||||
function xray_plan_config_outbounds(config, config_index, config_count, taken) {
|
||||
let source_outbounds = array_or_empty(config.outbounds);
|
||||
let planned = [];
|
||||
let tag_map = {};
|
||||
@@ -2563,9 +2545,9 @@ function xray_add_converted_outbound(result, item, tag_map, visible, display_nam
|
||||
push(result, converted);
|
||||
}
|
||||
|
||||
function xray_config_outbounds(config, config_index, config_count, tag_counts, taken) {
|
||||
function xray_config_outbounds(config, config_index, config_count, taken) {
|
||||
let source_outbounds = array_or_empty(config.outbounds);
|
||||
let plan = xray_plan_config_outbounds(config, config_index, config_count, tag_counts, taken);
|
||||
let plan = xray_plan_config_outbounds(config, config_index, config_count, taken);
|
||||
let planned = array_or_empty(plan.planned);
|
||||
let tag_map = object_or_empty(plan.tag_map);
|
||||
let display_name = xray_config_display_name(config, "");
|
||||
@@ -2620,7 +2602,7 @@ function xray_config_outbounds(config, config_index, config_count, tag_counts, t
|
||||
if (visible || dependency_tags[item.original_tag] || visible_source_tags[item.original_tag] === false) {
|
||||
let item_display_name = visible && display_name != ""
|
||||
? display_name
|
||||
: xray_display_name(item.original_tag, tag_counts, config_index);
|
||||
: xray_display_name(item.original_tag);
|
||||
xray_add_converted_outbound(result, item, tag_map, !hidden, item_display_name);
|
||||
}
|
||||
}
|
||||
@@ -2642,7 +2624,6 @@ function is_xray_config(value) {
|
||||
|
||||
function normalize_xray_configs(configs) {
|
||||
configs = array_or_empty(configs);
|
||||
let tag_counts = xray_tag_counts(configs);
|
||||
let taken = {};
|
||||
let outbounds = [];
|
||||
|
||||
@@ -2651,7 +2632,7 @@ function normalize_xray_configs(configs) {
|
||||
if (!is_xray_config(config))
|
||||
continue;
|
||||
|
||||
for (let outbound in xray_config_outbounds(config, i, length(configs), tag_counts, taken))
|
||||
for (let outbound in xray_config_outbounds(config, i, length(configs), taken))
|
||||
push(outbounds, outbound);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@ if grep -n -E 'require\("uci"\)\.cursor|uci -q|"uci", "-q"' "$BYEDPI_RUNTIME_UC"
|
||||
fi
|
||||
grep -Fq 'require("core.uci")' "$BYEDPI_RUNTIME_UC" ||
|
||||
fail "providers/byedpi/runtime.uc must use core.uci for runtime UCI access"
|
||||
grep -Fq 'require("singbox.constants")' "$BYEDPI_RUNTIME_UC" ||
|
||||
fail "providers/byedpi/runtime.uc must share the sing-box runtime tag allocator"
|
||||
grep -Fq 'runtime_constants.tag(base, postfix)' "$BYEDPI_RUNTIME_UC" ||
|
||||
fail "providers/byedpi/runtime.uc must allocate section tags through singbox.constants"
|
||||
|
||||
grep -Fq 'mode == "start-runtime"' "$BYEDPI_RUNTIME_UC" ||
|
||||
fail "providers/byedpi/runtime.uc must own ByeDPI start"
|
||||
|
||||
@@ -126,8 +126,8 @@ assert(outbound("independent").detour == "hop-out", "independent subscription ou
|
||||
assert(outbound("provider-group").detour == null, "subscription URLTest group must not receive Dial Fields");
|
||||
assert(outbound("source-interface-1-out").bind_interface == "wg0", "interface binding was not preserved");
|
||||
assert(outbound("source-interface-1-out").detour == null, "network interface must be excluded from section cascade");
|
||||
assert(outbound("source-json-1-out").detour == null, "plain JSON outbound must be excluded from section cascade");
|
||||
assert(outbound("source-json-2-out").detour == "direct-out", "explicit JSON detour was not preserved");
|
||||
assert(outbound("JSON plain").detour == null, "plain JSON outbound must be excluded from section cascade");
|
||||
assert(outbound("JSON explicit").detour == "direct-out", "explicit JSON detour was not preserved");
|
||||
assert(outbound("source-out").detour == null, "section selector must not receive its own detour");
|
||||
' "$WORK_DIR/output.json" || fail "section cascade generation"
|
||||
|
||||
|
||||
@@ -47,6 +47,12 @@ assert_eq dns-server-out-1 \
|
||||
assert_eq direct-out-1 \
|
||||
"$(ucode "$HELPERS_UC" outbound-tag direct)" \
|
||||
"reserved direct outbound tag"
|
||||
assert_eq bypass-out-1 \
|
||||
"$(ucode "$HELPERS_UC" outbound-tag bypass)" \
|
||||
"reserved bypass outbound tag"
|
||||
assert_eq tproxy6-in-1 \
|
||||
"$(ucode "$HELPERS_UC" inbound-tag tproxy6)" \
|
||||
"reserved IPv6 TProxy inbound tag"
|
||||
assert_eq direct-1-out \
|
||||
"$(ucode "$HELPERS_UC" outbound-tag direct-1)" \
|
||||
"numbered direct outbound tag"
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
PODKOP_LIB="$ROOT_DIR/podkop/files/usr/lib"
|
||||
GENERATOR_UC="$PODKOP_LIB/singbox/generator.uc"
|
||||
PARSER_UC="$PODKOP_LIB/subscription/parser.uc"
|
||||
VALIDATOR_UC="$PODKOP_LIB/config/validator.uc"
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$WORK_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
mkdir -p "$WORK_DIR/subscriptions"
|
||||
cat >"$WORK_DIR/subscriptions/proxy-subscription-1.json" <<'JSON'
|
||||
{
|
||||
"outbounds": [
|
||||
{ "type": "socks", "tag": "germany", "server": "127.0.0.1", "server_port": 1101 },
|
||||
{ "type": "socks", "tag": "germany", "server": "127.0.0.1", "server_port": 1102 },
|
||||
{ "type": "socks", "tag": "bypass-out", "server": "127.0.0.1", "server_port": 1103 },
|
||||
{ "type": "socks", "tag": "proxy-urltest-ut_name-out", "server": "127.0.0.1", "server_port": 1104 },
|
||||
{ "type": "socks", "tag": "json-hop", "server": "127.0.0.1", "server_port": 1105 },
|
||||
{ "type": "socks", "tag": "proxy-priority-pg_main-out", "server": "127.0.0.1", "server_port": 1106 },
|
||||
{ "type": "socks", "tag": "dns-server", "server": "127.0.0.1", "server_port": 1107 }
|
||||
]
|
||||
}
|
||||
JSON
|
||||
printf '%s\n' 'https://duplicate.example/sub' >"$WORK_DIR/subscriptions/proxy-subscription-1.url"
|
||||
: >"$WORK_DIR/subscriptions/proxy-subscription-1.user_agent"
|
||||
|
||||
cat >"$WORK_DIR/runtime-tags.json" <<'JSON'
|
||||
{
|
||||
"settings": { ".name": "settings", ".type": "settings", "log_level": "warn" },
|
||||
"section": [
|
||||
{
|
||||
".name": "proxy",
|
||||
".type": "section",
|
||||
"enabled": "1",
|
||||
"action": "connection",
|
||||
"subscription_urls": [ "https://duplicate.example/sub" ],
|
||||
"interfaces": [ "wg0" ],
|
||||
"outbound_jsons": [
|
||||
"{\"type\":\"socks\",\"tag\":\"germany\",\"server\":\"127.0.0.1\",\"server_port\":1201}",
|
||||
"{\"type\":\"socks\",\"tag\":\"germany\",\"server\":\"127.0.0.1\",\"server_port\":1203}",
|
||||
"{\"type\":\"direct\",\"tag\":\"direct-out\"}",
|
||||
"{\"type\":\"socks\",\"tag\":\"json-hop\",\"server\":\"127.0.0.1\",\"server_port\":1202}",
|
||||
"{\"type\":\"selector\",\"tag\":\"json-group\",\"outbounds\":[\"proxy-json-4-out\",\"direct-out\"],\"default\":\"json-hop\"}",
|
||||
"{\"type\":\"direct\",\"tag\":\"service-mixed-in\"}"
|
||||
],
|
||||
"domain_suffix": [ "example.org" ]
|
||||
},
|
||||
{
|
||||
".name": "backup",
|
||||
".type": "section",
|
||||
"enabled": "1",
|
||||
"action": "connection",
|
||||
"outbound_jsons": [
|
||||
"{\"type\":\"socks\",\"tag\":\"json-hop\",\"server\":\"127.0.0.1\",\"server_port\":1301}"
|
||||
],
|
||||
"domain_suffix": [ "backup.example.org" ]
|
||||
}
|
||||
],
|
||||
"urltest": [
|
||||
{
|
||||
".name": "ut_name",
|
||||
".type": "urltest",
|
||||
"section": "proxy",
|
||||
"name": "By name",
|
||||
"filter_mode": "include",
|
||||
"include_outbounds": [ "germany", "wg0", "json-hop" ]
|
||||
},
|
||||
{
|
||||
".name": "ut_regex",
|
||||
".type": "urltest",
|
||||
"section": "proxy",
|
||||
"name": "By regex",
|
||||
"filter_mode": "include",
|
||||
"include_regex": [ "^germany$" ]
|
||||
}
|
||||
],
|
||||
"priority_group": [
|
||||
{
|
||||
".name": "pg_main",
|
||||
".type": "priority_group",
|
||||
"section": "proxy",
|
||||
"name": "Priority"
|
||||
}
|
||||
],
|
||||
"priority_level": [
|
||||
{
|
||||
".name": "pl_main",
|
||||
".type": "priority_level",
|
||||
"group": "pg_main",
|
||||
"name": "All",
|
||||
"order": "0",
|
||||
"filter_mode": "disabled"
|
||||
}
|
||||
]
|
||||
}
|
||||
JSON
|
||||
|
||||
runtime_config="$WORK_DIR/runtime-config.json"
|
||||
mkdir -p "$runtime_config.section-cache"
|
||||
TMP_SUBSCRIPTION_FOLDER="$WORK_DIR/subscriptions" \
|
||||
ucode -L "$PODKOP_LIB" "$GENERATOR_UC" generate-config-fixture \
|
||||
"$WORK_DIR/runtime-tags.json" "$runtime_config" "127.0.0.1" "0"
|
||||
|
||||
ucode -e '
|
||||
let fs = require("fs");
|
||||
let config = json(fs.readfile(ARGV[0]));
|
||||
let proxy_cache = json(fs.readfile(ARGV[1]));
|
||||
let backup_cache = json(fs.readfile(ARGV[2]));
|
||||
let by_tag = {};
|
||||
let count = 0;
|
||||
for (let outbound in config.outbounds || []) {
|
||||
if (by_tag[outbound.tag])
|
||||
die("duplicate generated outbound tag: " + outbound.tag + "\n");
|
||||
by_tag[outbound.tag] = outbound;
|
||||
count++;
|
||||
}
|
||||
if (length(keys(by_tag)) != count)
|
||||
die("generated outbound tags are not globally unique\n");
|
||||
|
||||
for (let tag in [
|
||||
"direct-out", "bypass-out", "proxy-out", "backup-out",
|
||||
"germany", "germany-1", "germany-2", "germany-3",
|
||||
"bypass-out-1", "dns-server-1", "service-mixed-in-1",
|
||||
"proxy-urltest-ut_name-out-1", "proxy-priority-pg_main-out-1",
|
||||
"proxy-interface-1-out", "direct-out-1", "json-hop", "json-hop-1", "json-hop-2",
|
||||
"json-group", "proxy-urltest-ut_name-out", "proxy-urltest-ut_regex-out",
|
||||
"proxy-priority-pg_main-out"
|
||||
])
|
||||
if (!by_tag[tag])
|
||||
die("missing allocated outbound tag: " + tag + "\n");
|
||||
|
||||
let json_group = by_tag["json-group"];
|
||||
if (length(json_group.outbounds || []) != 2 ||
|
||||
json_group.outbounds[0] != "json-hop-1" || json_group.outbounds[1] != "direct-out-1")
|
||||
die("JSON outbound references were not rewritten to allocated tags\n");
|
||||
if (json_group.default != "json-hop-1")
|
||||
die("JSON selector default was not rewritten to its allocated tag\n");
|
||||
|
||||
let name_group = by_tag["proxy-urltest-ut_name-out"];
|
||||
let regex_group = by_tag["proxy-urltest-ut_regex-out"];
|
||||
if (length(name_group.outbounds || []) != 7)
|
||||
die("display-name filter did not select duplicate subscription, JSON and interface outbounds\n");
|
||||
for (let tag in [ "germany", "germany-1", "germany-2", "germany-3", "proxy-interface-1-out", "json-hop", "json-hop-1" ])
|
||||
if (index(name_group.outbounds, tag) < 0)
|
||||
die("display-name filter is missing " + tag + "\n");
|
||||
if (length(regex_group.outbounds || []) != 4)
|
||||
die("display-name regex did not collapse runtime suffixes\n");
|
||||
for (let tag in [ "germany", "germany-1", "germany-2", "germany-3" ])
|
||||
if (index(regex_group.outbounds, tag) < 0)
|
||||
die("display-name regex is missing " + tag + "\n");
|
||||
|
||||
let names = proxy_cache.outboundMetadata.names || {};
|
||||
for (let tag in [ "germany", "germany-1", "germany-2", "germany-3" ])
|
||||
if (names[tag] != "germany")
|
||||
die("runtime suffix leaked into display name for " + tag + "\n");
|
||||
if (names["proxy-interface-1-out"] != "wg0" ||
|
||||
names["json-hop"] != "json-hop" || names["json-hop-1"] != "json-hop")
|
||||
die("interface or JSON display tag metadata is missing\n");
|
||||
if (names["bypass-out-1"] != "bypass-out" ||
|
||||
names["dns-server-1"] != "dns-server" ||
|
||||
names["service-mixed-in-1"] != "service-mixed-in" ||
|
||||
names["proxy-urltest-ut_name-out-1"] != "proxy-urltest-ut_name-out" ||
|
||||
names["proxy-priority-pg_main-out-1"] != "proxy-priority-pg_main-out")
|
||||
die("system-tag collision suffix leaked into display metadata\n");
|
||||
if ((backup_cache.outboundMetadata.names || {})["json-hop-2"] != "json-hop")
|
||||
die("cross-section JSON tag allocation or display metadata is invalid\n");
|
||||
' "$runtime_config" "$runtime_config.section-cache/proxy.json" \
|
||||
"$runtime_config.section-cache/backup.json" || fail "runtime outbound tag allocation"
|
||||
|
||||
cat >"$WORK_DIR/duplicate-json-tags.json" <<'JSON'
|
||||
{
|
||||
"settings": { ".name": "settings", ".type": "settings" },
|
||||
"section": [
|
||||
{
|
||||
".name": "proxy",
|
||||
".type": "section",
|
||||
"enabled": "1",
|
||||
"action": "connection",
|
||||
"outbound_jsons": [
|
||||
"{\"type\":\"direct\",\"tag\":\"same\"}",
|
||||
"{\"type\":\"direct\",\"tag\":\"same\"}"
|
||||
],
|
||||
"domain_suffix": [ "example.org" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
JSON
|
||||
|
||||
cat >"$WORK_DIR/tagless-json.json" <<'JSON'
|
||||
{
|
||||
"settings": { ".name": "settings", ".type": "settings" },
|
||||
"section": [
|
||||
{
|
||||
".name": "proxy",
|
||||
".type": "section",
|
||||
"enabled": "1",
|
||||
"action": "connection",
|
||||
"outbound_jsons": [ "{\"type\":\"direct\"}" ],
|
||||
"domain_suffix": [ "example.org" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
JSON
|
||||
|
||||
validate_rejects() {
|
||||
local fixture="$1"
|
||||
local expected="$2"
|
||||
local output
|
||||
if output="$(PODKOP_LIB="$PODKOP_LIB" ucode -L "$PODKOP_LIB" "$VALIDATOR_UC" \
|
||||
validate-runtime-fixture "$fixture" '{}' 2>/dev/null)"; then
|
||||
fail "validator accepted $fixture"
|
||||
fi
|
||||
printf '%s\n' "$output" | grep -Fq "$expected" ||
|
||||
fail "validator message for $fixture does not contain: $expected"
|
||||
}
|
||||
|
||||
validate_rejects "$WORK_DIR/duplicate-json-tags.json" "duplicate JSON outbound tag 'same'"
|
||||
validate_rejects "$WORK_DIR/tagless-json.json" "JSON outbound without a non-empty tag"
|
||||
|
||||
cat >"$WORK_DIR/xray-duplicates.json" <<'JSON'
|
||||
[
|
||||
{
|
||||
"outbounds": [
|
||||
{
|
||||
"protocol": "socks",
|
||||
"tag": "germany",
|
||||
"settings": { "servers": [ { "address": "127.0.0.1", "port": 1401 } ] }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"outbounds": [
|
||||
{
|
||||
"protocol": "socks",
|
||||
"tag": "germany",
|
||||
"settings": { "servers": [ { "address": "127.0.0.1", "port": 1402 } ] }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
JSON
|
||||
|
||||
ucode -L "$PODKOP_LIB" "$PARSER_UC" normalize-content \
|
||||
"$WORK_DIR/xray-duplicates.json" "$WORK_DIR/xray-normalized.json"
|
||||
ucode -e '
|
||||
let fs = require("fs");
|
||||
let value = json(fs.readfile(ARGV[0]));
|
||||
let outbounds = value.outbounds || [];
|
||||
if (length(outbounds) != 2 || outbounds[0].tag != "germany" || outbounds[1].tag != "germany-1")
|
||||
die("Xray duplicate runtime tags were not allocated\n");
|
||||
if (outbounds[0].remark != "germany" || outbounds[1].remark != "germany")
|
||||
die("Xray runtime suffix leaked into user-visible names\n");
|
||||
' "$WORK_DIR/xray-normalized.json" || fail "Xray duplicate display names"
|
||||
|
||||
printf 'outbound tag regression checks passed\n'
|
||||
@@ -86,6 +86,10 @@ if grep -n -E 'require\("uci"\)\.cursor|uci -q|"uci", "-q"|command_output\(comma
|
||||
fi
|
||||
grep -Fq 'require("core.uci")' "$NFQUEUE_RUNTIME" ||
|
||||
fail "providers/nfqueue/runtime.uc must use core.uci for runtime UCI access"
|
||||
grep -Fq 'require("singbox.constants")' "$NFQUEUE_RUNTIME" ||
|
||||
fail "providers/nfqueue/runtime.uc must share the sing-box runtime tag allocator"
|
||||
grep -Fq 'runtime_constants.tag(base, postfix)' "$NFQUEUE_RUNTIME" ||
|
||||
fail "providers/nfqueue/runtime.uc must allocate section tags through singbox.constants"
|
||||
|
||||
for mode in \
|
||||
'mode == "start-runtime"' \
|
||||
|
||||
Reference in New Issue
Block a user