Files
NeoTachyon/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js
T
Dushnilin 387a78aebb Кнопка Generate WARP Config: исправление binary UCI и стилизация
- Добавлен генератор WARP конфигурации (warp_generator.uc)
- Исправлена запись binary UCI значений (<b 0x...>) в поля awg_i1-i5:
  warp_generator.uc использует to_hex(), section.js очищает значения перед записью
- generator.uc: добавлена защита uci_bin_to_hex() при чтении awg параметров
- generator.uc: поля awg_i3/i4/i5 со значением '0' пропускаются
- section.js: кнопка Generate WARP Config переработана в стиле Tachyon UI
  (cbi-button-neutral, состояния loading/success/error с анимациями)
2026-07-17 22:22:08 +03:00

8628 lines
234 KiB
JavaScript

"use strict";
"require form";
"require baseclass";
"require fs";
"require network";
"require ui";
"require uci";
"require view.tachyon.local_devices as localDevices";
"require view.tachyon.main as main";
const UCI_PACKAGE = main.TACHYON_UCI_PACKAGE;
const ACTION_PROVIDERS_AVAILABILITY_EVENT =
main.TACHYON_ACTION_PROVIDERS_AVAILABILITY_EVENT ||
"tachyon:action-providers-availability";
const RULE_SET_ITEM_SETTINGS_KEY = "rule_set_settings";
const ROUTING_ACTIONS = [
"connection",
"proxy",
"outbound",
"vpn",
"bypass",
"block",
"zapret",
"zapret2",
"byedpi",
];
const CONNECTIONS_BLOCKED_INTERFACES = [
"br-lan",
"eth0",
"eth1",
"wan",
"phy0-ap0",
"phy1-ap0",
"pppoe-wan",
"lan",
];
function valuesToText(values) {
if (!values) {
return "";
}
if (Array.isArray(values)) {
return values.filter(Boolean).join("\n");
}
return values ? `${values}` : "";
}
function normalizeOptionValues(value) {
if (!value) {
return [];
}
if (Array.isArray(value)) {
return value
.filter(Boolean)
.map((item) => `${item}`.trim())
.filter(Boolean);
}
return `${value}`
.split(/\s+/)
.map((item) => item.trim())
.filter(Boolean);
}
function getUciSectionName(section) {
return section && section[".name"] ? section[".name"] : "";
}
function getUciSectionLabel(section) {
return (section && section.label) || getUciSectionName(section);
}
function isOutboundDetourTargetSection(section, currentSectionId) {
const sectionName = getUciSectionName(section);
const action = (section && section.action) || "";
return (
sectionName &&
sectionName !== currentSectionId &&
section.enabled !== "0" &&
["connection", "proxy", "outbound", "vpn"].includes(action)
);
}
function getOutboundDetourTargetSections(currentSectionId) {
return (uci.sections(UCI_PACKAGE, "section") || []).filter((section) =>
isOutboundDetourTargetSection(section, currentSectionId),
);
}
function getDefaultOutboundDetourSection(currentSectionId) {
const targetSections = getOutboundDetourTargetSections(currentSectionId);
return targetSections.length ? getUciSectionName(targetSections[0]) : "";
}
function refreshOutboundDetourSectionOptionValues(option, sectionId) {
option.keylist = [];
option.vallist = [];
getOutboundDetourTargetSections(sectionId).forEach((targetSection) => {
option.value(
getUciSectionName(targetSection),
getUciSectionLabel(targetSection),
);
});
}
function isDnsDetourTargetSection(section, currentSectionId) {
const sectionName = getUciSectionName(section);
const action = (section && section.action) || "";
if (
!sectionName ||
sectionName === currentSectionId ||
section.enabled === "0"
) {
return false;
}
if (["connection", "proxy", "outbound", "vpn"].includes(action)) {
return true;
}
if (action === "zapret") {
return isZapretInstalledForUi();
}
if (action === "zapret2") {
return isZapret2InstalledForUi();
}
return action === "byedpi" && isByedpiInstalledForUi();
}
function refreshDnsDetourSectionOptionValues(option, sectionId) {
option.keylist = [];
option.vallist = [];
(uci.sections(UCI_PACKAGE, "section") || [])
.filter((section) => isDnsDetourTargetSection(section, sectionId))
.forEach((targetSection) => {
option.value(
getUciSectionName(targetSection),
getUciSectionLabel(targetSection),
);
});
}
function dependsOnRoutingAction(option) {
ROUTING_ACTIONS.forEach((action) => option.depends("action", action));
return option;
}
const ZAPRET_LEGACY_DEFAULT_NFQWS_OPT =
"--filter-tcp=80 <HOSTLIST> --dpi-desync=fake,fakedsplit --dpi-desync-autottl=2 --dpi-desync-fooling=badsum --new --filter-tcp=443 --hostlist=/opt/zapret/ipset/zapret-hosts-google.txt --dpi-desync=fake,multidisorder --dpi-desync-split-pos=1,midsld --dpi-desync-repeats=11 --dpi-desync-fooling=badsum --dpi-desync-fake-tls-mod=rnd,dupsid,sni=www.google.com --new --filter-udp=443 --hostlist=/opt/zapret/ipset/zapret-hosts-google.txt --dpi-desync=fake --dpi-desync-repeats=11 --dpi-desync-fake-quic=/opt/zapret/files/fake/quic_initial_www_google_com.bin --new --filter-udp=443 <HOSTLIST_NOAUTO> --dpi-desync=fake --dpi-desync-repeats=11 --new --filter-tcp=443 <HOSTLIST> --dpi-desync=multidisorder --dpi-desync-split-pos=1,sniext+1,host+1,midsld-2,midsld,midsld+2,endhost-1";
const ZAPRET_DEFAULT_NFQWS_OPT =
"--filter-tcp=80 --dpi-desync=fake,fakedsplit --dpi-desync-autottl=2 --dpi-desync-fooling=badsum --new --filter-tcp=443 --dpi-desync=fake,multidisorder --dpi-desync-split-pos=1,midsld --dpi-desync-repeats=11 --dpi-desync-fooling=badsum --dpi-desync-fake-tls-mod=rnd,dupsid,sni=www.google.com --new --filter-udp=443 --dpi-desync=fake --dpi-desync-repeats=11 --dpi-desync-fake-quic=/opt/zapret/files/fake/quic_initial_www_google_com.bin";
const ZAPRET2_DEFAULT_NFQWS2_OPT =
"--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=fake:blob=fake_default_tls:tcp_md5:tcp_seq=-10000 --lua-desync=multidisorder:pos=1,midsld --new --filter-udp=443 --filter-l7=quic --payload=quic_initial --lua-desync=fake:blob=fake_default_quic:repeats=6";
const BYEDPI_DEFAULT_CMD_OPTS = "-o 2 --auto=t,r,a,s -d 2";
const ANNOTATED_TEXTAREA_STYLE_ID = "fkp-annotated-textarea-styles";
const CONNECTIONS_DYNLIST_STYLE_ID = "fkp-connections-dynlist-styles";
const NFQWS_REMOTE_VALIDATION_DEBOUNCE_MS = 500;
const NFQWS_VALIDATION_COMMAND = "/usr/bin/tachyon";
const nfqwsRemoteValidationCache = new Map();
const nfqwsRemoteValidationInflight = new Map();
const nfqws2RemoteValidationCache = new Map();
const nfqws2RemoteValidationInflight = new Map();
const byedpiRemoteValidationCache = new Map();
const byedpiRemoteValidationInflight = new Map();
const BYEDPI_LONG_VALUE_OPTIONS = new Set([
"--max-conn",
"--conn-ip",
"--buf-size",
"--debug",
"--def-ttl",
"--auto",
"--auto-mode",
"--cache-ttl",
"--cache-dump",
"--timeout",
"--proto",
"--hosts",
"--ipset",
"--pf",
"--round",
"--split",
"--disorder",
"--oob",
"--disoob",
"--fake",
"--fake-sni",
"--ttl",
"--fake-offset",
"--fake-data",
"--fake-tls-mod",
"--oob-data",
"--mod-http",
"--tlsrec",
"--tlsminor",
"--udp-fake",
]);
const BYEDPI_LONG_FLAG_OPTIONS = new Set([
"--md5sig",
"--tfo",
"--drop-sack",
"--no-domain",
"--no-udp",
]);
const BYEDPI_SHORT_VALUE_OPTIONS = new Set([
"-c",
"-I",
"-b",
"-x",
"-g",
"-A",
"-L",
"-u",
"-y",
"-T",
"-K",
"-H",
"-j",
"-V",
"-R",
"-s",
"-d",
"-o",
"-q",
"-f",
"-n",
"-t",
"-O",
"-l",
"-Q",
"-e",
"-M",
"-r",
"-m",
"-a",
]);
const BYEDPI_SHORT_FLAG_OPTIONS = new Set(["-N", "-U", "-F", "-S", "-Y"]);
const NFQWS_OPTIONAL_ARG_OPTIONS = new Set([
"--comment",
"--ctrack-disable",
"--debug",
"--dpi-desync-any-protocol",
"--dpi-desync-autottl",
"--dpi-desync-autottl6",
"--dpi-desync-skip-nosni",
"--dpi-desync-tcp-flags-set",
"--dpi-desync-tcp-flags-unset",
"--dup-autottl",
"--dup-autottl6",
"--dup-replace",
"--dup-tcp-flags-set",
"--dup-tcp-flags-unset",
"--ipcache-hostname",
"--orig-autottl",
"--orig-autottl6",
"--orig-tcp-flags-set",
"--orig-tcp-flags-unset",
"--synack-split",
]);
const NFQWS_NO_ARG_OPTIONS = new Set([
"--bind-fix4",
"--bind-fix6",
"--daemon",
"--domcase",
"--dry-run",
"--hostcase",
"--hostnospace",
"--methodeol",
"--new",
"--skip",
"--version",
]);
const NFQWS_REQUIRED_ARG_OPTIONS = new Set([
"--ctrack-timeouts",
"--dpi-desync",
"--dpi-desync-badack-increment",
"--dpi-desync-badseq-increment",
"--dpi-desync-cutoff",
"--dpi-desync-fake-dht",
"--dpi-desync-fake-discord",
"--dpi-desync-fake-http",
"--dpi-desync-fake-quic",
"--dpi-desync-fake-stun",
"--dpi-desync-fake-syndata",
"--dpi-desync-fake-tcp-mod",
"--dpi-desync-fake-tls",
"--dpi-desync-fake-tls-mod",
"--dpi-desync-fake-unknown",
"--dpi-desync-fake-unknown-udp",
"--dpi-desync-fake-wireguard",
"--dpi-desync-fakedsplit-mod",
"--dpi-desync-fakedsplit-pattern",
"--dpi-desync-fooling",
"--dpi-desync-fwmark",
"--dpi-desync-hostfakesplit-midhost",
"--dpi-desync-hostfakesplit-mod",
"--dpi-desync-ipfrag-pos-tcp",
"--dpi-desync-ipfrag-pos-udp",
"--dpi-desync-repeats",
"--dpi-desync-split-http-req",
"--dpi-desync-split-pos",
"--dpi-desync-split-seqovl",
"--dpi-desync-split-seqovl-pattern",
"--dpi-desync-split-tls",
"--dpi-desync-start",
"--dpi-desync-ts-increment",
"--dpi-desync-ttl",
"--dpi-desync-ttl6",
"--dpi-desync-udplen-increment",
"--dpi-desync-udplen-pattern",
"--dup",
"--dup-badack-increment",
"--dup-badseq-increment",
"--dup-cutoff",
"--dup-fooling",
"--dup-ip-id",
"--dup-start",
"--dup-ts-increment",
"--dup-ttl",
"--dup-ttl6",
"--filter-l3",
"--filter-l7",
"--filter-tcp",
"--filter-udp",
"--hostlist",
"--hostlist-auto",
"--hostlist-auto-debug",
"--hostlist-auto-fail-threshold",
"--hostlist-auto-fail-time",
"--hostlist-auto-retrans-threshold",
"--hostlist-domains",
"--hostlist-exclude",
"--hostlist-exclude-domains",
"--hostspell",
"--ip-id",
"--ipcache-lifetime",
"--ipset",
"--ipset-exclude",
"--ipset-exclude-ip",
"--ipset-ip",
"--orig-mod-cutoff",
"--orig-mod-start",
"--orig-ttl",
"--orig-ttl6",
"--pidfile",
"--qnum",
"--uid",
"--user",
"--wsize",
"--wssize",
"--wssize-cutoff",
"--wssize-forced-cutoff",
]);
const NFQWS2_OPTIONAL_ARG_OPTIONS = new Set([
"--chdir",
"--comment",
"--ctrack-disable",
"--debug",
"--hostlist-auto-retrans-reset",
"--intercept",
"--ipcache-hostname",
"--new",
"--payload-disable",
"--reasm-disable",
"--server",
"--template",
"--writeable",
]);
const NFQWS2_NO_ARG_OPTIONS = new Set([
"--bind-fix4",
"--bind-fix6",
"--daemon",
"--dry-run",
"--skip",
"--version",
]);
const NFQWS2_REQUIRED_ARG_OPTIONS = new Set([
"--blob",
"--cookie",
"--ctrack-timeouts",
"--filter-l3",
"--filter-l7",
"--filter-tcp",
"--filter-udp",
"--fwmark",
"--fuzz",
"--hostlist",
"--hostlist-auto",
"--hostlist-auto-debug",
"--hostlist-auto-fail-threshold",
"--hostlist-auto-fail-time",
"--hostlist-auto-retrans-threshold",
"--hostlist-domains",
"--hostlist-exclude",
"--hostlist-exclude-domains",
"--import",
"--in-range",
"--ipcache-lifetime",
"--ipset",
"--ipset-exclude",
"--ipset-exclude-ip",
"--ipset-ip",
"--lua-gc",
"--lua-init",
"--lua-desync",
"--name",
"--out-range",
"--payload",
"--pidfile",
"--qnum",
"--uid",
"--user",
]);
const actionProvidersAvailabilityState = {
loaded: false,
zapretInstalled: false,
zapret2Installed: false,
byedpiInstalled: false,
singBoxExtended: false,
};
let actionProvidersAvailabilityPromise = null;
let actionProvidersAvailabilityLoader = null;
const outboundNameChoicesCache = new Map();
const outboundNameChoicesInflight = new Map();
const outboundNameSourceOptions = new Map();
const sectionGroupSourceOptions = new Map();
const dashboardFilterChoiceRefreshers = new Map();
const SECTION_CACHE_DIR = "/var/run/tachyon/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(
" ",
);
const REGION_NAME_FALLBACKS = {
XK: "Kosovo",
};
let regionDisplayNamesCache = {};
function updateActionProvidersAvailabilityState(nextState) {
if (!nextState) {
return;
}
actionProvidersAvailabilityState.loaded = true;
if (typeof nextState.zapretInstalled !== "undefined") {
actionProvidersAvailabilityState.zapretInstalled = Boolean(
nextState.zapretInstalled,
);
}
if (typeof nextState.zapret2Installed !== "undefined") {
actionProvidersAvailabilityState.zapret2Installed = Boolean(
nextState.zapret2Installed,
);
}
if (typeof nextState.byedpiInstalled !== "undefined") {
actionProvidersAvailabilityState.byedpiInstalled = Boolean(
nextState.byedpiInstalled,
);
}
if (typeof nextState.singBoxExtended !== "undefined") {
actionProvidersAvailabilityState.singBoxExtended = Boolean(
nextState.singBoxExtended,
);
}
actionProvidersAvailabilityPromise = null;
}
function updateActionProvidersAvailabilityFromSystemInfo(systemInfo) {
if (!systemInfo || !systemInfo.providerInfoLoaded) {
return;
}
updateActionProvidersAvailabilityState({
zapretInstalled: Boolean(systemInfo.zapret_installed),
zapret2Installed: Boolean(systemInfo.zapret2_installed),
byedpiInstalled: Boolean(systemInfo.byedpi_installed),
singBoxExtended: Boolean(systemInfo.sing_box_extended),
});
}
function setActionProvidersAvailabilityLoader(loader) {
actionProvidersAvailabilityLoader =
typeof loader === "function" ? loader : null;
}
if (typeof window !== "undefined") {
window.addEventListener(ACTION_PROVIDERS_AVAILABILITY_EVENT, (event) => {
updateActionProvidersAvailabilityState(event.detail);
});
}
if (main.store && typeof main.store.subscribe === "function") {
main.store.subscribe((next, _prev, diff) => {
if (!diff || diff.diagnosticsSystemInfo) {
updateActionProvidersAvailabilityFromSystemInfo(
next.diagnosticsSystemInfo,
);
}
});
}
function getLuciLanguage() {
if (typeof L !== "undefined" && L.env && L.env.lang) {
return `${L.env.lang}`.replace("_", "-");
}
if (document.documentElement.lang) {
return document.documentElement.lang;
}
return navigator.language || "en";
}
function getRegionDisplayName(code) {
const normalizedCode = `${code || ""}`.toUpperCase();
const language = getLuciLanguage();
const cacheKey = `${language}:${normalizedCode}`;
if (regionDisplayNamesCache[cacheKey]) {
return regionDisplayNamesCache[cacheKey];
}
try {
if (typeof Intl !== "undefined" && Intl.DisplayNames) {
const displayNames = new Intl.DisplayNames([language, "en"], {
type: "region",
});
const displayName = displayNames.of(normalizedCode);
if (displayName && displayName !== normalizedCode) {
regionDisplayNamesCache[cacheKey] = displayName;
return displayName;
}
}
} catch (_error) {
// Fall through to the static fallback.
}
const fallback = REGION_NAME_FALLBACKS[normalizedCode] || normalizedCode;
regionDisplayNamesCache[cacheKey] = fallback;
return fallback;
}
function getCountryFlagEmoji(code) {
const normalizedCode = `${code || ""}`.toUpperCase();
if (!/^[A-Z]{2}$/.test(normalizedCode)) {
return "";
}
return String.fromCodePoint(
...normalizedCode
.split("")
.map((char) => 0x1f1e6 + char.charCodeAt(0) - 65),
);
}
function getCountryOptionLabel(code) {
return `${getCountryFlagEmoji(code)} ${getRegionDisplayName(code)}`;
}
function validateCountryCode(_section_id, value) {
const values = Array.isArray(value) ? value : [value];
const normalizedValues = values
.filter((item) => item && `${item}`.length)
.map((item) => `${item}`.toUpperCase());
if (!normalizedValues.length) {
return true;
}
return normalizedValues.every((item) => COUNTRY_CODES.includes(item))
? true
: _("Unknown country");
}
function plainObject(value) {
return value && typeof value === "object" && !Array.isArray(value)
? value
: {};
}
function safeCacheSectionName(section_id) {
return /^[A-Za-z0-9_-]+$/.test(`${section_id || ""}`);
}
function filteredOutboundMetadataFromCache(cache) {
const metadata = plainObject(plainObject(cache).outboundMetadata);
const names = plainObject(metadata.names);
const countries = plainObject(metadata.countries);
const candidateTags = Array.isArray(cache.urltestCandidateTags)
? cache.urltestCandidateTags
: [];
const groups = plainObject(cache.urltestGroups);
const result = {
names: {},
countries: {},
};
if (candidateTags.length > 0) {
candidateTags.forEach((tag) => {
tag = `${tag || ""}`;
if (!tag) {
return;
}
if (names[tag] != null) {
result.names[tag] = names[tag];
}
if (countries[tag] != null) {
result.countries[tag] = countries[tag];
}
});
return result;
}
Object.entries(names).forEach(([tag, name]) => {
if (!groups[tag]) {
result.names[tag] = name;
}
});
Object.entries(countries).forEach(([tag, country]) => {
if (!groups[tag]) {
result.countries[tag] = country;
}
});
return result;
}
function readOutboundMetadataFromSectionCache(section_id) {
if (!safeCacheSectionName(section_id)) {
return Promise.resolve({ names: {}, countries: {} });
}
return fs
.read(`${SECTION_CACHE_DIR}/${section_id}.json`)
.then((raw) => filteredOutboundMetadataFromCache(JSON.parse(raw || "{}")))
.catch(() => ({ names: {}, countries: {} }));
}
function loadOutboundNameChoices(section_id) {
if (outboundNameChoicesCache.has(section_id)) {
return Promise.resolve(outboundNameChoicesCache.get(section_id));
}
if (outboundNameChoicesInflight.has(section_id)) {
return outboundNameChoicesInflight.get(section_id);
}
const task = readOutboundMetadataFromSectionCache(section_id)
.then((metadata) => {
const names = Object.values(plainObject(metadata.names));
const choices = names
.filter(Boolean)
.filter((name, index, values) => values.indexOf(name) === index)
.sort((a, b) => `${a}`.localeCompare(`${b}`));
outboundNameChoicesCache.set(section_id, choices);
return choices;
})
.catch(() => [])
.finally(() => {
outboundNameChoicesInflight.delete(section_id);
});
outboundNameChoicesInflight.set(section_id, task);
return task;
}
function normalizeDynamicListItems(value) {
if (!value) {
return [];
}
if (Array.isArray(value)) {
return value
.map((item) => `${item || ""}`.trim())
.filter((item) => item.length);
}
const normalized = `${value}`.trim();
return normalized.length ? [normalized] : [];
}
function uniqueDynamicListItems(value) {
const seen = new Set();
const result = [];
normalizeDynamicListItems(value).forEach((item) => {
if (!seen.has(item)) {
seen.add(item);
result.push(item);
}
});
return result;
}
function readItemSettingsMap(section_id, settingsKey) {
const raw = uci.get(UCI_PACKAGE, section_id, settingsKey);
if (!raw) {
return {};
}
try {
const parsed = JSON.parse(`${raw}`);
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
? parsed
: {};
} catch (_error) {
return {};
}
}
function compactItemSettings(values) {
const result = {};
Object.entries(values || {}).forEach(([key, value]) => {
if (value === undefined || value === null || value === "") {
return;
}
if (Array.isArray(value)) {
const items = value
.map((item) => `${item || ""}`.trim())
.filter((item) => item.length);
if (items.length) {
result[key] = items;
}
return;
}
result[key] = `${value}`;
});
return result;
}
function writeItemSettingsMap(section_id, settingsKey, map) {
const result = {};
Object.entries(map || {}).forEach(([item, settings]) => {
const compact = compactItemSettings(settings);
if (item && Object.keys(compact).length) {
result[item] = compact;
}
});
if (Object.keys(result).length) {
uci.set(UCI_PACKAGE, section_id, settingsKey, JSON.stringify(result));
} else {
uci.unset(UCI_PACKAGE, section_id, settingsKey);
}
}
function cleanupListItemSettings(section_id, settingsKey, values) {
if (!settingsKey) {
return;
}
const keep = new Set(normalizeDynamicListItems(values));
const map = readItemSettingsMap(section_id, settingsKey);
let changed = false;
Object.keys(map).forEach((item) => {
if (!keep.has(item)) {
delete map[item];
changed = true;
}
});
if (changed) {
writeItemSettingsMap(section_id, settingsKey, map);
}
}
function itemSettingsFlag(settings, key, defaultValue) {
const value = settings ? settings[key] : null;
if (value === undefined || value === null || value === "") {
return Boolean(defaultValue);
}
return value === true || `${value}` === "1";
}
function ensureConnectionsDynamicListStyles() {
if (document.getElementById(CONNECTIONS_DYNLIST_STYLE_ID)) {
return;
}
document.head.appendChild(
E(
"style",
{ id: CONNECTIONS_DYNLIST_STYLE_ID },
`
.fkp-connections-dynlist > .item {
--fkp-dynlist-action-width: 2em;
padding-right: calc(var(--fkp-dynlist-action-width) * 2);
position: relative;
}
.fkp-connections-dynlist > .item > .fkp-dynlist-settings {
align-items: center;
border: 1px solid var(--border-color-high, currentColor);
border-right: 0;
border-radius: 0;
bottom: -1px;
color: inherit;
cursor: pointer;
display: inline-flex;
font: inherit;
font-size: 0.9em;
justify-content: center;
line-height: 1;
min-height: 0;
padding: 0;
pointer-events: auto;
position: absolute;
right: calc(var(--fkp-dynlist-action-width) - 1px);
user-select: none;
text-decoration: none;
top: -1px;
width: var(--fkp-dynlist-action-width);
z-index: 1;
}
.fkp-connections-dynlist > .item > .fkp-dynlist-settings:hover,
.fkp-connections-dynlist > .item > .fkp-dynlist-settings:focus {
--focus-color-rgb: 82, 168, 236;
outline: 0;
border-color: rgba(var(--focus-color-rgb), 0.8) !important;
box-shadow: inset 0 1px 3px hsla(var(--border-color-low-hsl), .01), 0 0 8px rgba(var(--focus-color-rgb), 0.6);
text-decoration: none;
}
.fkp-connections-dynlist > .add-item > .cbi-dropdown {
width: 100%;
}
.fkp-interface-dynlist-label {
align-items: center;
display: inline-flex;
gap: 0.25em;
max-width: 100%;
vertical-align: middle;
}
.fkp-interface-dynlist-label > img {
flex: 0 0 auto;
height: 1.35em;
width: auto;
}
.fkp-interface-dynlist-label > span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fkp-button-add-dynlist > .add-item {
align-items: stretch;
background: transparent;
border: 0;
border-radius: 0;
box-shadow: none;
display: flex;
margin-top: 4px;
max-width: 100%;
overflow: visible;
padding: 0;
width: var(--fkp-button-add-width, 210px);
}
.fkp-button-add-dynlist > .add-item > input[type="text"] {
display: none !important;
}
.fkp-button-add-dynlist > .add-item > .cbi-button-add {
align-items: center !important;
background: linear-gradient(var(--background-color-high) 0%, var(--border-color-low) 100%) !important;
border: 1px solid var(--border-color-high) !important;
border-radius: 3px !important;
box-shadow: inset 0 1px 3px hsla(var(--border-color-low-hsl), .01) !important;
box-sizing: border-box !important;
color: var(--text-color-medium) !important;
cursor: pointer !important;
display: flex !important;
font-size: 13px !important;
height: 30px !important;
justify-content: flex-start !important;
margin-left: 0 !important;
max-height: 30px !important;
min-height: 30px !important;
max-width: 100% !important;
overflow: hidden !important;
padding: 0 4px !important;
text-overflow: ellipsis !important;
transition: border linear .2s, box-shadow linear .2s !important;
white-space: nowrap !important;
width: 100% !important;
}
.fkp-button-add-dynlist > .add-item > .cbi-button-add:hover,
.fkp-button-add-dynlist > .add-item > .cbi-button-add:focus {
border-color: rgba(82, 168, 236, .8) !important;
box-shadow: inset 0 1px 3px hsla(var(--border-color-low-hsl), .01), 0 0 8px rgba(82, 168, 236, .6) !important;
outline: 0;
}
body.modal-overlay-active > #modal_overlay > .modal.cbi-modal > .cbi-map.flash {
animation: none !important;
}
`,
),
);
}
function findDynamicListItemByValue(dl, value) {
const stringValue = `${value}`;
const items = dl.querySelectorAll(".item");
for (let i = 0; i < items.length; i += 1) {
const hidden = items[i].querySelector('input[type="hidden"]');
if (
hidden &&
hidden.parentNode === items[i] &&
hidden.value === stringValue
) {
return items[i];
}
}
return null;
}
function dynamicListItemCurrentValue(item, fallback) {
const hidden = item ? item.querySelector('input[type="hidden"]') : null;
return hidden && hidden.parentNode === item ? hidden.value : fallback;
}
function dynamicListItemValues(dl) {
return Array.from(dl.querySelectorAll(".item"))
.map((item) => {
const hidden = item.querySelector('input[type="hidden"]');
return hidden && hidden.parentNode === item ? hidden.value : null;
})
.filter((value) => value != null);
}
function updateDynamicListItemLabel(item, label) {
const labelNode = item ? item.querySelector(".fkp-dynlist-label") : null;
if (labelNode) {
labelNode.textContent = `${label || ""}`;
}
}
function addDynamicListItem(widget, value, text) {
if (!widget || !widget.node) {
return null;
}
const normalized = `${value || ""}`.trim();
if (!normalized) {
return null;
}
if (
typeof widget.options.hasEquivalentValue === "function" &&
widget.options.hasEquivalentValue(normalized, widget.node)
) {
widget.dispatchCbiDynlistChange(widget.node, normalized);
return null;
}
widget.addItem(widget.node, normalized, text || null, false);
widget.dispatchCbiDynlistChange(widget.node, normalized);
updateButtonAddDynamicListLayout(widget.node, widget.options.addButtonLabel);
return findDynamicListItemByValue(widget.node, normalized);
}
function updateButtonAddDynamicListLayout(dl, label) {
const addButton = dl ? dl.querySelector(".add-item > .cbi-button-add") : null;
if (!addButton) {
return;
}
addButton.textContent = label || "+";
addButton.setAttribute("role", "button");
addButton.setAttribute("tabindex", "0");
dl.style.setProperty(
"--fkp-button-add-width",
dl.querySelector(".item") ? "100%" : "210px",
);
}
function setDynamicListItemValue(item, value) {
const hidden = item ? item.querySelector('input[type="hidden"]') : null;
if (hidden && hidden.parentNode === item) {
hidden.value = `${value || ""}`;
}
}
function parseOutboundJsonObject(value) {
try {
const parsed = JSON.parse(`${value || ""}`);
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
? parsed
: null;
} catch (_error) {
return null;
}
}
function outboundJsonDisplayTag(value) {
const parsed = parseOutboundJsonObject(value);
const tag = parsed && typeof parsed.tag === "string" ? parsed.tag.trim() : "";
return tag || "";
}
function outboundJsonListItemLabel(value) {
return E(
"span",
{ class: "fkp-dynlist-label" },
outboundJsonDisplayTag(value) || _("JSON outbound"),
);
}
function cleanFormSectionData(sectionData) {
const result = {};
Object.entries(sectionData || {}).forEach(([key, value]) => {
if (!key || key.charAt(0) === ".") {
return;
}
result[key] = Array.isArray(value) ? value.slice() : value;
});
return result;
}
const SettingsUIDynamicList = ui.DynamicList.extend({
render() {
ensureConnectionsDynamicListStyles();
const node = ui.DynamicList.prototype.render.apply(this, arguments);
node.classList.add("fkp-connections-dynlist");
return node;
},
addItem(dl, value, text, flash) {
if (
flash &&
typeof this.options.hasEquivalentValue === "function" &&
this.options.hasEquivalentValue(value, dl)
) {
this.dispatchCbiDynlistChange(dl, value);
return;
}
const itemText =
typeof this.options.itemLabel === "function"
? this.options.itemLabel(value, text)
: text;
ui.DynamicList.prototype.addItem.call(this, dl, value, itemText, flash);
const item = findDynamicListItemByValue(dl, value);
const hasSettings =
typeof this.options.hasSettings === "function"
? this.options.hasSettings(value)
: true;
if (!item || item.querySelector(".fkp-dynlist-settings") || !hasSettings) {
return;
}
item.appendChild(
E(
"span",
{
role: "button",
tabindex: this.options.disabled ? null : "0",
class: "fkp-dynlist-settings",
"aria-label": _("Settings"),
"aria-disabled": this.options.disabled ? "true" : null,
click: (event) => {
event.preventDefault();
event.stopPropagation();
if (this.options.disabled) {
return;
}
if (typeof this.options.settingsHandler === "function") {
this.options.settingsHandler(
dynamicListItemCurrentValue(item, value),
item,
this,
{},
);
}
},
keydown: (event) => {
if (event.key !== "Enter" && event.key !== " ") {
return;
}
event.preventDefault();
event.stopPropagation();
if (
!this.options.disabled &&
typeof this.options.settingsHandler === "function"
) {
this.options.settingsHandler(
dynamicListItemCurrentValue(item, value),
item,
this,
{},
);
}
},
},
"\u2699",
),
);
},
handleClick(event) {
if (event.target.closest(".fkp-dynlist-settings")) {
return;
}
return ui.DynamicList.prototype.handleClick.apply(this, arguments);
},
});
const ButtonAddSettingsUIDynamicList = SettingsUIDynamicList.extend({
render() {
const node = SettingsUIDynamicList.prototype.render.apply(this, arguments);
node.classList.add("fkp-button-add-dynlist");
const input = node.querySelector('.add-item > input[type="text"]');
if (input) {
input.setAttribute("aria-hidden", "true");
input.setAttribute("tabindex", "-1");
}
updateButtonAddDynamicListLayout(node, this.options.addButtonLabel);
node.addEventListener("cbi-dynlist-change", () => {
updateButtonAddDynamicListLayout(node, this.options.addButtonLabel);
});
return node;
},
addButtonItem() {
if (typeof this.options.settingsHandler === "function") {
this.options.settingsHandler("", null, this, { adding: true });
}
},
handleClick(event) {
if (
!this.options.disabled &&
event.target.closest(".add-item > .cbi-button-add")
) {
event.preventDefault();
event.stopPropagation();
this.addButtonItem(event.currentTarget);
return;
}
return SettingsUIDynamicList.prototype.handleClick.apply(this, arguments);
},
handleKeydown(event) {
if (
!this.options.disabled &&
event.target.closest(".add-item > .cbi-button-add") &&
(event.key === "Enter" || event.key === " ")
) {
event.preventDefault();
event.stopPropagation();
this.addButtonItem(event.currentTarget);
return;
}
return ui.DynamicList.prototype.handleKeydown.apply(this, arguments);
},
});
const SettingsDynamicList = form.DynamicList.extend({
childOwner(section_id) {
return typeof this.childOwnerId === "function"
? `${this.childOwnerId(section_id) || ""}`.trim()
: section_id;
},
parentSection(section_id) {
return typeof this.parentSectionId === "function"
? `${this.parentSectionId(section_id) || ""}`.trim()
: section_id;
},
load(section_id) {
if (this.childType) {
return getChildItemIds(
this.childOwner(section_id),
this.childType,
this.ownerOption,
);
}
return form.DynamicList.prototype.load.apply(this, arguments);
},
renderWidget(section_id, _option_index, cfgvalue) {
const value = cfgvalue != null ? cfgvalue : this.default;
const choices = this.transformChoices();
const WidgetClass = this.buttonAdd
? ButtonAddSettingsUIDynamicList
: SettingsUIDynamicList;
const widget = new WidgetClass(L.toArray(value), choices, {
id: this.cbid(section_id),
sort: this.keylist,
allowduplicates: this.allowduplicates,
optional: this.optional || this.rmempty,
datatype: this.datatype,
placeholder: this.placeholder,
validate: L.bind(this.validate, this, section_id),
disabled: this.readonly != null ? this.readonly : this.map.readonly,
addButtonLabel:
typeof this.addButtonLabel === "function"
? this.addButtonLabel(section_id)
: this.addButtonLabel,
settingsHandler: (itemValue, _item, widget, context) => {
if (typeof this.renderItemSettingsModal === "function") {
const ownerId = this.childOwner(section_id);
this.renderItemSettingsModal(
ownerId,
`${itemValue}`,
this,
widget,
_item,
Object.assign({}, context || {}, {
parentSectionId: this.parentSection(section_id),
ownerId,
}),
);
}
},
itemLabel: (itemValue, text) => {
if (typeof this.renderListItemLabel === "function") {
return this.renderListItemLabel(
this.childOwner(section_id),
`${itemValue}`,
text,
);
}
return text;
},
hasSettings: (itemValue) => {
if (typeof this.hasItemSettings === "function") {
return this.hasItemSettings(
this.childOwner(section_id),
`${itemValue}`,
);
}
if (this.childType) {
return isExistingChildItem(
this.childOwner(section_id),
`${itemValue}`,
this.childType,
this.ownerOption,
);
}
return true;
},
hasEquivalentValue: (itemValue, dl) => {
const inputValueForItem = (value) => {
const ownerId = this.childOwner(section_id);
if (typeof this.inputValueForItem === "function") {
return `${this.inputValueForItem(ownerId, `${value || ""}`) || ""}`.trim();
}
if (this.childType && this.childValueOption) {
return childItemInputValue(
ownerId,
`${value || ""}`,
this.childType,
this.childValueOption,
this.ownerOption,
);
}
return `${value || ""}`.trim();
};
const normalized = inputValueForItem(itemValue);
return Boolean(
normalized &&
dynamicListItemValues(dl).some(
(existingValue) =>
inputValueForItem(existingValue) === normalized,
),
);
},
});
const node = widget.render();
if (typeof this.onListChange === "function") {
node.addEventListener("cbi-dynlist-change", () => {
this.onListChange(section_id);
});
}
return node;
},
parse(section_id) {
if (
this.isActive(section_id) &&
typeof this.validateItemsOnSave === "function"
) {
const result = this.validateItemsOnSave(
this.childType ? this.childOwner(section_id) : section_id,
this.formvalue(section_id),
this,
section_id,
);
if (result !== true) {
const title = this.stripTags(this.title).trim();
return Promise.reject(
new TypeError(
`${_('Option "%s" contains an invalid input value.').format(title || this.option)} ${result}`,
),
);
}
}
return form.DynamicList.prototype.parse.apply(this, arguments);
},
write(section_id, value) {
if (this.childType) {
const ownerId = this.childOwner(section_id);
const itemIds = materializeChildItems(
ownerId,
{
typeName: this.childType,
valueOption: this.childValueOption,
ownerOption: this.ownerOption,
createId: this.createId,
defaults: this.childDefaults,
stagedSettings:
typeof this.stagedChildSettings === "function"
? (itemValue, itemId, created) =>
this.stagedChildSettings(ownerId, itemValue, itemId, created)
: null,
},
value,
);
cleanupRemovedChildItems(
ownerId,
this.childType,
itemIds,
this.ownerOption,
);
if (typeof this.afterMaterializeChildItems === "function") {
this.afterMaterializeChildItems(ownerId, itemIds);
}
uci.unset(UCI_PACKAGE, section_id, this.option);
cleanupListItemSettings(ownerId, this.settingsKey, itemIds);
if (typeof this.clearStagedChildSettings === "function") {
this.clearStagedChildSettings(ownerId);
}
return;
}
const result = form.DynamicList.prototype.write.apply(this, arguments);
cleanupListItemSettings(section_id, this.settingsKey, value);
return result;
},
remove(section_id) {
if (this.childType) {
cleanupRemovedChildItems(
this.childOwner(section_id),
this.childType,
[],
this.ownerOption,
);
uci.unset(UCI_PACKAGE, section_id, this.option);
return;
}
if (this.settingsKey) {
uci.unset(UCI_PACKAGE, section_id, this.settingsKey);
}
return form.DynamicList.prototype.remove.apply(this, arguments);
},
});
const ButtonAddSettingsDynamicList = SettingsDynamicList.extend({
buttonAdd: true,
});
function configureLiveDynamicListChoices(option, getChoices) {
option.renderWidget = function (section_id, _option_index, cfgvalue) {
const values = L.toArray(cfgvalue != null ? cfgvalue : this.default);
const choices = getChoices(section_id, values);
const labels = {};
choices.forEach((choice) => {
labels[choice.value] = choice.label;
});
refreshOptionChoices(this, choices);
let choiceSignature = JSON.stringify(
choices.map((choice) => [choice.value, choice.label]),
);
// form.DynamicList passes null when there are no choices, which permanently
// renders a plain input. An empty object keeps the stock combobox alive so
// later live choices can be added without reopening the modal.
const widget = new ui.DynamicList(values, labels, {
id: this.cbid(section_id),
sort: this.keylist,
allowduplicates: this.allowduplicates,
optional: this.optional || this.rmempty,
datatype: this.datatype,
placeholder: this.placeholder,
validate: L.bind(this.validate, this, section_id),
disabled: this.readonly != null ? this.readonly : this.map.readonly,
});
const node = widget.render();
const refreshChoices = () => {
if (!node.isConnected) {
return false;
}
const currentValues = widget.getValue();
const currentChoices = getChoices(section_id, currentValues);
const currentLabels = {};
currentChoices.forEach((choice) => {
currentLabels[choice.value] = choice.label;
});
const currentSignature = JSON.stringify(
currentChoices.map((choice) => [choice.value, choice.label]),
);
if (currentSignature === choiceSignature) {
return;
}
choiceSignature = currentSignature;
refreshOptionChoices(this, currentChoices);
widget.choices = currentLabels;
widget.clearChoices();
widget.addChoices(
currentChoices.map((choice) => choice.value),
currentLabels,
);
return true;
};
const refreshBeforeOpening = (event) => {
if (event.target && event.target.closest(".add-item")) {
refreshChoices();
}
};
node.addEventListener("mousedown", refreshBeforeOpening, true);
node.addEventListener("focusin", refreshBeforeOpening, true);
if (!dashboardFilterChoiceRefreshers.has(section_id)) {
dashboardFilterChoiceRefreshers.set(section_id, new Set());
}
dashboardFilterChoiceRefreshers.get(section_id).add(refreshChoices);
return node;
};
}
function countryChoices() {
return COUNTRY_CODES.map((code) => ({
value: code,
label: getCountryOptionLabel(code),
})).sort((a, b) => a.label.localeCompare(b.label));
}
function currentOutboundNameChoices(section_id, values) {
if (!outboundNameChoicesCache.has(section_id)) {
loadOutboundNameChoices(section_id);
}
const seen = new Set();
const result = [];
const append = (name) => {
const value = `${name || ""}`.trim();
if (!value || seen.has(value)) {
return;
}
seen.add(value);
result.push({ value, label: value });
};
(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((itemId) => {
const normalized = childItemInputValue(
section_id,
itemId,
"section_interface",
"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 currentSectionGroupValues(section_id, typeName) {
const liveValues = currentLiveDynamicListValues(section_id, typeName);
if (liveValues != null) {
return liveValues;
}
const option = sectionGroupSourceOptions.get(typeName);
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 child sections when the widget is not mounted.
}
}
return getChildItemIds(section_id, typeName);
}
function sectionGroupDisplayName(section_id, typeName, value) {
const itemId = `${value || ""}`.trim();
if (isExistingChildItem(section_id, itemId, typeName)) {
return `${uci.get(UCI_PACKAGE, itemId, "name") || itemId}`.trim();
}
const option = sectionGroupSourceOptions.get(typeName);
const pending =
option && option.pendingChildSettings
? option.pendingChildSettings[section_id]
: null;
const settings = pending && pending[itemId];
return `${(settings && settings.name) || itemId}`.trim();
}
function currentSectionGroupChoices(section_id, selectedValues = []) {
const seen = new Set();
const result = [];
const append = (typeName, value) => {
value = `${value || ""}`.trim();
if (!value) {
return;
}
const name = sectionGroupDisplayName(section_id, typeName, value) || value;
if (seen.has(name)) {
return;
}
seen.add(name);
result.push({ value: name, label: name });
};
currentSectionGroupValues(section_id, "urltest").forEach((value) =>
append("urltest", value),
);
currentSectionGroupValues(section_id, "priority_group").forEach((value) =>
append("priority_group", value),
);
normalizeDynamicListItems(selectedValues).forEach((value) => {
value = `${value || ""}`.trim();
if (value && !seen.has(value)) {
seen.add(value);
result.push({ value, label: value });
}
});
return result.sort((left, right) => left.label.localeCompare(right.label));
}
function refreshDashboardFilterChoiceWidgets(section_id) {
const refreshers = dashboardFilterChoiceRefreshers.get(section_id);
if (!refreshers) {
return;
}
refreshers.forEach((refresh) => {
if (refresh() === false) {
refreshers.delete(refresh);
}
});
if (refreshers.size === 0) {
dashboardFilterChoiceRefreshers.delete(section_id);
}
}
function isDownloadThroughTargetSection(section, currentSectionId) {
const sectionName = getUciSectionName(section);
const action = (section && section.action) || "";
if (
!sectionName ||
sectionName === currentSectionId ||
section.enabled === "0"
) {
return false;
}
if (["connection", "proxy", "outbound", "vpn"].includes(action)) {
return true;
}
if (action === "zapret") {
return isZapretInstalledForUi();
}
if (action === "zapret2") {
return isZapret2InstalledForUi();
}
if (action === "byedpi") {
return isByedpiInstalledForUi();
}
return false;
}
function subscriptionDownloadTargetChoices(section_id) {
return (uci.sections(UCI_PACKAGE, "section") || [])
.filter((section) => isDownloadThroughTargetSection(section, section_id))
.map((section) => ({
value: getUciSectionName(section),
label: getUciSectionLabel(section),
}));
}
function dnsTypeChoices() {
return [
{ value: "doh", label: _("DNS over HTTPS (DoH)") },
{ value: "dot", label: _("DNS over TLS (DoT)") },
{ value: "udp", label: "UDP" },
];
}
function isConnectionNetworkInterfaceAllowed(deviceName, device) {
if (CONNECTIONS_BLOCKED_INTERFACES.includes(deviceName)) {
return false;
}
if (!device) {
return true;
}
const type = device.getType();
const isWireless =
type === "wifi" || type === "wireless" || type.indexOf("wlan") >= 0;
return !isWireless;
}
function renderNetworkInterfaceChoice(device) {
const name = device.getName();
const type = device.getType();
return E([
E("img", {
title: device.getI18n(),
src: L.resource(
"icons/%s%s.svg".format(type, device.isUp() ? "" : "_disabled"),
),
}),
E("span", { class: "hide-open" }, [name]),
E("span", { class: "hide-close" }, [device.getI18n()]),
]);
}
function renderNetworkInterfaceListItem(device, fallbackName) {
const name = device ? device.getName() : fallbackName;
const type = device ? device.getType() : "ethernet";
const up = device ? device.isUp() : false;
return E("span", { class: "fkp-interface-dynlist-label" }, [
E("img", {
title: device ? device.getI18n() : _("Network Interface"),
src: L.resource("icons/%s%s.svg".format(type, up ? "" : "_disabled")),
}),
E("span", {}, [name]),
]);
}
function refreshNetworkInterfaceOptionValues(option) {
option.keylist = [];
option.vallist = [];
option.interfaceChoiceMap = {};
option.interfaceDeviceMap = {};
(option.devices || []).forEach((device) => {
const name = device.getName();
const type = device.getType();
if (
name === "lo" ||
type === "alias" ||
!isConnectionNetworkInterfaceAllowed(name, device)
) {
return;
}
option.value(name, renderNetworkInterfaceChoice(device));
option.interfaceChoiceMap[name] = true;
option.interfaceDeviceMap[name] = device;
});
}
const InterfaceSettingsDynamicList = SettingsDynamicList.extend({
load(section_id) {
return network.getDevices().then(
L.bind(function (devices) {
this.devices = devices || [];
refreshNetworkInterfaceOptionValues(this);
return this.super("load", section_id);
}, this),
);
},
validate(section_id, value) {
value = childItemInputValue(section_id, value, "section_interface", "name");
if (!value || value.length === 0) {
return true;
}
if (!this.interfaceChoiceMap || !this.interfaceChoiceMap[value]) {
return _("Select an existing network interface");
}
return true;
},
renderListItemLabel(section_id, value, text) {
value = childItemInputValue(section_id, value, "section_interface", "name");
return renderNetworkInterfaceListItem(
this.interfaceDeviceMap ? this.interfaceDeviceMap[value] : null,
value || text,
);
},
});
function urlTestFilterModeChoices() {
return [
{ value: "disabled", label: _("All servers") },
{ value: "exclude", label: _("All except selected") },
{ value: "include", label: _("Only selected") },
{ value: "mixed", label: _("Only selected except exclusions") },
];
}
function priorityLevelFilterModeChoices() {
return [
{ value: "disabled", label: _("All remaining servers") },
{ value: "include", label: _("Only selected") },
{ value: "exclude", label: _("All remaining except selected") },
{ value: "mixed", label: _("Only selected except exclusions") },
];
}
function serverCountryDetectionChoices() {
return [
{ value: "flag_emoji", label: _("By flag emoji from name") },
{ value: "country_is", label: _("Via country.is") },
];
}
function proxyProtocolChoices() {
return [
["vless", "VLESS"],
["vmess", "VMess"],
["trojan", "Trojan"],
["shadowsocks", "Shadowsocks"],
["socks", "SOCKS"],
["http", "HTTP"],
["hysteria2", "Hysteria2"],
["direct", "Direct"],
];
}
function proxyTransportChoices() {
return [
["tcp", "TCP"],
["ws", "WebSocket"],
["grpc", "gRPC"],
["http", "HTTP"],
["httpupgrade", "HTTPUpgrade"],
["xhttp", "XHTTP"],
];
}
function proxySecurityChoices() {
return [
["none", "None"],
["tls", "TLS"],
["reality", "Reality"],
];
}
function addProxyParameterFilterOptions(itemSection, options) {
const prefix = options.prefix;
const dependencies = options.dependencies;
let o = itemSection.option(
form.Flag,
`${prefix}_proxy_parameters`,
options.toggleLabel,
options.toggleDescription,
);
dependencies.forEach((dependency) => o.depends(dependency));
o.default = "0";
o.rmempty = false;
[
[
"protocols",
_("Protocol"),
options.protocolDescription,
proxyProtocolChoices(),
],
[
"transports",
_("Transport"),
options.transportDescription,
proxyTransportChoices(),
],
[
"securities",
_("Security"),
options.securityDescription,
proxySecurityChoices(),
],
].forEach(([suffix, label, description, choices]) => {
const list = itemSection.option(
form.DynamicList,
`${prefix}_${suffix}`,
label,
description,
);
dependencies.forEach((dependency) =>
list.depends(
Object.assign({}, dependency, {
[`${prefix}_proxy_parameters`]: "1",
}),
),
);
list.rmempty = true;
choices.forEach(([value, choiceLabel]) => list.value(value, choiceLabel));
list.placeholder = _("-- Select --");
});
}
function urlTestUrlChoices() {
return Array.isArray(main.LATENCY_TEST_URL_OPTIONS)
? main.LATENCY_TEST_URL_OPTIONS
: [main.DEFAULT_LATENCY_TEST_URL || "https://www.gstatic.com/generate_204"];
}
function validateUrlTestTolerance(value) {
if (!value || `${value}`.length === 0) {
return _("Must be a number in the range of 0 - 10000");
}
const normalized = `${value}`;
const parsed = parseFloat(normalized);
if (
/^[0-9]+$/.test(normalized) &&
!isNaN(parsed) &&
isFinite(parsed) &&
parsed >= 0 &&
parsed <= 10000
) {
return true;
}
return _("Must be a number in the range of 0 - 10000");
}
function validateUrlTestUrl(value) {
const validation = main.validateUrl(`${value || ""}`.trim());
return validation.valid ? true : validation.message;
}
function optionMapValue(option, section_id, key) {
const value =
option && option.map && option.map.data
? option.map.data.get(option.map.config, section_id, key)
: uci.get(UCI_PACKAGE, section_id, key);
return value == null ? "" : value;
}
function subscriptionUrlSettingsKeys() {
return [
"subscription_update_enabled",
"subscription_update_interval",
"download_via_proxy_enabled",
"download_via_proxy_section",
"auto_user_agent",
"user_agent",
"auto_hwid",
"hwid",
"show_dashboard_metadata",
"prefix_nodes",
"node_prefix",
"include_urltest_groups",
"hide_urltest_group_outbounds",
"hide_detour_outbounds",
];
}
function defaultSubscriptionUrlSettings() {
return {
subscription_update_enabled: "1",
subscription_update_interval: "1h",
download_via_proxy_enabled: "0",
download_via_proxy_section: "",
auto_user_agent: "1",
user_agent: "",
auto_hwid: "1",
hwid: "",
show_dashboard_metadata: "1",
prefix_nodes: "0",
node_prefix: "",
include_urltest_groups: "1",
hide_urltest_group_outbounds: "1",
hide_detour_outbounds: "1",
};
}
function interfaceSettingsKeys() {
return [
"domain_resolver_enabled",
"domain_resolver_dns_type",
"domain_resolver_dns_server",
];
}
function defaultInterfaceSettings() {
return {
domain_resolver_enabled: "0",
domain_resolver_dns_type: "udp",
domain_resolver_dns_server: "8.8.8.8",
};
}
function subscriptionUserAgentChoices() {
return [
"sing-box",
"Happ",
"v2rayN",
"v2rayNG",
"v2RayTun",
"Incy",
"Hiddify",
"HiddifyNext",
"Clash",
"Clash.Meta",
"ClashMetaForAndroid",
"Mihomo",
"NekoBox",
"Karing",
"Husi",
];
}
function urlTestSettingsKeys() {
return [
"name",
"check_interval",
"tolerance",
"testing_url",
"idle_timeout",
"interrupt_exist_connections",
"pin_dashboard",
"filter_mode",
"detect_server_country",
"include_countries",
"include_outbounds",
"include_regex",
"include_proxy_parameters",
"include_protocols",
"include_transports",
"include_securities",
"exclude_countries",
"exclude_outbounds",
"exclude_regex",
"exclude_proxy_parameters",
"exclude_protocols",
"exclude_transports",
"exclude_securities",
];
}
function defaultUrlTestSettings(name) {
return {
name: "",
check_interval: "3m",
tolerance: "50",
testing_url: "https://www.gstatic.com/generate_204",
idle_timeout: "30m",
interrupt_exist_connections: "1",
pin_dashboard: "1",
filter_mode: "disabled",
detect_server_country: "flag_emoji",
};
}
function urlTestChildDefaults() {
return {
check_interval: "3m",
tolerance: "50",
testing_url: "https://www.gstatic.com/generate_204",
idle_timeout: "30m",
interrupt_exist_connections: "1",
pin_dashboard: "1",
filter_mode: "disabled",
detect_server_country: "flag_emoji",
};
}
function priorityGroupSettingsKeys() {
return [
"name",
"health_url",
"active_check_interval",
"check_timeout",
"recovery_check_interval",
"pick_fastest",
"switch_to_faster_same_priority",
"fastest_check_interval",
"interrupt_exist_connections",
"pin_dashboard",
];
}
function defaultPriorityGroupSettings() {
return {
name: "",
health_url: "https://www.gstatic.com/generate_204",
active_check_interval: "5s",
check_timeout: "2s",
recovery_check_interval: "15s",
pick_fastest: "0",
switch_to_faster_same_priority: "0",
fastest_check_interval: "3m",
interrupt_exist_connections: "1",
pin_dashboard: "1",
};
}
function priorityGroupChildDefaults() {
return {
health_url: "https://www.gstatic.com/generate_204",
active_check_interval: "5s",
check_timeout: "2s",
recovery_check_interval: "15s",
pick_fastest: "0",
switch_to_faster_same_priority: "0",
fastest_check_interval: "3m",
interrupt_exist_connections: "1",
pin_dashboard: "1",
};
}
function priorityLevelSettingsKeys() {
return [
"name",
"order",
"direct",
"filter_mode",
"detect_server_country",
"country",
"server_name",
"regex",
"include_proxy_parameters",
"include_protocols",
"include_transports",
"include_securities",
"exclude_countries",
"exclude_outbounds",
"exclude_regex",
"exclude_proxy_parameters",
"exclude_protocols",
"exclude_transports",
"exclude_securities",
];
}
function defaultPriorityLevelSettings() {
return {
name: "",
order: "0",
direct: "0",
filter_mode: "include",
detect_server_country: "flag_emoji",
};
}
function randomPriorityGroupId() {
for (let i = 0; i < 100; i += 1) {
const value = Math.floor(Math.random() * 0xffffffff)
.toString(16)
.padStart(8, "0");
const id = `pg_${value}`;
if (!uci.get(UCI_PACKAGE, id)) {
return id;
}
}
return `pg_${Date.now().toString(16)}`;
}
function addSubscriptionUrlItemOptions(itemSection, options = {}) {
const parentSectionForItem =
typeof options.parentSectionId === "function"
? options.parentSectionId
: parentSectionIdForItem;
let o = itemSection.option(
form.Flag,
"subscription_update_enabled",
_("Subscription auto update"),
_("Update this subscription automatically"),
);
o.default = "1";
o.rmempty = false;
o = itemSection.option(
form.Value,
"subscription_update_interval",
_("Subscription update interval"),
_("Use sing-box duration format like 1d, 12h or 30m"),
);
o.depends("subscription_update_enabled", "1");
o.placeholder = "1h";
o.validate = function (itemId, value) {
return optionMapValue(this, itemId, "subscription_update_enabled") === "1"
? validateRequiredSingBoxDuration(value)
: validateOptionalSingBoxDuration(value);
};
o = itemSection.option(
form.Flag,
"download_via_proxy_enabled",
_("Download subscription through a section"),
_("Download subscriptions via the selected section"),
);
o.default = "0";
o.rmempty = false;
o = itemSection.option(
form.ListValue,
"download_via_proxy_section",
_("Download through"),
);
o.depends("download_via_proxy_enabled", "1");
o.load = function (itemId) {
const sectionId = parentSectionForItem(itemId);
refreshOptionChoices(this, subscriptionDownloadTargetChoices(sectionId));
return optionMapValue(this, itemId, "download_via_proxy_section") || "";
};
o.validate = function (itemId, value) {
const sectionId = parentSectionForItem(itemId);
if (optionMapValue(this, itemId, "download_via_proxy_enabled") !== "1") {
return true;
}
if (!value) {
return _("Select a section for downloading this subscription");
}
if (value === sectionId) {
return _("Current section cannot download its own subscription");
}
return true;
};
o = itemSection.option(
form.Flag,
"auto_user_agent",
_("Automatic User-Agent selection"),
_(
"Try compatible User-Agent profiles automatically when downloading this subscription",
),
);
o.default = "1";
o.rmempty = false;
o = itemSection.option(
form.Value,
"user_agent",
_("User-Agent"),
_("Select a common client profile or enter a custom User-Agent"),
);
o.depends("auto_user_agent", "0");
o.placeholder = _("-- Select --");
o.rmempty = false;
subscriptionUserAgentChoices().forEach((choice) => o.value(choice));
o.load = function (itemId) {
return optionMapValue(this, itemId, "user_agent") || "";
};
o.validate = function (itemId, value) {
if (optionMapValue(this, itemId, "auto_user_agent") !== "0") {
return true;
}
return `${value || ""}`.trim() ? true : _("Select or enter a User-Agent");
};
o = itemSection.option(
form.Flag,
"auto_hwid",
_("Auto-generate HWID"),
_("Generate HWID from router hardware information for this subscription"),
);
o.default = "1";
o.rmempty = false;
o = itemSection.option(
form.Value,
"hwid",
_("HWID"),
_("Enter the HWID sent with subscription requests"),
);
o.depends("auto_hwid", "0");
o.rmempty = false;
o.validate = function (itemId, value) {
if (optionMapValue(this, itemId, "auto_hwid") !== "0") {
return true;
}
return `${value || ""}`.trim() ? true : _("Enter HWID");
};
o = itemSection.option(
form.Flag,
"show_dashboard_metadata",
_("Show metadata on dashboard"),
_("Show subscription metadata for this source on the dashboard"),
);
o.default = "1";
o.rmempty = false;
o = itemSection.option(
form.Flag,
"prefix_nodes",
_("Add prefix to nodes"),
_(
"Automatically add text to the name of each server from this subscription for convenient filtering.",
),
);
o.default = "0";
o.rmempty = false;
o = itemSection.option(form.Value, "node_prefix", _("Prefix text"));
o.depends("prefix_nodes", "1");
o.rmempty = false;
o = itemSection.option(
form.Flag,
"include_urltest_groups",
_("Import subscription URLTest groups"),
_("Import URLTest groups returned by this subscription provider"),
);
o.default = "1";
o.rmempty = false;
o = itemSection.option(
form.Flag,
"hide_urltest_group_outbounds",
_("Hide URLTest group nodes"),
_(
"Hide individual nodes that are already included in imported subscription URLTest groups",
),
);
o.depends("include_urltest_groups", "1");
o.default = "1";
o.rmempty = false;
o = itemSection.option(
form.Flag,
"hide_detour_outbounds",
_("Hide cascade connection nodes"),
_("Hide intermediate nodes used as detours by other subscription nodes"),
);
o.default = "1";
o.rmempty = false;
}
function addInterfaceItemOptions(itemSection) {
let o = itemSection.option(
form.Flag,
"domain_resolver_enabled",
_("Domain Resolver"),
_("Enable built-in DNS resolver for domains handled by this section"),
);
o.default = "0";
o.rmempty = false;
o = itemSection.option(
form.ListValue,
"domain_resolver_dns_type",
_("DNS protocol"),
_("DNS protocol used by the resolver"),
);
o.depends("domain_resolver_enabled", "1");
dnsTypeChoices().forEach((choice) => o.value(choice.value, choice.label));
o.default = "udp";
o = itemSection.option(
form.Value,
"domain_resolver_dns_server",
_("DNS server"),
_("DNS server used by the resolver"),
);
o.depends("domain_resolver_enabled", "1");
o.default = "8.8.8.8";
o.validate = function (itemId, value) {
if (optionMapValue(this, itemId, "domain_resolver_enabled") !== "1") {
return true;
}
const validation = main.validateDNS(value);
return validation.valid ? true : validation.message;
};
}
function addUrlTestItemOptions(itemSection, options = {}) {
const parentSectionForItem =
typeof options.parentSectionId === "function"
? options.parentSectionId
: parentSectionIdForItem;
let o = itemSection.option(
form.Value,
"name",
_("Display name"),
_("Name displayed on the dashboard"),
);
o.rmempty = false;
o.load = function (itemId) {
return (
optionMapValue(this, itemId, "name") ||
optionMapValue(this, itemId, "display_name") ||
""
);
};
o.validate = function (_itemId, value) {
return `${value || ""}`.trim() ? true : _("Enter a display name");
};
o = itemSection.option(
form.Value,
"check_interval",
_("Check interval"),
_("Use sing-box duration format like 1d, 12h or 30m"),
);
o.default = "3m";
o.rmempty = false;
o.validate = function (_itemId, value) {
return validateRequiredSingBoxDuration(value);
};
o = itemSection.option(
form.Value,
"tolerance",
_("Tolerance"),
_(
"Minimum latency difference in milliseconds that triggers switching to a faster server.",
),
);
o.default = "50";
o.rmempty = false;
o.validate = function (_itemId, value) {
return validateUrlTestTolerance(value);
};
o = itemSection.option(
form.Value,
"testing_url",
_("Check URL"),
_("URL used to test server latency"),
);
o.default = "https://www.gstatic.com/generate_204";
o.rmempty = false;
urlTestUrlChoices().forEach((value) => o.value(value));
o.validate = function (_itemId, value) {
return validateUrlTestUrl(value);
};
o = itemSection.option(
form.Value,
"idle_timeout",
_("Idle timeout"),
_(
"Stop checking when URLTest group is not used. Use sing-box duration format like 1d, 12h or 30m.",
),
);
o.default = "30m";
o.rmempty = false;
o.validate = function (_itemId, value) {
return validateRequiredSingBoxDuration(value);
};
o = itemSection.option(
form.Flag,
"interrupt_exist_connections",
_("Interrupt connections"),
_("Interrupt connections when URLTest switches the selected server"),
);
o.default = "1";
o.rmempty = false;
o = itemSection.option(
form.Flag,
"pin_dashboard",
_("Pin on dashboard"),
_("Keep URLTest before latency-sorted servers"),
);
o.default = "1";
o.rmempty = false;
o = itemSection.option(
form.ListValue,
"filter_mode",
_("Server filtering"),
_("Allows limiting the list of servers for URLTest"),
);
urlTestFilterModeChoices().forEach((choice) =>
o.value(choice.value, choice.label),
);
o.default = "disabled";
o = itemSection.option(
form.ListValue,
"detect_server_country",
_("Detect server country"),
);
o.depends("filter_mode", "exclude");
o.depends("filter_mode", "include");
o.depends("filter_mode", "mixed");
serverCountryDetectionChoices().forEach((choice) =>
o.value(choice.value, choice.label),
);
o.default = "flag_emoji";
const includeProxyParameterOptions = {
prefix: "include",
toggleLabel: _("Include by proxy parameters"),
toggleDescription: _(
"Additionally filter servers by protocol, transport, and security. Add only servers matching the specified parameters.",
),
dependencies: [{ filter_mode: "include" }, { filter_mode: "mixed" }],
protocolDescription: _(
"Test only servers with one of the selected protocols.",
),
transportDescription: _(
"Test only servers with one of the selected transports.",
),
securityDescription: _(
"Test only servers with one of the selected security types.",
),
};
const excludeProxyParameterOptions = {
prefix: "exclude",
toggleLabel: _("Exclude by proxy parameters"),
toggleDescription: _(
"Additionally exclude servers by protocol, transport, and security. Exclude only servers matching the specified parameters.",
),
dependencies: [{ filter_mode: "exclude" }, { filter_mode: "mixed" }],
protocolDescription: _(
"Do not test servers with one of the selected protocols.",
),
transportDescription: _(
"Do not test servers with one of the selected transports.",
),
securityDescription: _(
"Do not test servers with one of the selected security types.",
),
};
[
[
"include_countries",
_("Include countries"),
_("Test servers only from the specified countries."),
countryChoices(),
validateCountryCode,
["include", "mixed"],
],
[
"include_outbounds",
_("Include servers"),
_("Test only selected servers."),
null,
null,
["include", "mixed"],
],
[
"include_regex",
_("Include by regular expression"),
_("Test servers whose names match the expression."),
null,
validateRegex,
["include", "mixed"],
],
[
"exclude_countries",
_("Exclude countries"),
_("Do not test servers from these countries."),
countryChoices(),
validateCountryCode,
["exclude", "mixed"],
],
[
"exclude_outbounds",
_("Exclude servers"),
_("Do not test specified servers."),
null,
null,
["exclude", "mixed"],
],
[
"exclude_regex",
_("Exclude by regular expression"),
_("Do not test servers whose names match the expression."),
null,
validateRegex,
["exclude", "mixed"],
],
].forEach(([key, label, description, choices, validator, modes]) => {
const list = itemSection.option(form.DynamicList, key, label, description);
modes.forEach((mode) => list.depends("filter_mode", mode));
list.rmempty = true;
if (choices) {
choices.forEach((choice) => list.value(choice.value, choice.label));
list.placeholder = _("-- Select --");
}
if (key.endsWith("_outbounds")) {
list.load = function (itemId) {
const sectionId = parentSectionForItem(itemId);
const values = normalizeOptionValues(optionMapValue(this, itemId, key));
return loadOutboundNameChoices(sectionId).then(() => {
refreshOptionChoices(
this,
currentOutboundNameChoices(sectionId, values),
);
return values;
});
};
list.placeholder = _("-- Select --");
configureLiveDynamicListChoices(list, (itemId, values) =>
currentOutboundNameChoices(parentSectionForItem(itemId), values),
);
}
if (validator) {
list.validate = function (_itemId, value) {
return validator(null, value);
};
}
if (key === "include_regex") {
addProxyParameterFilterOptions(itemSection, includeProxyParameterOptions);
} else if (key === "exclude_regex") {
addProxyParameterFilterOptions(itemSection, excludeProxyParameterOptions);
}
});
}
function priorityLevelSettingsForValidation(groupId, levelId, option) {
const store = childPendingSettingsStore(option, groupId);
if (isExistingChildItem(groupId, levelId, "priority_level", "group")) {
return readChildSettings(
levelId,
priorityLevelSettingsKeys(),
defaultPriorityLevelSettings(),
);
}
return Object.assign({}, store[levelId] || {});
}
function validatePriorityLevelItemsBeforeSave(groupId, values, option) {
const normalizedValues = normalizeDynamicListItems(values);
for (const value of normalizedValues) {
const levelId = `${value || ""}`;
const settings = priorityLevelSettingsForValidation(
groupId,
levelId,
option,
);
if (!`${settings.name || ""}`.trim()) {
return _("Enter a level name");
}
}
return true;
}
function addPriorityLevelItemOptions(itemSection, options = {}) {
const parentSectionForItem =
typeof options.parentSectionId === "function"
? options.parentSectionId
: parentSectionIdForItem;
let o = itemSection.option(
form.Value,
"name",
_("Level name"),
_("Name shown in the priority level list"),
);
o.rmempty = false;
o.validate = function (_itemId, value) {
return `${value || ""}`.trim() ? true : _("Enter a level name");
};
o = itemSection.option(
form.Flag,
"direct",
_("Direct connection"),
_("Traffic for this level goes directly."),
);
o.default = "0";
o.rmempty = false;
o = itemSection.option(
form.ListValue,
"filter_mode",
_("Server filtering"),
_(
"All remaining servers means every server not already assigned to a higher-priority level.",
),
);
priorityLevelFilterModeChoices().forEach((choice) =>
o.value(choice.value, choice.label),
);
o.default = "include";
o.depends("direct", "0");
o = itemSection.option(
form.ListValue,
"detect_server_country",
_("Detect server country"),
);
["exclude", "include", "mixed"].forEach((mode) =>
o.depends({ direct: "0", filter_mode: mode }),
);
serverCountryDetectionChoices().forEach((choice) =>
o.value(choice.value, choice.label),
);
o.default = "flag_emoji";
const includeProxyParameterOptions = {
prefix: "include",
toggleLabel: _("Include by proxy parameters"),
toggleDescription: _(
"Additionally filter servers by protocol, transport, and security. Add only servers matching the specified parameters.",
),
dependencies: [
{ direct: "0", filter_mode: "include" },
{ direct: "0", filter_mode: "mixed" },
],
protocolDescription: _("Only servers with one of the selected protocols."),
transportDescription: _(
"Only servers with one of the selected transports.",
),
securityDescription: _(
"Only servers with one of the selected security types.",
),
};
const excludeProxyParameterOptions = {
prefix: "exclude",
toggleLabel: _("Exclude by proxy parameters"),
toggleDescription: _(
"Additionally exclude servers by protocol, transport, and security. Exclude only servers matching the specified parameters.",
),
dependencies: [
{ direct: "0", filter_mode: "exclude" },
{ direct: "0", filter_mode: "mixed" },
],
protocolDescription: _(
"Exclude servers with one of the selected protocols from this level.",
),
transportDescription: _(
"Exclude servers with one of the selected transports from this level.",
),
securityDescription: _(
"Exclude servers with one of the selected security types from this level.",
),
};
[
[
"country",
_("Include countries"),
_("Only from the specified countries."),
countryChoices(),
validateCountryCode,
["include", "mixed"],
],
[
"server_name",
_("Include servers"),
_("Only the specified servers."),
null,
null,
["include", "mixed"],
],
[
"regex",
_("Include by regular expression"),
_("Only servers whose names match the expression."),
null,
validateRegex,
["include", "mixed"],
],
[
"exclude_countries",
_("Exclude countries"),
_("Remove servers from the specified countries from this level."),
countryChoices(),
validateCountryCode,
["exclude", "mixed"],
],
[
"exclude_outbounds",
_("Exclude servers"),
_("Remove the specified servers from this level."),
null,
null,
["exclude", "mixed"],
],
[
"exclude_regex",
_("Exclude by regular expression"),
_("Remove servers whose names match the expression from this level."),
null,
validateRegex,
["exclude", "mixed"],
],
].forEach(([key, label, description, choices, validator, modes]) => {
const list = itemSection.option(form.DynamicList, key, label, description);
modes.forEach((mode) => list.depends({ direct: "0", filter_mode: mode }));
list.rmempty = true;
if (choices) {
choices.forEach((choice) => list.value(choice.value, choice.label));
list.placeholder = _("-- Select --");
}
if (key === "server_name" || key === "exclude_outbounds") {
list.load = function (itemId) {
const sectionId = parentSectionForItem(itemId);
const values = normalizeOptionValues(optionMapValue(this, itemId, key));
return loadOutboundNameChoices(sectionId).then(() => {
refreshOptionChoices(
this,
currentOutboundNameChoices(sectionId, values),
);
return values;
});
};
list.placeholder = _("-- Select --");
configureLiveDynamicListChoices(list, (itemId, values) =>
currentOutboundNameChoices(parentSectionForItem(itemId), values),
);
}
if (validator) {
list.validate = function (_itemId, value) {
return validator(null, value);
};
}
if (key === "regex") {
addProxyParameterFilterOptions(itemSection, includeProxyParameterOptions);
} else if (key === "exclude_regex") {
addProxyParameterFilterOptions(itemSection, excludeProxyParameterOptions);
}
});
}
function addPriorityGroupItemOptions(itemSection, options = {}) {
const parentSectionForGroup =
typeof options.parentSectionId === "function"
? options.parentSectionId
: parentSectionIdForItem;
const ownerId =
typeof options.ownerId === "function" ? options.ownerId : () => "";
let o = itemSection.option(
form.Value,
"name",
_("Display name"),
_("Name displayed on the dashboard"),
);
o.rmempty = false;
o.validate = function (_itemId, value) {
return `${value || ""}`.trim() ? true : _("Enter a display name");
};
o = itemSection.option(
form.Value,
"health_url",
_("Check URL"),
_("URL used to check whether a server is alive"),
);
o.default = "https://www.gstatic.com/generate_204";
o.rmempty = false;
urlTestUrlChoices().forEach((value) => o.value(value));
o.validate = function (_itemId, value) {
return validateUrlTestUrl(value);
};
o = itemSection.option(
form.Value,
"active_check_interval",
_("Check interval"),
_("How often the currently selected server is checked"),
);
o.default = "5s";
o.rmempty = false;
o.validate = function (_itemId, value) {
return validateRequiredSingBoxDuration(value);
};
o = itemSection.option(
form.Value,
"check_timeout",
_("Unavailability timeout"),
_("Check timeout after which the server is considered dead"),
);
o.default = "2s";
o.rmempty = false;
o.validate = function (_itemId, value) {
return validateRequiredSingBoxDuration(value);
};
o = itemSection.option(
form.Value,
"recovery_check_interval",
_("Higher-level check interval"),
_(
"How often higher priority levels are checked while a lower level is active",
),
);
o.default = "15s";
o.rmempty = false;
o.validate = function (_itemId, value) {
return validateRequiredSingBoxDuration(value);
};
o = itemSection.option(
form.Flag,
"pick_fastest",
_("Select the fastest node"),
_(
"When switching to another level, test every server and select the fastest instead of the first working one.",
),
);
o.default = "0";
o.rmempty = false;
o = itemSection.option(
form.Flag,
"switch_to_faster_same_priority",
_("Automatically select the fastest node in the current level"),
_(
"Periodically check the current level and switch to a faster server even when the current one works.",
),
);
o.default = "0";
o.rmempty = false;
o = itemSection.option(
form.Value,
"fastest_check_interval",
_("Faster server search interval"),
_("Use sing-box duration format like 1d, 12h or 30m"),
);
o.depends("switch_to_faster_same_priority", "1");
o.default = "3m";
o.rmempty = false;
o.validate = function (itemId, value) {
return optionMapValue(this, itemId, "switch_to_faster_same_priority") ===
"1"
? validateRequiredSingBoxDuration(value)
: true;
};
o = itemSection.option(
form.Flag,
"interrupt_exist_connections",
_("Interrupt connections"),
_("Interrupt connections when priority failover switches server"),
);
o.default = "1";
o.rmempty = false;
o = itemSection.option(
form.Flag,
"pin_dashboard",
_("Pin on dashboard"),
_("Keep Priority before latency-sorted servers"),
);
o.default = "1";
o.rmempty = false;
o = itemSection.option(
ButtonAddSettingsDynamicList,
"priority_level",
_("Priority levels"),
_("Top level has the highest priority; lower levels are used as fallback"),
);
o.rmempty = true;
o.modalonly = true;
o.addButtonLabel = _("+ Add level");
o.childType = "priority_level";
o.ownerOption = "group";
o.childOwnerId = ownerId;
o.parentSectionId = parentSectionForGroup;
o.childValueOption = "name";
o.childDefaults = defaultPriorityLevelSettings();
o.renderItemSettingsModal = showPriorityLevelSettingsModal;
o.validateItemsOnSave = function (groupId, values) {
return validatePriorityLevelItemsBeforeSave(groupId, values, this);
};
o.hasItemSettings = function (groupId, value) {
const normalized = `${value || ""}`.trim();
if (isExistingChildItem(groupId, normalized, "priority_level", "group")) {
return true;
}
return normalized.length > 0;
};
o.inputValueForItem = function (groupId, value) {
const inputValue = childItemInputValue(
groupId,
value,
"priority_level",
"name",
"group",
);
const store = childPendingSettingsStore(this, groupId);
return store[inputValue] && store[inputValue].name
? store[inputValue].name
: inputValue;
};
o.stagedChildSettings = function (groupId, value) {
const id = childItemInputValue(
groupId,
value,
"priority_level",
"name",
"group",
);
const store = childPendingSettingsStore(this, groupId);
return store[id] ? Object.assign({}, store[id]) : null;
};
o.clearStagedChildSettings = function (groupId) {
if (this.pendingChildSettings) {
delete this.pendingChildSettings[groupId];
}
};
o.afterMaterializeChildItems = function (_groupId, itemIds) {
itemIds.forEach((itemId, index) => {
uci.set(UCI_PACKAGE, itemId, "order", `${index}`);
});
};
o.renderListItemLabel = function (groupId, itemId) {
return E(
"span",
{ class: "fkp-dynlist-label" },
this.inputValueForItem(groupId, itemId),
);
};
}
function addDashboardGroupFilterOption(
optionSection,
key,
label,
description,
modes,
) {
const list = optionSection.option(form.DynamicList, key, label, description);
modes.forEach((mode) => {
list.depends({
action: "connection",
dashboard_filter_mode: mode,
urltest: /.+/,
});
list.depends({
action: "connection",
dashboard_filter_mode: mode,
priority_group: /.+/,
});
});
list.rmempty = true;
list.placeholder = _("-- Select --");
list.load = function (section_id) {
const values = getConfigListValues(section_id, key);
const choices = currentSectionGroupChoices(section_id, values);
refreshOptionChoices(this, choices);
return values;
};
list.validate = function (section_id, value) {
const selected = new Set(
currentSectionGroupChoices(section_id).map((choice) => choice.value),
);
return normalizeDynamicListItems(value).every((item) => selected.has(item))
? true
: _("Select an existing URLTest or Priority group");
};
configureLiveDynamicListChoices(list, currentSectionGroupChoices);
}
function addDashboardServerFilterOptions(section) {
const optionSection = {
option: (optionType, ...args) => {
const option = section.taboption("settings", optionType, ...args);
option.modalonly = true;
return option;
},
};
let o = optionSection.option(
form.ListValue,
"dashboard_filter_mode",
_("Servers on dashboard"),
_("Filter the servers that will be displayed on the dashboard."),
);
urlTestFilterModeChoices().forEach((choice) =>
o.value(choice.value, choice.label),
);
o.default = "disabled";
o.depends("action", "connection");
o = optionSection.option(
form.ListValue,
"dashboard_detect_server_country",
_("Detect server country"),
);
["exclude", "include", "mixed"].forEach((mode) =>
o.depends({ action: "connection", dashboard_filter_mode: mode }),
);
serverCountryDetectionChoices().forEach((choice) =>
o.value(choice.value, choice.label),
);
o.default = "flag_emoji";
const includeProxyParameterOptions = {
prefix: "dashboard_include",
toggleLabel: _("Include by proxy parameters"),
toggleDescription: _(
"Additionally show servers by protocol, transport, and security. Add only servers matching the specified parameters.",
),
dependencies: [
{ action: "connection", dashboard_filter_mode: "include" },
{ action: "connection", dashboard_filter_mode: "mixed" },
],
protocolDescription: _(
"Show only servers with one of the selected protocols.",
),
transportDescription: _(
"Show only servers with one of the selected transports.",
),
securityDescription: _(
"Show only servers with one of the selected security types.",
),
};
const excludeProxyParameterOptions = {
prefix: "dashboard_exclude",
toggleLabel: _("Exclude by proxy parameters"),
toggleDescription: _(
"Additionally hide servers by protocol, transport, and security. Hide servers matching the specified parameters.",
),
dependencies: [
{ action: "connection", dashboard_filter_mode: "exclude" },
{ action: "connection", dashboard_filter_mode: "mixed" },
],
protocolDescription: _("Hide servers with one of the selected protocols."),
transportDescription: _(
"Hide servers with one of the selected transports.",
),
securityDescription: _(
"Hide servers with one of the selected security types.",
),
};
[
[
"dashboard_include_countries",
_("Include countries"),
_("Show servers only from the specified countries."),
countryChoices(),
validateCountryCode,
["include", "mixed"],
],
[
"dashboard_include_outbounds",
_("Include servers"),
_("Show only selected servers."),
null,
null,
["include", "mixed"],
],
[
"dashboard_include_regex",
_("Include by regular expression"),
_("Show servers whose names match the expression."),
null,
validateRegex,
["include", "mixed"],
],
[
"dashboard_exclude_countries",
_("Exclude countries"),
_("Hide servers from the specified countries."),
countryChoices(),
validateCountryCode,
["exclude", "mixed"],
],
[
"dashboard_exclude_outbounds",
_("Exclude servers"),
_("Hide selected servers."),
null,
null,
["exclude", "mixed"],
],
[
"dashboard_exclude_regex",
_("Exclude by regular expression"),
_("Hide servers whose names match the expression."),
null,
validateRegex,
["exclude", "mixed"],
],
].forEach(([key, label, description, choices, validator, modes]) => {
const list = optionSection.option(
form.DynamicList,
key,
label,
description,
);
modes.forEach((mode) =>
list.depends({ action: "connection", dashboard_filter_mode: mode }),
);
list.rmempty = true;
if (choices) {
choices.forEach((choice) => list.value(choice.value, choice.label));
list.placeholder = _("-- Select --");
}
if (key.endsWith("_outbounds")) {
list.load = function (section_id) {
const values = getConfigListValues(section_id, key);
loadOutboundNameChoices(section_id).then(() => {
refreshDashboardFilterChoiceWidgets(section_id);
});
return values;
};
list.placeholder = _("-- Select --");
configureLiveDynamicListChoices(list, currentOutboundNameChoices);
}
if (validator) {
list.validate = function (_section_id, value) {
return validator(null, value);
};
}
if (key === "dashboard_include_regex") {
addDashboardGroupFilterOption(
optionSection,
"dashboard_include_groups",
_("Add URLTest / Priority group"),
_(
"Show servers from selected groups without repeating their filtering criteria.",
),
["include", "mixed"],
);
addProxyParameterFilterOptions(
optionSection,
includeProxyParameterOptions,
);
} else if (key === "dashboard_exclude_regex") {
addDashboardGroupFilterOption(
optionSection,
"dashboard_exclude_groups",
_("Exclude URLTest / Priority group"),
_(
"Hide servers from selected groups without repeating their filtering criteria.",
),
["exclude", "mixed"],
);
addProxyParameterFilterOptions(
optionSection,
excludeProxyParameterOptions,
);
}
});
}
function settingValueEquals(left, right) {
const normalize = (value) => {
if (Array.isArray(value)) {
return JSON.stringify(value.map((item) => `${item || ""}`));
}
return value === undefined || value === null ? "" : `${value}`;
};
return normalize(left) === normalize(right);
}
function readChildSettings(itemId, keys, defaults) {
const result = Object.assign({}, defaults || {});
keys.forEach((key) => {
const value = uci.get(UCI_PACKAGE, itemId, key);
if (value !== null && value !== undefined) {
result[key] = Array.isArray(value) ? value.slice() : value;
}
});
return result;
}
function changedSettings(base, next, keys) {
const result = {};
keys.forEach((key) => {
if (!settingValueEquals(base ? base[key] : null, next ? next[key] : null)) {
result[key] = next ? next[key] : null;
}
});
return result;
}
function hasChangedSettings(settings) {
return Object.keys(settings || {}).length > 0;
}
function childPendingSettingsStore(option, section_id) {
if (!option.pendingChildSettings) {
option.pendingChildSettings = {};
}
if (!option.pendingChildSettings[section_id]) {
option.pendingChildSettings[section_id] = {};
}
return option.pendingChildSettings[section_id];
}
function pendingChildSettings(option, section_id, value, defaults) {
value = `${value || ""}`.trim();
const store = childPendingSettingsStore(option, section_id);
if (!store[value]) {
store[value] = Object.assign({}, defaults || {});
}
return store[value];
}
function renderStackedJsonSettingsModal(title, map, onSave) {
const modal = document.querySelector("#modal_overlay > .modal.cbi-modal");
const activeMap = modal ? modal.querySelector(".cbi-map:not(.hidden)") : null;
const buttonRow = modal ? modal.querySelector("div.button-row") : null;
const heading = modal ? modal.querySelector("h4") : null;
if (!modal || !activeMap || !buttonRow || !heading) {
return Promise.resolve();
}
return map.render().then((nodes) => {
const titleNode = E("span", title ? ` » ${title}` : "");
const originalButtonClass = buttonRow.getAttribute("class") || "";
const originalButtonNodes = Array.from(buttonRow.childNodes);
let closed = false;
let saveButton;
let validationSummary;
const clearValidationSummary = () => {
if (validationSummary && validationSummary.parentNode) {
validationSummary.parentNode.removeChild(validationSummary);
}
validationSummary = null;
};
const showValidationSummary = (error) => {
clearValidationSummary();
const message = error?.message || "";
validationSummary = E(
"div",
{
class:
"alert-message warning fkp-stacked-settings-validation-summary",
},
[
E("strong", {}, _("Cannot save settings")),
E("div", {}, _("Fix the highlighted fields and save again.")),
message ? E("small", {}, message) : "",
],
);
buttonRow.parentNode.insertBefore(validationSummary, buttonRow);
const invalidInput = nodes.querySelector(".cbi-input-invalid");
if (invalidInput) {
invalidInput.scrollIntoView({ block: "center", behavior: "smooth" });
invalidInput.focus({ preventScroll: true });
}
};
const restoreButtonRow = () => {
buttonRow.textContent = "";
originalButtonNodes.forEach((node) => buttonRow.appendChild(node));
buttonRow.setAttribute("class", originalButtonClass);
};
const close = () => {
if (closed) {
return;
}
closed = true;
clearValidationSummary();
if (nodes.parentNode) {
nodes.parentNode.removeChild(nodes);
}
if (titleNode.parentNode) {
titleNode.parentNode.removeChild(titleNode);
}
activeMap.classList.remove("hidden");
restoreButtonRow();
};
const save = () => {
if (saveButton) {
saveButton.disabled = true;
}
clearValidationSummary();
return map
.parse()
.then(() => {
onSave(cleanFormSectionData(map.data.get(map.config, "settings")));
close();
})
.catch((error) => {
if (saveButton) {
saveButton.disabled = false;
}
showValidationSummary(error);
});
};
buttonRow.textContent = "";
buttonRow.append(
E(
"button",
{
class: "btn cbi-button",
click: close,
},
_("Close"),
),
" ",
(saveButton = E(
"button",
{
class: "btn cbi-button cbi-button-positive important",
click: save,
},
_("Save"),
)),
);
heading.appendChild(titleNode);
activeMap.classList.add("hidden");
activeMap.parentNode.insertBefore(nodes, activeMap.nextElementSibling);
});
}
function showChildItemSettingsModal(section_id, itemValue, option, settings) {
const value = `${itemValue || ""}`.trim();
const existing = isExistingChildItem(
section_id,
value,
settings.typeName,
settings.ownerOption,
);
const inputValue = childItemInputValue(
section_id,
value,
settings.typeName,
settings.valueOption,
settings.ownerOption,
);
const defaults =
typeof settings.defaults === "function"
? settings.defaults(inputValue)
: Object.assign({}, settings.defaults || {});
const initialSettings = existing
? readChildSettings(value, settings.keys, defaults)
: Object.assign(
{},
pendingChildSettings(option, section_id, inputValue, defaults),
);
const data = {
settings: Object.assign({}, initialSettings),
};
const map = new form.JSONMap(data);
const itemSection = map.section(form.NamedSection, "settings");
itemSection.anonymous = true;
itemSection.addremove = false;
settings.addOptions(itemSection, {
parentSectionId: () => section_id,
ownerId: () => value || section_id,
});
return renderStackedJsonSettingsModal(
settings.title(inputValue),
map,
(nextSettings) => {
if (existing) {
const diff = changedSettings(
initialSettings,
nextSettings,
settings.keys,
);
if (hasChangedSettings(diff)) {
applyChildItemSettings(value, diff);
}
if (typeof settings.afterSave === "function") {
settings.afterSave(value, inputValue, nextSettings, existing);
}
return;
}
const nextInputValue =
`${nextSettings[settings.valueOption] || inputValue || ""}`.trim();
const store = childPendingSettingsStore(option, section_id);
if (nextInputValue !== inputValue) {
delete store[inputValue];
}
store[nextInputValue] = nextSettings;
if (typeof settings.afterSave === "function") {
settings.afterSave(value, nextInputValue, nextSettings, existing);
}
},
);
}
function ruleSetIncludesSubnets(section_id, value) {
const settings = readItemSettingsMap(section_id, RULE_SET_ITEM_SETTINGS_KEY);
const itemSettings = settings && settings[value];
if (
itemSettings &&
typeof itemSettings === "object" &&
itemSettings.include_subnets != null
) {
return itemSettingsFlag(itemSettings, "include_subnets", false);
}
return getConfigListValues(section_id, "rule_set_with_subnets").includes(
value,
);
}
function showSubscriptionUrlSettingsModal(_section_id, itemValue, option) {
return showChildItemSettingsModal(_section_id, itemValue, option, {
typeName: "subscription_url",
valueOption: "url",
keys: subscriptionUrlSettingsKeys(),
defaults: defaultSubscriptionUrlSettings(),
addOptions: addSubscriptionUrlItemOptions,
title: () => _("Subscription URL settings"),
});
}
function showInterfaceSettingsModal(_section_id, itemValue, option) {
return showChildItemSettingsModal(_section_id, itemValue, option, {
typeName: "section_interface",
valueOption: "name",
keys: interfaceSettingsKeys(),
defaults: defaultInterfaceSettings(),
addOptions: addInterfaceItemOptions,
title: () => _("Network interface settings"),
});
}
function showUrlTestSettingsModal(
_section_id,
itemValue,
option,
widget,
itemNode,
context = {},
) {
return showChildItemSettingsModal(_section_id, itemValue, option, {
typeName: "urltest",
valueOption: "name",
keys: urlTestSettingsKeys(),
defaults: defaultUrlTestSettings,
addOptions: addUrlTestItemOptions,
title: (name) => {
const normalized = `${name || ""}`.trim();
return !normalized || /^urltest-[a-z0-9]+-\d+$/.test(normalized)
? _("URLTest settings")
: `${_("URLTest settings")}: ${normalized}`;
},
afterSave: (itemId, inputValue, settings, existing) => {
const displayName = `${settings.name || inputValue || ""}`.trim();
if (existing) {
uci.unset(UCI_PACKAGE, itemId, "id");
uci.unset(UCI_PACKAGE, itemId, "display_name");
updateDynamicListItemLabel(itemNode, displayName);
return;
}
if (context.adding) {
addDynamicListItem(widget, displayName, displayName);
} else {
updateDynamicListItemLabel(itemNode, displayName);
}
},
});
}
function showPriorityLevelSettingsModal(
groupId,
itemValue,
option,
widget,
itemNode,
context = {},
) {
return showChildItemSettingsModal(groupId, itemValue, option, {
typeName: "priority_level",
ownerOption: "group",
valueOption: "name",
keys: priorityLevelSettingsKeys(),
defaults: defaultPriorityLevelSettings(),
addOptions: (itemSection) =>
addPriorityLevelItemOptions(itemSection, {
parentSectionId: () => context.parentSectionId || "",
}),
title: (name) => {
const normalized = `${name || ""}`.trim();
return normalized
? `${_("Priority level settings")}: ${normalized}`
: _("Priority level settings");
},
afterSave: (itemId, inputValue, settings, existing) => {
const displayName = `${settings.name || inputValue || ""}`.trim();
if (existing) {
updateDynamicListItemLabel(itemNode, displayName);
return;
}
if (context.adding) {
addDynamicListItem(widget, displayName, displayName);
} else {
updateDynamicListItemLabel(itemNode, displayName);
}
},
});
}
function createPriorityGroupItem(section_id, groupId, settings) {
const created = uci.add(UCI_PACKAGE, "priority_group", groupId) || groupId;
uci.set(UCI_PACKAGE, created, "section", section_id);
Object.entries(priorityGroupChildDefaults()).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== "") {
uci.set(UCI_PACKAGE, created, key, `${value}`);
}
});
applyChildItemSettings(created, settings);
return created;
}
function showPriorityGroupSettingsModal(
section_id,
itemValue,
option,
widget,
itemNode,
context = {},
) {
const groupId = context.adding
? randomPriorityGroupId()
: `${itemValue || ""}`.trim();
if (!groupId) {
return null;
}
return showChildItemSettingsModal(section_id, groupId, option, {
typeName: "priority_group",
valueOption: "name",
keys: priorityGroupSettingsKeys(),
defaults: defaultPriorityGroupSettings(),
addOptions: addPriorityGroupItemOptions,
title: (name) => {
if (context.adding) {
return _("Priority settings");
}
const normalized = `${name || ""}`.trim();
return normalized
? `${_("Priority settings")}: ${normalized}`
: _("Priority settings");
},
afterSave: (itemId, inputValue, settings, existing) => {
const displayName = `${settings.name || inputValue || ""}`.trim();
if (!existing) {
const created = createPriorityGroupItem(section_id, groupId, settings);
const store = childPendingSettingsStore(option, section_id);
delete store[groupId];
delete store[inputValue];
delete store[displayName];
addDynamicListItem(widget, created, displayName);
return;
}
updateDynamicListItemLabel(itemNode, displayName);
},
});
}
function validateUrlTestItemsBeforeSave(section_id, values, option) {
const store = childPendingSettingsStore(option, section_id);
for (const value of normalizeDynamicListItems(values)) {
const itemId = `${value || ""}`;
let name = "";
if (isExistingChildItem(section_id, itemId, "urltest")) {
name =
uci.get(UCI_PACKAGE, itemId, "name") ||
uci.get(UCI_PACKAGE, itemId, "display_name") ||
"";
} else if (store[itemId]) {
name = store[itemId].name || "";
}
if (!`${name || ""}`.trim()) {
return _("Enter a display name");
}
}
return true;
}
function validatePriorityGroupItemsBeforeSave(section_id, values) {
for (const value of normalizeDynamicListItems(values)) {
const groupId = `${value || ""}`.trim();
if (!isExistingChildItem(section_id, groupId, "priority_group")) {
return _("Open priority settings and enter a display name");
}
if (!`${uci.get(UCI_PACKAGE, groupId, "name") || ""}`.trim()) {
return _("Enter a display name");
}
}
return true;
}
function showOutboundJsonSettingsModal(
_section_id,
itemValue,
_option,
widget,
itemNode,
context = {},
) {
const data = {
settings: {
outbound_json: `${itemValue || ""}`,
},
};
const map = new form.JSONMap(data);
const itemSection = map.section(form.NamedSection, "settings");
itemSection.anonymous = true;
itemSection.addremove = false;
const jsonOption = itemSection.option(
form.TextValue,
"outbound_json",
_("JSON outbound"),
_("Enter a complete sing-box outbound object"),
);
jsonOption.rows = 12;
jsonOption.wrap = "soft";
jsonOption.textarea = true;
jsonOption.modalonly = true;
jsonOption.rmempty = false;
jsonOption.validate = function (_itemId, 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);
return renderStackedJsonSettingsModal(
_("JSON outbound settings"),
map,
(settings) => {
const value = `${settings.outbound_json || ""}`.trim();
if (context.adding) {
addDynamicListItem(widget, value);
return;
}
setDynamicListItemValue(itemNode, value);
updateDynamicListItemLabel(itemNode, outboundJsonDisplayTag(value));
if (widget && widget.node) {
widget.dispatchCbiDynlistChange(widget.node, value);
}
},
);
}
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 || ""}`, tags);
if (!validation.valid) {
const tag = outboundJsonDisplayTag(value);
return tag ? `${tag}: ${validation.message}` : validation.message;
}
const tag = outboundJsonDisplayTag(value);
tags.push(tag);
}
return true;
}
function showRuleSetSettingsModal(section_id, itemValue, option, widget) {
const data = {
settings: {
include_subnets: ruleSetIncludesSubnets(section_id, itemValue)
? "1"
: "0",
},
};
const map = new form.JSONMap(data);
const section = map.section(form.NamedSection, "settings");
section.anonymous = true;
section.addremove = false;
const includeSubnets = section.option(
form.Flag,
"include_subnets",
_("Include IP addresses and subnets"),
_("Subnets from the list will be extracted and added to nftables"),
);
includeSubnets.default = "0";
includeSubnets.rmempty = false;
return renderStackedJsonSettingsModal(
_("Rule set settings"),
map,
(settings) => {
const value = settings.include_subnets === "1";
const refs = uniqueDynamicListItems(
widget && typeof widget.getValue === "function"
? widget.getValue()
: getCustomRulesetReferences(section_id),
);
const subnets = new Set(
getConfigListValues(section_id, "rule_set_with_subnets").filter((ref) =>
refs.includes(ref),
),
);
if (value) {
subnets.add(itemValue);
} else {
subnets.delete(itemValue);
}
writeListOption(
section_id,
"rule_set",
refs.filter((ref) => !subnets.has(ref)),
);
writeListOption(section_id, "rule_set_with_subnets", [...subnets]);
if (option && typeof option.getUIElement === "function") {
option.getUIElement(section_id).setValue(refs);
}
},
);
}
function ensureActionProvidersAvailabilityLoaded() {
if (actionProvidersAvailabilityState.loaded) {
return Promise.resolve(actionProvidersAvailabilityState);
}
if (actionProvidersAvailabilityPromise) {
return actionProvidersAvailabilityPromise;
}
if (actionProvidersAvailabilityLoader) {
actionProvidersAvailabilityPromise = actionProvidersAvailabilityLoader()
.then((capabilities) => {
updateActionProvidersAvailabilityState({
zapretInstalled: Boolean(capabilities?.zapretInstalled),
zapret2Installed: Boolean(capabilities?.zapret2Installed),
byedpiInstalled: Boolean(capabilities?.byedpiInstalled),
singBoxExtended: Boolean(capabilities?.singBoxExtended),
});
return actionProvidersAvailabilityState;
})
.catch(() => {
actionProvidersAvailabilityLoader = null;
actionProvidersAvailabilityPromise = null;
return ensureActionProvidersAvailabilityLoaded();
})
.finally(() => {
actionProvidersAvailabilityPromise = null;
});
return actionProvidersAvailabilityPromise;
}
actionProvidersAvailabilityPromise = Promise.allSettled([
main.TachyonShellMethods.checkZapretRuntime(),
main.TachyonShellMethods.checkZapret2Runtime(),
main.TachyonShellMethods.checkByedpiRuntime(),
])
.then(([zapretResult, zapret2Result, byedpiResult]) => {
const zapret =
zapretResult && zapretResult.status === "fulfilled"
? zapretResult.value
: null;
const zapret2 =
zapret2Result && zapret2Result.status === "fulfilled"
? zapret2Result.value
: null;
const byedpi =
byedpiResult && byedpiResult.status === "fulfilled"
? byedpiResult.value
: null;
actionProvidersAvailabilityState.loaded = true;
actionProvidersAvailabilityState.zapretInstalled = Boolean(
zapret && zapret.success && zapret.data && zapret.data.zapret_installed,
);
actionProvidersAvailabilityState.zapret2Installed = Boolean(
zapret2 &&
zapret2.success &&
zapret2.data &&
zapret2.data.zapret2_installed,
);
actionProvidersAvailabilityState.byedpiInstalled = Boolean(
byedpi && byedpi.success && byedpi.data && byedpi.data.byedpi_installed,
);
return actionProvidersAvailabilityState;
})
.catch(() => {
actionProvidersAvailabilityState.loaded = true;
actionProvidersAvailabilityState.zapretInstalled = false;
actionProvidersAvailabilityState.zapret2Installed = false;
actionProvidersAvailabilityState.byedpiInstalled = false;
// singBoxExtended is set via systemInfo, keep existing value
return actionProvidersAvailabilityState;
})
.finally(() => {
actionProvidersAvailabilityPromise = null;
});
return actionProvidersAvailabilityPromise;
}
function isZapretInstalledForUi() {
return actionProvidersAvailabilityState.zapretInstalled;
}
function isZapret2InstalledForUi() {
return actionProvidersAvailabilityState.zapret2Installed;
}
function isByedpiInstalledForUi() {
return actionProvidersAvailabilityState.byedpiInstalled;
}
function isSingBoxExtendedForUi() {
return actionProvidersAvailabilityState.singBoxExtended;
}
function getRuleConfiguredAction(section_id) {
const action = uci.get(UCI_PACKAGE, section_id, "action");
return action ? `${action}` : null;
}
function getRuleResolvedAction(section_id) {
return getRuleConfiguredAction(section_id) || "connection";
}
function getActionOptionLabel(action) {
switch (`${action}`) {
case "block":
return "Block";
case "bypass":
return "Bypass";
case "connection":
return "Connection";
case "dns":
return "DNS";
case "vpn":
return "VPN";
case "zapret":
return "Zapret";
case "zapret2":
return "Zapret2";
case "byedpi":
return "ByeDPI";
case "awg":
return "AmneziaWG";
case "warp":
return "WARP";
case "anytls":
return "AnyTLS";
case "snell":
return "Snell";
case "mieru":
return "Mieru";
case "sudoku":
return "Sudoku";
case "masque":
return "MASQUE";
case "openvpn":
return "OpenVPN";
case "outbound":
return _("JSON outbound");
case "proxy":
default:
return "Proxy";
}
}
function getRuleActionDisplayValue(section_id) {
const action = getRuleResolvedAction(section_id);
if (action === "zapret") {
return "Zapret";
}
if (action === "zapret2") {
return "Zapret2";
}
if (action === "byedpi") {
return "ByeDPI";
}
if (action === "awg") {
return "AmneziaWG";
}
if (action === "warp") {
return "WARP";
}
if (action === "anytls") {
return "AnyTLS";
}
if (action === "snell") {
return "Snell";
}
if (action === "mieru") {
return "Mieru";
}
if (action === "sudoku") {
return "Sudoku";
}
if (action === "masque") {
return "MASQUE";
}
if (action === "openvpn") {
return "OpenVPN";
}
return getActionOptionLabel(action);
}
function getRuleActionDisplayMarkup(section_id) {
return getRuleActionDisplayValue(section_id);
}
function populateActionOptionValues(option) {
delete option.keylist;
delete option.vallist;
option.value("connection", getActionOptionLabel("connection"));
if (isSingBoxExtendedForUi()) {
option.value("awg", getActionOptionLabel("awg"));
option.value("warp", getActionOptionLabel("warp"));
option.value("masque", getActionOptionLabel("masque"));
option.value("openvpn", getActionOptionLabel("openvpn"));
option.value("anytls", getActionOptionLabel("anytls"));
option.value("snell", getActionOptionLabel("snell"));
option.value("mieru", getActionOptionLabel("mieru"));
option.value("sudoku", getActionOptionLabel("sudoku"));
}
option.value("bypass", "Bypass");
option.value("block", "Block");
option.value("dns", "DNS");
if (isZapretInstalledForUi()) {
option.value("zapret", getActionOptionLabel("zapret"));
}
if (isZapret2InstalledForUi()) {
option.value("zapret2", getActionOptionLabel("zapret2"));
}
if (isByedpiInstalledForUi()) {
option.value("byedpi", getActionOptionLabel("byedpi"));
}
}
function getConfigListValues(section_id, key) {
return normalizeOptionValues(uci.get(UCI_PACKAGE, section_id, key));
}
function stringArraysEqual(left, right) {
left = normalizeDynamicListItems(left);
right = normalizeDynamicListItems(right);
return (
left.length === right.length &&
left.every((value, index) => value === right[index])
);
}
function writeListOption(section_id, key, values) {
const normalized = normalizeOptionValues(values);
if (stringArraysEqual(getConfigListValues(section_id, key), normalized)) {
return;
}
if (normalized.length) {
uci.set(UCI_PACKAGE, section_id, key, normalized);
} else {
uci.unset(UCI_PACKAGE, section_id, key);
}
}
function childOwnerOption(ownerOption) {
return ownerOption || "section";
}
function childItemOrder(item) {
const value = item ? item.order : null;
const parsed = Number.parseInt(value == null ? "0" : `${value}`, 10);
return Number.isFinite(parsed) ? parsed : 0;
}
function getChildItemIds(section_id, typeName, ownerOption) {
const ownerKey = childOwnerOption(ownerOption);
const items = uci
.sections(UCI_PACKAGE, typeName)
.filter((item) => item[ownerKey] === section_id);
if (typeName === "priority_level") {
items.sort((a, b) => {
const orderDiff = childItemOrder(a) - childItemOrder(b);
if (orderDiff !== 0) {
return orderDiff;
}
return `${a[".name"] || ""}`.localeCompare(`${b[".name"] || ""}`);
});
}
return items.map((item) => item[".name"]).filter(Boolean);
}
function childItemValue(itemId, valueOption, fallback) {
if (!valueOption) {
return itemId;
}
const value = uci.get(UCI_PACKAGE, itemId, valueOption);
return value == null || value === "" ? fallback || itemId : `${value}`;
}
function childItemInputValue(
section_id,
value,
typeName,
valueOption,
ownerOption,
) {
const itemId = `${value || ""}`;
if (isExistingChildItem(section_id, itemId, typeName, ownerOption)) {
return childItemValue(itemId, valueOption, itemId);
}
return itemId;
}
function isExistingChildItem(section_id, itemId, typeName, ownerOption) {
const ownerKey = childOwnerOption(ownerOption);
return Boolean(
itemId &&
uci.get(UCI_PACKAGE, itemId, ".type") === typeName &&
uci.get(UCI_PACKAGE, itemId, ownerKey) === section_id,
);
}
function findChildItemForInput(section_id, options, inputValue) {
const rawValue = `${inputValue || ""}`.trim();
if (
isExistingChildItem(
section_id,
rawValue,
options.typeName,
options.ownerOption,
)
) {
return rawValue;
}
if (options.valueOption) {
return getChildItemIds(
section_id,
options.typeName,
options.ownerOption,
).find(
(itemId) =>
childItemValue(itemId, options.valueOption, itemId) === rawValue,
);
}
return null;
}
function createChildItem(section_id, options, inputValue) {
const rawValue = `${inputValue || ""}`.trim();
const existing = findChildItemForInput(section_id, options, rawValue);
if (existing) {
return {
value: existing,
text: options.valueOption
? childItemValue(existing, options.valueOption, existing)
: existing,
created: false,
};
}
const requestedId =
typeof options.createId === "function"
? `${options.createId(rawValue, section_id) || ""}`.trim()
: "";
const itemId =
(requestedId && uci.add(UCI_PACKAGE, options.typeName, requestedId)) ||
requestedId ||
uci.add(UCI_PACKAGE, options.typeName);
uci.set(
UCI_PACKAGE,
itemId,
childOwnerOption(options.ownerOption),
section_id,
);
if (options.valueOption) {
uci.set(UCI_PACKAGE, itemId, options.valueOption, rawValue);
}
if (options.defaults && typeof options.defaults === "object") {
Object.entries(options.defaults).forEach(([key, value]) => {
value =
typeof value === "function"
? value(rawValue, section_id, itemId)
: value;
if (value !== undefined && value !== null && value !== "") {
uci.set(UCI_PACKAGE, itemId, key, `${value}`);
}
});
}
return {
value: itemId,
text: options.valueOption ? rawValue : itemId,
created: true,
};
}
function applyChildItemSettings(itemId, settings) {
Object.entries(settings || {}).forEach(([key, value]) => {
if (!key || key.charAt(0) === ".") {
return;
}
if (value === undefined || value === null || value === "") {
uci.unset(UCI_PACKAGE, itemId, key);
} else if (Array.isArray(value)) {
uci.set(
UCI_PACKAGE,
itemId,
key,
value
.map((item) => `${item || ""}`.trim())
.filter((item) => item.length),
);
} else {
uci.set(UCI_PACKAGE, itemId, key, `${value}`);
}
});
}
function materializeChildItems(section_id, options, inputValue) {
const result = [];
const seen = new Set();
normalizeDynamicListItems(inputValue).forEach((value) => {
const createdItem = createChildItem(section_id, options, value);
const itemId = createdItem.value;
const stagedSettings =
createdItem.created && typeof options.stagedSettings === "function"
? options.stagedSettings(value, itemId, createdItem.created)
: null;
if (stagedSettings) {
applyChildItemSettings(itemId, stagedSettings);
}
if (itemId && !seen.has(itemId)) {
seen.add(itemId);
result.push(itemId);
}
});
return result;
}
function cleanupPriorityLevelsForGroup(groupId) {
getChildItemIds(groupId, "priority_level", "group").forEach((levelId) => {
uci.remove(UCI_PACKAGE, levelId);
});
}
function cleanupRemovedChildItems(
section_id,
typeName,
keepValues,
ownerOption,
) {
const keep = new Set(normalizeDynamicListItems(keepValues));
getChildItemIds(section_id, typeName, ownerOption).forEach((itemId) => {
if (!keep.has(itemId)) {
if (typeName === "priority_group") {
cleanupPriorityLevelsForGroup(itemId);
}
uci.remove(UCI_PACKAGE, itemId);
}
});
}
function parentSectionIdForItem(itemId) {
return uci.get(UCI_PACKAGE, itemId, "section") || "";
}
function refreshOptionChoices(option, choices) {
delete option.keylist;
delete option.vallist;
(choices || []).forEach((choice) => {
if (typeof choice === "object") {
option.value(choice.value, choice.label);
} else {
option.value(choice);
}
});
}
function validateRegex(_section_id, value) {
if (!value || !value.length) {
return true;
}
try {
new RegExp(value);
return true;
} catch (_error) {
return _("Invalid regular expression");
}
}
function validateKeyword(_section_id, value) {
if (!value || !value.length) {
return true;
}
if (/[,\s]/.test(value)) {
return _("Keyword must not contain spaces or commas");
}
return true;
}
function isSingBoxDuration(value) {
return /^(?=.*[1-9])([0-9]+(?:\.[0-9]+)?(?:ns|us|ms|s|m|h|d))+$/.test(value);
}
function writeOptionalDurationOption(section_id, key, value) {
const normalized = value ? `${value}`.trim() : "";
const disabledKey = `${key}_disabled`;
if (normalized.length) {
uci.set(UCI_PACKAGE, section_id, key, normalized);
uci.unset(UCI_PACKAGE, section_id, disabledKey);
} else {
uci.unset(UCI_PACKAGE, section_id, key);
uci.set(UCI_PACKAGE, section_id, disabledKey, "1");
}
}
function validateOptionalSingBoxDuration(value) {
const normalized = value ? `${value}`.trim() : "";
if (!normalized.length) {
return true;
}
if (isSingBoxDuration(normalized)) {
return true;
}
return _("Use sing-box duration format like 1d, 12h or 30m");
}
function validateRequiredSingBoxDuration(value) {
const normalized = value ? `${value}`.trim() : "";
if (!normalized.length) {
return _("Use sing-box duration format like 1d, 12h or 30m");
}
if (isSingBoxDuration(normalized)) {
return true;
}
return _("Use sing-box duration format like 1d, 12h or 30m");
}
function parseSubscriptionUrlEntry(value) {
const normalized = value ? `${value}`.trim() : "";
if (!normalized.length) {
return { valid: true, url: "" };
}
if (normalized.includes("|")) {
return {
valid: false,
message: _("Configure User-Agent in the item settings"),
};
}
return { valid: true, url: normalized };
}
function validateSubscriptionUrlEntry(_section_id, value) {
if (!value || value.length === 0) {
return true;
}
const parsed = parseSubscriptionUrlEntry(value);
if (!parsed.valid) {
return parsed.message;
}
const validation = main.validateUrl(parsed.url);
return validation.valid ? true : validation.message;
}
function getDuplicateTextListErrors(values, normalizeValue, duplicateMessage) {
const seen = new Set();
const duplicates = [];
values.forEach((item) => {
const normalized = normalizeValue ? normalizeValue(item) : item;
if (seen.has(normalized)) {
if (!duplicates.includes(item)) {
duplicates.push(item);
}
return;
}
seen.add(normalized);
});
return duplicates.map((item) => `${item}: ${duplicateMessage}`);
}
function getValidationHeaderText() {
return _("Validation errors:");
}
function getDuplicateValueText() {
return _("Duplicate value");
}
function escapeHtml(value) {
return `${value}`
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function ensureAnnotatedTextareaStyles() {
if (
typeof document === "undefined" ||
!document.head ||
document.getElementById(ANNOTATED_TEXTAREA_STYLE_ID)
) {
return;
}
document.head.insertAdjacentHTML(
"beforeend",
`<style id="${ANNOTATED_TEXTAREA_STYLE_ID}">
.fkp-annotated-textarea {
position: relative;
}
.fkp-annotated-textarea > textarea {
position: relative;
z-index: 1;
background: transparent !important;
}
.fkp-annotated-textarea__overlay {
position: absolute;
inset: 0;
z-index: 0;
pointer-events: none;
overflow: hidden;
box-sizing: border-box;
color: transparent;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: break-word;
}
.fkp-annotated-textarea__invalid {
color: transparent;
text-decoration-line: underline;
text-decoration-style: wavy;
text-decoration-color: var(--error-color-medium, #d44);
text-decoration-thickness: 1.5px;
text-underline-offset: 2px;
text-decoration-skip-ink: none;
}
</style>`,
);
}
function applyTextareaInputAttributes(textarea) {
textarea.setAttribute("spellcheck", "false");
textarea.setAttribute("autocomplete", "off");
textarea.setAttribute("autocorrect", "off");
textarea.setAttribute("autocapitalize", "off");
textarea.setAttribute("data-gramm", "false");
textarea.setAttribute("data-gramm_editor", "false");
textarea.setAttribute("data-enable-grammarly", "false");
textarea.style.resize = "vertical";
textarea.style.maxWidth = "100%";
const getRowsMinHeight = () => {
const rows = Number.parseInt(textarea.getAttribute("rows") || "0", 10);
if (!rows || typeof window === "undefined") {
return 0;
}
const style = window.getComputedStyle(textarea);
const fontSize = Number.parseFloat(style.fontSize) || 16;
const lineHeight =
Number.parseFloat(style.lineHeight) || Math.ceil(fontSize * 1.2);
const verticalPadding =
(Number.parseFloat(style.paddingTop) || 0) +
(Number.parseFloat(style.paddingBottom) || 0);
const verticalBorder =
(Number.parseFloat(style.borderTopWidth) || 0) +
(Number.parseFloat(style.borderBottomWidth) || 0);
return Math.ceil(rows * lineHeight + verticalPadding + verticalBorder);
};
const applyMinHeight = () => {
const storedMinHeight = Number.parseFloat(
textarea.getAttribute("data-fkp-default-min-height") || "0",
);
const nextMinHeight = Math.max(
storedMinHeight,
textarea.offsetHeight,
getRowsMinHeight(),
);
if (nextMinHeight > 0) {
if (storedMinHeight <= 0) {
textarea.setAttribute(
"data-fkp-default-min-height",
`${nextMinHeight}`,
);
}
textarea.style.minHeight = `${nextMinHeight}px`;
return true;
}
return false;
};
if (
!applyMinHeight() &&
typeof window !== "undefined" &&
typeof window.requestAnimationFrame === "function"
) {
window.requestAnimationFrame(() => {
if (!applyMinHeight()) {
window.setTimeout(applyMinHeight, 0);
}
});
}
textarea.addEventListener("focus", applyMinHeight);
textarea.addEventListener("pointerdown", applyMinHeight);
}
function syncAnnotatedTextareaOverlay(textarea, wrapper, overlay) {
if (
typeof window === "undefined" ||
!textarea ||
!wrapper ||
!overlay ||
typeof window.getComputedStyle !== "function"
) {
return;
}
const style = window.getComputedStyle(textarea);
wrapper.style.backgroundColor = style.backgroundColor;
wrapper.style.borderRadius = style.borderRadius;
overlay.style.font = style.font;
overlay.style.lineHeight = style.lineHeight;
overlay.style.letterSpacing = style.letterSpacing;
overlay.style.paddingTop = style.paddingTop;
overlay.style.paddingRight = style.paddingRight;
overlay.style.paddingBottom = style.paddingBottom;
overlay.style.paddingLeft = style.paddingLeft;
overlay.style.borderTopWidth = style.borderTopWidth;
overlay.style.borderRightWidth = style.borderRightWidth;
overlay.style.borderBottomWidth = style.borderBottomWidth;
overlay.style.borderLeftWidth = style.borderLeftWidth;
overlay.style.borderStyle = "solid";
overlay.style.borderColor = "transparent";
overlay.style.textAlign = style.textAlign;
overlay.style.direction = style.direction;
overlay.style.tabSize = style.tabSize;
overlay.style.textIndent = style.textIndent;
overlay.style.textTransform = style.textTransform;
overlay.style.boxSizing = style.boxSizing;
overlay.style.scrollPaddingTop = style.scrollPaddingTop;
overlay.scrollTop = textarea.scrollTop;
overlay.scrollLeft = textarea.scrollLeft;
}
function createAnnotationKey(annotation) {
return `${annotation.start}:${annotation.end}`;
}
function addAnnotationIssue(annotationMap, annotation, message) {
const key = createAnnotationKey(annotation);
const existing = annotationMap.get(key);
if (existing) {
if (!existing.messages.includes(message)) {
existing.messages.push(message);
}
return;
}
annotationMap.set(key, {
start: annotation.start,
end: annotation.end,
messages: [message],
});
}
function finalizeAnnotations(annotationMap) {
return Array.from(annotationMap.values())
.map((annotation) => ({
start: annotation.start,
end: annotation.end,
message: annotation.messages.join("; "),
}))
.sort((left, right) => left.start - right.start || left.end - right.end);
}
function renderAnnotatedTextareaOverlay(value, annotations) {
const text = value ? `${value}` : "";
const normalizedAnnotations = Array.isArray(annotations) ? annotations : [];
if (!text.length) {
return "&#8203;";
}
if (!normalizedAnnotations.length) {
return `${escapeHtml(text)}${text.endsWith("\n") ? "\n " : ""}`;
}
let cursor = 0;
let html = "";
normalizedAnnotations.forEach((annotation) => {
if (
annotation.start < cursor ||
annotation.start >= annotation.end ||
annotation.start < 0
) {
return;
}
html += escapeHtml(text.slice(cursor, annotation.start));
html += `<span class="fkp-annotated-textarea__invalid">${escapeHtml(
text.slice(annotation.start, annotation.end),
)}</span>`;
cursor = annotation.end;
});
html += escapeHtml(text.slice(cursor));
if (text.endsWith("\n")) {
html += "\n ";
}
return html;
}
function attachAnnotatedTextarea(textarea, analyzer) {
if (!textarea || typeof analyzer !== "function") {
return;
}
ensureAnnotatedTextareaStyles();
if (textarea.__tachyonAnnotatedTextareaController) {
textarea.__tachyonAnnotatedTextareaController.analyzer = analyzer;
textarea.__tachyonAnnotatedTextareaController.update();
return;
}
const wrapper = textarea.parentNode;
if (!wrapper) {
return;
}
wrapper.classList.add("fkp-annotated-textarea");
const overlay = document.createElement("div");
overlay.className = "fkp-annotated-textarea__overlay";
overlay.setAttribute("aria-hidden", "true");
wrapper.insertBefore(overlay, textarea.nextSibling);
const controller = {
analyzer,
textarea,
wrapper,
overlay,
update() {
const analysis = this.analyzer(this.textarea.value);
this.overlay.innerHTML = renderAnnotatedTextareaOverlay(
this.textarea.value,
analysis.annotations,
);
syncAnnotatedTextareaOverlay(this.textarea, this.wrapper, this.overlay);
},
};
textarea.__tachyonAnnotatedTextareaController = controller;
const updateAnnotatedTextarea = () => controller.update();
textarea.addEventListener("input", updateAnnotatedTextarea);
textarea.addEventListener("change", updateAnnotatedTextarea);
textarea.addEventListener("scroll", updateAnnotatedTextarea, {
passive: true,
});
textarea.addEventListener("keyup", updateAnnotatedTextarea);
if (typeof ResizeObserver === "function") {
const resizeObserver = new ResizeObserver(() => controller.update());
resizeObserver.observe(textarea);
controller.resizeObserver = resizeObserver;
}
controller.update();
}
function refreshAnnotatedTextareaValidation(option, section_id, textarea) {
if (option && typeof option.triggerValidation === "function") {
option.triggerValidation(section_id);
}
if (
textarea &&
textarea.__tachyonAnnotatedTextareaController &&
typeof textarea.__tachyonAnnotatedTextareaController.update === "function"
) {
textarea.__tachyonAnnotatedTextareaController.update();
}
}
function attachNfqwsRemoteValidation(option, section_id, textarea) {
if (!textarea || textarea.__tachyonNfqwsRemoteValidationAttached) {
return;
}
textarea.__tachyonNfqwsRemoteValidationAttached = true;
textarea.__tachyonNfqwsRemoteValidationRequestId = 0;
textarea.__tachyonNfqwsRemoteValidationTimer = null;
const runValidation = () => {
const value = textarea.value;
const localAnalysis = buildNfqwsLocalAnalysis(value);
if (!localAnalysis.valid) {
refreshAnnotatedTextareaValidation(option, section_id, textarea);
return;
}
const requestId =
(textarea.__tachyonNfqwsRemoteValidationRequestId || 0) + 1;
textarea.__tachyonNfqwsRemoteValidationRequestId = requestId;
validateNfqwsStrategyRemotely(value).then(() => {
if (textarea.__tachyonNfqwsRemoteValidationRequestId !== requestId) {
return;
}
refreshAnnotatedTextareaValidation(option, section_id, textarea);
});
};
const scheduleValidation = (delay = NFQWS_REMOTE_VALIDATION_DEBOUNCE_MS) => {
if (textarea.__tachyonNfqwsRemoteValidationTimer) {
window.clearTimeout(textarea.__tachyonNfqwsRemoteValidationTimer);
}
textarea.__tachyonNfqwsRemoteValidationTimer = window.setTimeout(() => {
textarea.__tachyonNfqwsRemoteValidationTimer = null;
runValidation();
}, delay);
};
textarea.addEventListener("input", () => scheduleValidation());
textarea.addEventListener("change", () => scheduleValidation(0));
textarea.addEventListener("blur", () => scheduleValidation(0));
scheduleValidation(0);
}
function attachNfqws2RemoteValidation(option, section_id, textarea) {
if (!textarea || textarea.__tachyonNfqws2RemoteValidationAttached) {
return;
}
textarea.__tachyonNfqws2RemoteValidationAttached = true;
textarea.__tachyonNfqws2RemoteValidationRequestId = 0;
textarea.__tachyonNfqws2RemoteValidationTimer = null;
const runValidation = () => {
const value = textarea.value;
const localAnalysis = buildNfqws2LocalAnalysis(value);
if (!localAnalysis.valid) {
refreshAnnotatedTextareaValidation(option, section_id, textarea);
return;
}
const requestId =
(textarea.__tachyonNfqws2RemoteValidationRequestId || 0) + 1;
textarea.__tachyonNfqws2RemoteValidationRequestId = requestId;
validateNfqws2StrategyRemotely(value).then(() => {
if (textarea.__tachyonNfqws2RemoteValidationRequestId !== requestId) {
return;
}
refreshAnnotatedTextareaValidation(option, section_id, textarea);
});
};
const scheduleValidation = (delay = NFQWS_REMOTE_VALIDATION_DEBOUNCE_MS) => {
if (textarea.__tachyonNfqws2RemoteValidationTimer) {
window.clearTimeout(textarea.__tachyonNfqws2RemoteValidationTimer);
}
textarea.__tachyonNfqws2RemoteValidationTimer = window.setTimeout(() => {
textarea.__tachyonNfqws2RemoteValidationTimer = null;
runValidation();
}, delay);
};
textarea.addEventListener("input", () => scheduleValidation());
textarea.addEventListener("change", () => scheduleValidation(0));
textarea.addEventListener("blur", () => scheduleValidation(0));
scheduleValidation(0);
}
function parseCommentAwareListTokens(value) {
const text = value ? `${value}` : "";
const tokens = [];
const lines = text.split(/\r\n|\r|\n/);
const newlines = text.match(/\r\n|\r|\n/g) || [];
let offset = 0;
lines.forEach((line, index) => {
const hashIndex = line.indexOf("#");
const slashIndex = line.indexOf("//");
let commentIndex = -1;
if (hashIndex >= 0 && slashIndex >= 0) {
commentIndex = Math.min(hashIndex, slashIndex);
} else if (hashIndex >= 0) {
commentIndex = hashIndex;
} else if (slashIndex >= 0) {
commentIndex = slashIndex;
}
const source = commentIndex >= 0 ? line.slice(0, commentIndex) : line;
const matcher = /[^,\s]+/g;
let match;
while ((match = matcher.exec(source)) !== null) {
tokens.push({
value: match[0],
start: offset + match.index,
end: offset + match.index + match[0].length,
});
}
offset += line.length + (newlines[index] ? newlines[index].length : 0);
});
return tokens;
}
function analyzeTextListValue(value, validateItem, emptyMessage, options = {}) {
const text = value ? `${value}` : "";
if (!text.length) {
return { valid: true, message: "", annotations: [] };
}
const tokens = parseCommentAwareListTokens(text);
if (!tokens.length) {
return { valid: false, message: emptyMessage, annotations: [] };
}
const duplicateMessage = options.duplicateMessage || getDuplicateValueText();
const annotationMap = new Map();
const errors = [];
const seen = new Set();
tokens.forEach((token) => {
if (typeof validateItem === "function") {
const validation = validateItem(token.value);
if (!validation.valid) {
errors.push(`${token.value}: ${validation.message}`);
addAnnotationIssue(annotationMap, token, validation.message);
}
}
const normalized = options.normalizeDuplicateValue
? options.normalizeDuplicateValue(token.value)
: token.value;
if (!normalized) {
return;
}
if (seen.has(normalized)) {
errors.push(`${token.value}: ${duplicateMessage}`);
addAnnotationIssue(annotationMap, token, duplicateMessage);
return;
}
seen.add(normalized);
});
if (!errors.length) {
return { valid: true, message: "", annotations: [] };
}
return {
valid: false,
message: [getValidationHeaderText(), ...errors].join("\n"),
annotations: finalizeAnnotations(annotationMap),
};
}
function analyzeDomainSuffixText(value) {
const validateDomainCondition = (domain) => {
const normalized = `${domain || ""}`.trim();
if (normalized.includes("/")) {
return { valid: false, message: _("Invalid domain address") };
}
return main.validateDomain(normalized, true);
};
return analyzeTextListValue(
value,
(item) => {
const colonIndex = item.indexOf(":");
const prefix = colonIndex > 0 ? item.slice(0, colonIndex) : "";
const body = colonIndex > 0 ? item.slice(colonIndex + 1) : item;
if (!prefix) {
return validateDomainCondition(body);
}
if (!["full", "keyword", "regex"].includes(prefix)) {
return {
valid: false,
message: _("Allowed domain prefixes are full:, keyword:, and regex:"),
};
}
if (!body.length) {
return { valid: false, message: _("Value cannot be empty") };
}
if (prefix === "full") {
return validateDomainCondition(body);
}
if (prefix === "keyword") {
const validation = validateKeyword(null, body);
return validation === true
? { valid: true, message: _("Valid") }
: { valid: false, message: validation };
}
if (/[,\s]/.test(body)) {
return {
valid: false,
message: _("Regular expression must not contain spaces or commas"),
};
}
const validation = validateRegex(null, body);
return validation === true
? { valid: true, message: _("Valid") }
: { valid: false, message: validation };
},
_("At least one valid domain must be specified."),
{
normalizeDuplicateValue: (item) => `${item}`.toLowerCase(),
},
);
}
function domainValuesWithPrefix(section_id, key, prefix) {
return getConfigListValues(section_id, key).map((value) =>
prefix ? `${prefix}:${value}` : value,
);
}
function domainTextValuesWithPrefix(section_id, key, prefix) {
const legacyText = uci.get(UCI_PACKAGE, section_id, `${key}_text`);
if (!legacyText) {
return [];
}
return main
.parseValueList(legacyText)
.map((value) => (prefix ? `${prefix}:${value}` : value));
}
function uniqueDomainTextValues(values) {
const seen = new Set();
return values.filter((value) => {
const key = `${value}`.toLowerCase();
if (!key || seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
}
function appendUniqueDomainTextValues(textValue, values) {
const originalText = typeof textValue === "string" ? textValue : "";
const seen = new Set(
main.parseValueList(originalText).map((value) => `${value}`.toLowerCase()),
);
const additions = uniqueDomainTextValues(values).filter((value) => {
const key = `${value}`.toLowerCase();
if (!key || seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
if (!additions.length) {
return originalText;
}
const base = originalText.replace(/\s+$/, "");
return [base, ...additions].filter(Boolean).join("\n");
}
function loadCombinedDomainText(section_id) {
const textValue =
uci.get(UCI_PACKAGE, section_id, "domain") ||
uci.get(UCI_PACKAGE, section_id, "domain_suffix_text");
const values = [
...domainValuesWithPrefix(section_id, "domain_suffix", ""),
...domainValuesWithPrefix(section_id, "domain_keyword", "keyword"),
...domainValuesWithPrefix(section_id, "domain_regex", "regex"),
...domainTextValuesWithPrefix(section_id, "domain_suffix", ""),
...domainTextValuesWithPrefix(section_id, "domain", "full"),
...domainTextValuesWithPrefix(section_id, "domain_keyword", "keyword"),
...domainTextValuesWithPrefix(section_id, "domain_regex", "regex"),
];
return appendUniqueDomainTextValues(textValue, values);
}
function analyzeIpCidrText(value) {
return analyzeTextListValue(
value,
(item) => main.validateSubnet(item),
_("At least one valid IP or subnet must be specified."),
{
normalizeDuplicateValue: (item) => `${item}`.trim(),
},
);
}
function validatePortCondition(_section_id, value) {
const normalized = value ? `${value}`.trim() : "";
if (!normalized.length) {
return true;
}
const match = normalized.match(/^(\d+)(?:-(\d+))?$/);
if (!match) {
return _("Invalid port or range. Use 80 or 1000-2000");
}
const start = Number.parseInt(match[1], 10);
const end = match[2] ? Number.parseInt(match[2], 10) : start;
if (start < 1 || start > 65535 || end < 1 || end > 65535) {
return _("Port must be between 1 and 65535");
}
if (start > end) {
return _("Port range start must be less than or equal to end");
}
return true;
}
function getNfqwsOptionArgumentMode(option) {
if (NFQWS_REQUIRED_ARG_OPTIONS.has(option)) {
return "required";
}
if (NFQWS_OPTIONAL_ARG_OPTIONS.has(option)) {
return "optional";
}
if (NFQWS_NO_ARG_OPTIONS.has(option)) {
return "none";
}
return "unknown";
}
function getNfqws2OptionArgumentMode(option) {
if (NFQWS2_REQUIRED_ARG_OPTIONS.has(option)) {
return "required";
}
if (NFQWS2_OPTIONAL_ARG_OPTIONS.has(option)) {
return "optional";
}
if (NFQWS2_NO_ARG_OPTIONS.has(option)) {
return "none";
}
return "unknown";
}
function normalizeNfqwsStrategyWhitespace(value) {
return value ? `${value}`.replace(/\s+/g, " ").trim() : "";
}
function parseNfqwsRuntimeTokens(value) {
const text = value ? `${value}` : "";
const tokens = [];
const matcher = /\S+/g;
let match;
while ((match = matcher.exec(text)) !== null) {
tokens.push({
value: match[0],
start: match.index,
end: match.index + match[0].length,
});
}
return tokens;
}
function normalizeNfqwsStrategyValue(value) {
const normalized = normalizeNfqwsStrategyWhitespace(value);
if (!normalized.length) {
return "";
}
return normalized === ZAPRET_LEGACY_DEFAULT_NFQWS_OPT
? ZAPRET_DEFAULT_NFQWS_OPT
: normalized;
}
function getCachedNfqwsRemoteValidation(value) {
const normalized = normalizeNfqwsStrategyValue(value);
return normalized.length
? nfqwsRemoteValidationCache.get(normalized) || null
: null;
}
function cacheNfqwsRemoteValidation(value, result) {
const normalized = normalizeNfqwsStrategyValue(value);
if (!normalized.length) {
return result;
}
const cached = {
valid: result && result.valid === true,
message: result && result.message ? `${result.message}` : "",
needle: result && result.needle ? `${result.needle}` : "",
needles:
result && Array.isArray(result.needles)
? result.needles.filter(Boolean).map((item) => `${item}`)
: result && result.needle
? [`${result.needle}`]
: [],
};
nfqwsRemoteValidationCache.set(normalized, cached);
return cached;
}
function buildNfqwsRemoteValidationFallback(error) {
const message =
error && error.message
? `${error.message}`
: _("Unable to validate the NFQWS strategy through the backend parser.");
return {
valid: false,
message: _("Backend validation failed: %s").format(message),
needle: "",
needles: [],
};
}
function validateNfqwsStrategyRemotely(value) {
const normalized = normalizeNfqwsStrategyValue(value);
if (!normalized.length) {
return Promise.resolve({
valid: true,
message: "",
needle: "",
needles: [],
});
}
if (nfqwsRemoteValidationCache.has(normalized)) {
return Promise.resolve(nfqwsRemoteValidationCache.get(normalized));
}
if (nfqwsRemoteValidationInflight.has(normalized)) {
return nfqwsRemoteValidationInflight.get(normalized);
}
const validationTask = fs
.exec(NFQWS_VALIDATION_COMMAND, [
"validate_nfqws_strategy_json",
normalized,
])
.then((result) => {
const payload = JSON.parse(
(result && result.stdout ? result.stdout : "{}").trim() || "{}",
);
return cacheNfqwsRemoteValidation(normalized, {
valid: payload.valid === true,
message: payload.message || "",
needle: payload.needle || "",
needles: Array.isArray(payload.needles)
? payload.needles.filter(Boolean)
: payload.needle
? [payload.needle]
: [],
});
})
.catch((error) =>
cacheNfqwsRemoteValidation(
normalized,
buildNfqwsRemoteValidationFallback(error),
),
)
.finally(() => {
nfqwsRemoteValidationInflight.delete(normalized);
});
nfqwsRemoteValidationInflight.set(normalized, validationTask);
return validationTask;
}
function getNfqwsForbiddenTokenInfo(token, index) {
const configFileMessage = _(
"External nfqws config files bypass Tachyon queue management and explicit validation.",
);
const hostSelectionMessage = _(
"Resource selection by hostname inside nfqws is not supported here; sing-box selects resources before NFQUEUE.",
);
const ipSelectionMessage = _(
"Resource selection by IP or CIDR inside nfqws is not supported here; sing-box selects resources before NFQUEUE.",
);
const placeholderMessage = _(
"Zapret hostlist templates are not supported here because Tachyon does not expand them for per-rule NFQWS strategies.",
);
const queueMessage = _(
"The NFQUEUE number is assigned by Tachyon for each rule and must not be overridden here.",
);
const fwmarkMessage = _(
"The desync fwmark is managed by Tachyon for loop prevention and must not be overridden here.",
);
const daemonMessage = _(
"Tachyon manages the nfqws process lifecycle itself, so daemon mode is not allowed here.",
);
const dryRunMessage = _(
"This field must start a working nfqws strategy; --dry-run exits immediately and is not allowed here.",
);
const versionMessage = _(
"This field must start a working nfqws strategy; --version exits immediately and is not allowed here.",
);
if (index === 0 && (token.startsWith("@") || token.startsWith("$"))) {
return {
reason: configFileMessage,
captureNextValue: false,
};
}
if (token === "<HOSTLIST>" || token === "<HOSTLIST_NOAUTO>") {
return {
reason: placeholderMessage,
captureNextValue: false,
};
}
if (
token === "--hostlist" ||
token.startsWith("--hostlist=") ||
token === "--hostlist-domains" ||
token.startsWith("--hostlist-domains=") ||
token === "--hostlist-exclude" ||
token.startsWith("--hostlist-exclude=") ||
token === "--hostlist-exclude-domains" ||
token.startsWith("--hostlist-exclude-domains=") ||
token === "--hostlist-auto" ||
token.startsWith("--hostlist-auto=") ||
token === "--hostlist-auto-fail-threshold" ||
token.startsWith("--hostlist-auto-fail-threshold=") ||
token === "--hostlist-auto-fail-time" ||
token.startsWith("--hostlist-auto-fail-time=") ||
token === "--hostlist-auto-retrans-threshold" ||
token.startsWith("--hostlist-auto-retrans-threshold=") ||
token === "--hostlist-auto-debug" ||
token.startsWith("--hostlist-auto-debug=")
) {
return {
reason: hostSelectionMessage,
captureNextValue: !token.includes("="),
};
}
if (
token === "--ipset" ||
token.startsWith("--ipset=") ||
token === "--ipset-ip" ||
token.startsWith("--ipset-ip=") ||
token === "--ipset-exclude" ||
token.startsWith("--ipset-exclude=") ||
token === "--ipset-exclude-ip" ||
token.startsWith("--ipset-exclude-ip=")
) {
return {
reason: ipSelectionMessage,
captureNextValue: !token.includes("="),
};
}
if (token === "--qnum" || token.startsWith("--qnum=")) {
return {
reason: queueMessage,
captureNextValue: !token.includes("="),
};
}
if (
token === "--dpi-desync-fwmark" ||
token.startsWith("--dpi-desync-fwmark=")
) {
return {
reason: fwmarkMessage,
captureNextValue: !token.includes("="),
};
}
if (token === "--daemon") {
return {
reason: daemonMessage,
captureNextValue: false,
};
}
if (token === "--dry-run") {
return {
reason: dryRunMessage,
captureNextValue: false,
};
}
if (token === "--version") {
return {
reason: versionMessage,
captureNextValue: false,
};
}
return null;
}
function buildNfqwsLocalAnalysis(value) {
const text = value ? `${value}` : "";
if (!text.trim().length) {
return {
valid: false,
message: _("NFQWS strategy cannot be empty"),
annotations: [],
};
}
if (text.trim() === ZAPRET_LEGACY_DEFAULT_NFQWS_OPT) {
return { valid: true, message: "", annotations: [] };
}
const tokens = parseNfqwsRuntimeTokens(text);
const annotationMap = new Map();
const errors = [];
for (let index = 0; index < tokens.length; ) {
const token = tokens[index];
const bareToken = token.value.includes("=")
? token.value.slice(0, token.value.indexOf("="))
: token.value;
const nextToken = tokens[index + 1] || null;
const forbidden = getNfqwsForbiddenTokenInfo(token.value, index);
if (forbidden) {
addAnnotationIssue(annotationMap, token, forbidden.reason);
let displayToken = token.value;
if (
forbidden.captureNextValue &&
nextToken &&
!nextToken.value.startsWith("--")
) {
addAnnotationIssue(annotationMap, nextToken, forbidden.reason);
displayToken = `${displayToken} ${nextToken.value}`;
index += 2;
} else {
index += 1;
}
errors.push(`${displayToken}: ${forbidden.reason}`);
continue;
}
if (!token.value.startsWith("--")) {
const reason = _(
"Unexpected standalone token. Use explicit flags such as --name or --name=value.",
);
addAnnotationIssue(annotationMap, token, reason);
errors.push(`${token.value}: ${reason}`);
index += 1;
continue;
}
const mode = getNfqwsOptionArgumentMode(bareToken);
if (mode === "unknown") {
const reason = _("Unknown NFQWS flag.");
addAnnotationIssue(annotationMap, token, reason);
errors.push(`${token.value}: ${reason}`);
index += 1;
continue;
}
if (mode === "none") {
if (token.value.includes("=")) {
const reason = _("This flag does not accept a value.");
addAnnotationIssue(annotationMap, token, reason);
errors.push(`${token.value}: ${reason}`);
}
index += 1;
continue;
}
if (mode === "optional") {
if (
nextToken &&
!token.value.includes("=") &&
!nextToken.value.startsWith("--")
) {
const reason = _(
"Optional values must be attached with '=' here; a separate token would be ignored by nfqws.",
);
addAnnotationIssue(annotationMap, token, reason);
addAnnotationIssue(annotationMap, nextToken, reason);
errors.push(`${token.value} ${nextToken.value}: ${reason}`);
index += 2;
} else {
index += 1;
}
continue;
}
if (!token.value.includes("=")) {
if (!nextToken || nextToken.value.startsWith("--")) {
const reason = _("This option requires a value.");
addAnnotationIssue(annotationMap, token, reason);
errors.push(`${token.value}: ${reason}`);
index += 1;
continue;
}
index += 2;
continue;
}
index += 1;
}
if (!errors.length) {
return { valid: true, message: "", annotations: [] };
}
return {
valid: false,
message: [getValidationHeaderText(), ...errors].join("\n"),
annotations: finalizeAnnotations(annotationMap),
};
}
function addNfqwsRemoteValidationNeedleAnnotations(
annotationMap,
tokens,
remoteValidation,
needle,
) {
if (!needle.length) {
return;
}
let matched = false;
tokens.forEach((token) => {
const tokenValue = token.value || "";
const optionMatch =
needle.startsWith("--") &&
(tokenValue === needle || tokenValue.startsWith(`${needle}=`));
const valueMatch =
tokenValue === needle ||
tokenValue.endsWith(`=${needle}`) ||
(!needle.startsWith("--") && tokenValue.includes(`=${needle},`)) ||
(!needle.startsWith("--") && tokenValue.endsWith(`=${needle}`));
if (optionMatch || valueMatch) {
addAnnotationIssue(annotationMap, token, remoteValidation.message);
matched = true;
}
});
if (matched) {
return;
}
if (needle.startsWith("--")) {
tokens
.filter((token) => token.value && token.value.startsWith(needle))
.forEach((token) =>
addAnnotationIssue(annotationMap, token, remoteValidation.message),
);
}
}
function addNfqwsRemoteValidationAnnotations(
annotationMap,
tokens,
remoteValidation,
) {
const needles =
remoteValidation &&
Array.isArray(remoteValidation.needles) &&
remoteValidation.needles.length
? remoteValidation.needles.map((needle) => `${needle}`)
: remoteValidation && remoteValidation.needle
? [`${remoteValidation.needle}`]
: [];
needles.forEach((needle) =>
addNfqwsRemoteValidationNeedleAnnotations(
annotationMap,
tokens,
remoteValidation,
needle,
),
);
}
function analyzeNfqwsStrategy(value) {
const localAnalysis = buildNfqwsLocalAnalysis(value);
if (!localAnalysis.valid) {
return localAnalysis;
}
const remoteValidation = getCachedNfqwsRemoteValidation(value);
if (!remoteValidation || remoteValidation.valid) {
return localAnalysis;
}
const text = value ? `${value}` : "";
const tokens = parseNfqwsRuntimeTokens(text);
const annotationMap = new Map();
localAnalysis.annotations.forEach((annotation) =>
addAnnotationIssue(annotationMap, annotation, annotation.message),
);
addNfqwsRemoteValidationAnnotations(annotationMap, tokens, remoteValidation);
return {
valid: false,
message: [getValidationHeaderText(), remoteValidation.message].join("\n"),
annotations: finalizeAnnotations(annotationMap),
};
}
function normalizeNfqws2StrategyValue(value) {
const normalized = normalizeNfqwsStrategyWhitespace(value);
return normalized.length ? normalized : ZAPRET2_DEFAULT_NFQWS2_OPT;
}
function getCachedNfqws2RemoteValidation(value) {
const normalized = normalizeNfqws2StrategyValue(value);
return normalized.length
? nfqws2RemoteValidationCache.get(normalized) || null
: null;
}
function cacheNfqws2RemoteValidation(value, result) {
const normalized = normalizeNfqws2StrategyValue(value);
if (!normalized.length) {
return result;
}
const cached = {
valid: result && result.valid === true,
message: result && result.message ? `${result.message}` : "",
needle: result && result.needle ? `${result.needle}` : "",
needles:
result && Array.isArray(result.needles)
? result.needles.filter(Boolean).map((item) => `${item}`)
: result && result.needle
? [`${result.needle}`]
: [],
};
nfqws2RemoteValidationCache.set(normalized, cached);
return cached;
}
function buildNfqws2RemoteValidationFallback(error) {
const message =
error && error.message
? `${error.message}`
: _("Unable to validate the NFQWS2 strategy through the backend parser.");
return {
valid: false,
message: _("Backend validation failed: %s").format(message),
needle: "",
needles: [],
};
}
function validateNfqws2StrategyRemotely(value) {
const normalized = normalizeNfqws2StrategyValue(value);
if (!normalized.length) {
return Promise.resolve({
valid: true,
message: "",
needle: "",
needles: [],
});
}
if (nfqws2RemoteValidationCache.has(normalized)) {
return Promise.resolve(nfqws2RemoteValidationCache.get(normalized));
}
if (nfqws2RemoteValidationInflight.has(normalized)) {
return nfqws2RemoteValidationInflight.get(normalized);
}
const validationTask = fs
.exec(NFQWS_VALIDATION_COMMAND, [
"validate_nfqws2_strategy_json",
normalized,
])
.then((result) => {
const payload = JSON.parse(
(result && result.stdout ? result.stdout : "{}").trim() || "{}",
);
return cacheNfqws2RemoteValidation(normalized, {
valid: payload.valid === true,
message: payload.message || "",
needle: payload.needle || "",
needles: Array.isArray(payload.needles)
? payload.needles.filter(Boolean)
: payload.needle
? [payload.needle]
: [],
});
})
.catch((error) =>
cacheNfqws2RemoteValidation(
normalized,
buildNfqws2RemoteValidationFallback(error),
),
)
.finally(() => {
nfqws2RemoteValidationInflight.delete(normalized);
});
nfqws2RemoteValidationInflight.set(normalized, validationTask);
return validationTask;
}
function getNfqws2ForbiddenTokenInfo(token, index, nextToken) {
const configFileMessage = _(
"External nfqws2 config files bypass Tachyon queue management and explicit validation.",
);
const hostSelectionMessage = _(
"Resource selection by hostname inside nfqws2 is not supported here; sing-box selects resources before NFQUEUE.",
);
const ipSelectionMessage = _(
"Resource selection by IP or CIDR inside nfqws2 is not supported here; sing-box selects resources before NFQUEUE.",
);
const placeholderMessage = _(
"Zapret2 hostlist templates are not supported here because Tachyon does not expand them for per-rule NFQWS2 strategies.",
);
const queueMessage = _(
"The NFQUEUE number is assigned by Tachyon for each rule and must not be overridden here.",
);
const fwmarkMessage = _(
"The desync fwmark is managed by Tachyon for loop prevention and must not be overridden here.",
);
const fuzzMessage = _(
"Fuzzing is not supported here because Tachyon needs deterministic runtime validation.",
);
const interceptMessage = _(
"Disabling interception is incompatible with action=zapret2 because Tachyon sends matched traffic through NFQUEUE.",
);
const daemonMessage = _(
"Tachyon manages the nfqws2 process lifecycle itself, so daemon mode is not allowed here.",
);
const dryRunMessage = _(
"This field must start a working nfqws2 strategy; --dry-run exits immediately and is not allowed here.",
);
const versionMessage = _(
"This field must start a working nfqws2 strategy; --version exits immediately and is not allowed here.",
);
if (index === 0 && (token.startsWith("@") || token.startsWith("$"))) {
return {
reason: configFileMessage,
captureNextValue: false,
};
}
if (token === "<HOSTLIST>" || token === "<HOSTLIST_NOAUTO>") {
return {
reason: placeholderMessage,
captureNextValue: false,
};
}
if (
token === "--hostlist" ||
token.startsWith("--hostlist=") ||
token === "--hostlist-domains" ||
token.startsWith("--hostlist-domains=") ||
token === "--hostlist-exclude" ||
token.startsWith("--hostlist-exclude=") ||
token === "--hostlist-exclude-domains" ||
token.startsWith("--hostlist-exclude-domains=") ||
token === "--hostlist-auto" ||
token.startsWith("--hostlist-auto=") ||
token === "--hostlist-auto-fail-threshold" ||
token.startsWith("--hostlist-auto-fail-threshold=") ||
token === "--hostlist-auto-fail-time" ||
token.startsWith("--hostlist-auto-fail-time=") ||
token === "--hostlist-auto-retrans-threshold" ||
token.startsWith("--hostlist-auto-retrans-threshold=") ||
token === "--hostlist-auto-debug" ||
token.startsWith("--hostlist-auto-debug=") ||
token === "--hostlist-auto-retrans-reset" ||
token.startsWith("--hostlist-auto-retrans-reset=")
) {
return {
reason: hostSelectionMessage,
captureNextValue: !token.includes("="),
};
}
if (
token === "--ipset" ||
token.startsWith("--ipset=") ||
token === "--ipset-ip" ||
token.startsWith("--ipset-ip=") ||
token === "--ipset-exclude" ||
token.startsWith("--ipset-exclude=") ||
token === "--ipset-exclude-ip" ||
token.startsWith("--ipset-exclude-ip=")
) {
return {
reason: ipSelectionMessage,
captureNextValue: !token.includes("="),
};
}
if (token === "--qnum" || token.startsWith("--qnum=")) {
return {
reason: queueMessage,
captureNextValue: !token.includes("="),
};
}
if (
token === "--fwmark" ||
token.startsWith("--fwmark=") ||
token === "--dpi-desync-fwmark" ||
token.startsWith("--dpi-desync-fwmark=")
) {
return {
reason: fwmarkMessage,
captureNextValue: !token.includes("="),
};
}
if (token === "--fuzz" || token.startsWith("--fuzz=")) {
return {
reason: fuzzMessage,
captureNextValue: !token.includes("="),
};
}
if (
token === "--intercept=0" ||
token === "--intercept=false" ||
token === "--intercept=no" ||
(token === "--intercept" && nextToken && nextToken.value === "0")
) {
return {
reason: interceptMessage,
captureNextValue: token === "--intercept",
};
}
if (token === "--daemon") {
return {
reason: daemonMessage,
captureNextValue: false,
};
}
if (token === "--dry-run") {
return {
reason: dryRunMessage,
captureNextValue: false,
};
}
if (token === "--version") {
return {
reason: versionMessage,
captureNextValue: false,
};
}
return null;
}
function buildNfqws2LocalAnalysis(value) {
const text = value ? `${value}` : "";
if (!text.trim().length) {
return {
valid: false,
message: _("NFQWS2 strategy cannot be empty"),
annotations: [],
};
}
const tokens = parseNfqwsRuntimeTokens(text);
const annotationMap = new Map();
const errors = [];
for (let index = 0; index < tokens.length; ) {
const token = tokens[index];
const bareToken = token.value.includes("=")
? token.value.slice(0, token.value.indexOf("="))
: token.value;
const nextToken = tokens[index + 1] || null;
const forbidden = getNfqws2ForbiddenTokenInfo(
token.value,
index,
nextToken,
);
if (forbidden) {
addAnnotationIssue(annotationMap, token, forbidden.reason);
let displayToken = token.value;
if (
forbidden.captureNextValue &&
nextToken &&
!nextToken.value.startsWith("--")
) {
addAnnotationIssue(annotationMap, nextToken, forbidden.reason);
displayToken = `${displayToken} ${nextToken.value}`;
index += 2;
} else {
index += 1;
}
errors.push(`${displayToken}: ${forbidden.reason}`);
continue;
}
if (!token.value.startsWith("--")) {
const reason = _(
"Unexpected standalone token. Use explicit flags such as --name or --name=value.",
);
addAnnotationIssue(annotationMap, token, reason);
errors.push(`${token.value}: ${reason}`);
index += 1;
continue;
}
const mode = getNfqws2OptionArgumentMode(bareToken);
if (mode === "unknown") {
const reason = _("Unknown NFQWS2 flag.");
addAnnotationIssue(annotationMap, token, reason);
errors.push(`${token.value}: ${reason}`);
index += 1;
continue;
}
if (mode === "none") {
if (token.value.includes("=")) {
const reason = _("This flag does not accept a value.");
addAnnotationIssue(annotationMap, token, reason);
errors.push(`${token.value}: ${reason}`);
}
index += 1;
continue;
}
if (mode === "optional") {
if (
nextToken &&
!token.value.includes("=") &&
!nextToken.value.startsWith("--")
) {
const reason = _(
"Optional values must be attached with '=' here; a separate token would be ignored by nfqws2.",
);
addAnnotationIssue(annotationMap, token, reason);
addAnnotationIssue(annotationMap, nextToken, reason);
errors.push(`${token.value} ${nextToken.value}: ${reason}`);
index += 2;
} else {
index += 1;
}
continue;
}
if (!token.value.includes("=")) {
if (!nextToken || nextToken.value.startsWith("--")) {
const reason = _("This option requires a value.");
addAnnotationIssue(annotationMap, token, reason);
errors.push(`${token.value}: ${reason}`);
index += 1;
continue;
}
index += 2;
continue;
}
index += 1;
}
if (!errors.length) {
return { valid: true, message: "", annotations: [] };
}
return {
valid: false,
message: [getValidationHeaderText(), ...errors].join("\n"),
annotations: finalizeAnnotations(annotationMap),
};
}
function analyzeNfqws2Strategy(value) {
const localAnalysis = buildNfqws2LocalAnalysis(value);
if (!localAnalysis.valid) {
return localAnalysis;
}
const remoteValidation = getCachedNfqws2RemoteValidation(value);
if (!remoteValidation || remoteValidation.valid) {
return localAnalysis;
}
const text = value ? `${value}` : "";
const tokens = parseNfqwsRuntimeTokens(text);
const annotationMap = new Map();
localAnalysis.annotations.forEach((annotation) =>
addAnnotationIssue(annotationMap, annotation, annotation.message),
);
addNfqwsRemoteValidationAnnotations(annotationMap, tokens, remoteValidation);
return {
valid: false,
message: [getValidationHeaderText(), remoteValidation.message].join("\n"),
annotations: finalizeAnnotations(annotationMap),
};
}
function normalizeByedpiStrategyWhitespace(value) {
return value ? `${value}`.replace(/\s+/g, " ").trim() : "";
}
function normalizeByedpiStrategyValue(value) {
const normalized = normalizeByedpiStrategyWhitespace(value);
return normalized.length ? normalized : BYEDPI_DEFAULT_CMD_OPTS;
}
function getCachedByedpiRemoteValidation(value) {
const normalized = normalizeByedpiStrategyValue(value);
return normalized.length
? byedpiRemoteValidationCache.get(normalized) || null
: null;
}
function cacheByedpiRemoteValidation(value, result) {
const normalized = normalizeByedpiStrategyValue(value);
if (!normalized.length) {
return result;
}
const cached = {
valid: result && result.valid === true,
message: result && result.message ? `${result.message}` : "",
needle: result && result.needle ? `${result.needle}` : "",
needles:
result && Array.isArray(result.needles)
? result.needles.filter(Boolean).map((item) => `${item}`)
: result && result.needle
? [`${result.needle}`]
: [],
};
byedpiRemoteValidationCache.set(normalized, cached);
return cached;
}
function buildByedpiRemoteValidationFallback(error) {
const message =
error && error.message
? `${error.message}`
: _("Unable to validate the ByeDPI strategy through the backend parser.");
return {
valid: false,
message: _("Backend validation failed: %s").format(message),
needle: "",
needles: [],
};
}
function validateByedpiStrategyRemotely(value) {
const normalized = normalizeByedpiStrategyValue(value);
if (!normalized.length) {
return Promise.resolve({
valid: true,
message: "",
needle: "",
needles: [],
});
}
if (byedpiRemoteValidationCache.has(normalized)) {
return Promise.resolve(byedpiRemoteValidationCache.get(normalized));
}
if (byedpiRemoteValidationInflight.has(normalized)) {
return byedpiRemoteValidationInflight.get(normalized);
}
const validationTask = fs
.exec(NFQWS_VALIDATION_COMMAND, [
"validate_byedpi_strategy_json",
normalized,
])
.then((result) => {
const payload = JSON.parse(
(result && result.stdout ? result.stdout : "{}").trim() || "{}",
);
return cacheByedpiRemoteValidation(normalized, {
valid: payload.valid === true,
message: payload.message || "",
needle: payload.needle || "",
needles: Array.isArray(payload.needles)
? payload.needles.filter(Boolean)
: payload.needle
? [payload.needle]
: [],
});
})
.catch((error) =>
cacheByedpiRemoteValidation(
normalized,
buildByedpiRemoteValidationFallback(error),
),
)
.finally(() => {
byedpiRemoteValidationInflight.delete(normalized);
});
byedpiRemoteValidationInflight.set(normalized, validationTask);
return validationTask;
}
function getByedpiShortOptionName(token) {
return token.length > 2 ? token.slice(0, 2) : token;
}
function byedpiTokenLooksLikeOption(token) {
return /^--.+/.test(token) || /^-[A-Za-z].*/.test(token);
}
function getByedpiControlledTokenInfo(token) {
const listenMessage = _(
"ByeDPI listen address and port are assigned by Tachyon and must not be set in the strategy.",
);
const transparentMessage = _(
"Transparent proxy mode is incompatible with action=byedpi because Tachyon connects to ciadpi through SOCKS.",
);
const daemonMessage = _(
"Tachyon manages the ciadpi process lifecycle itself, so daemon mode is not allowed here.",
);
const pidfileMessage = _(
"Tachyon manages ciadpi pid files itself, so pidfile options are not allowed here.",
);
const exitMessage = _(
"This field must start a working ciadpi strategy; help/version options exit immediately and are not allowed here.",
);
if (
token === "--ip" ||
token.startsWith("--ip=") ||
token === "-i" ||
/^-i.+/.test(token) ||
token === "--port" ||
token.startsWith("--port=") ||
token === "-p" ||
/^-p.+/.test(token)
) {
return {
reason: listenMessage,
captureNextValue:
token === "--ip" ||
token === "-i" ||
token === "--port" ||
token === "-p",
};
}
if (token === "--transparent" || token === "-E" || /^-E.+/.test(token)) {
return {
reason: transparentMessage,
captureNextValue: false,
};
}
if (token === "--daemon" || token === "-D" || /^-D.+/.test(token)) {
return {
reason: daemonMessage,
captureNextValue: false,
};
}
if (
token === "--pidfile" ||
token.startsWith("--pidfile=") ||
token === "-w" ||
/^-w.+/.test(token)
) {
return {
reason: pidfileMessage,
captureNextValue: token === "--pidfile" || token === "-w",
};
}
if (
token === "--help" ||
token === "-h" ||
/^-h.+/.test(token) ||
token === "--version" ||
token === "-v" ||
/^-v.+/.test(token)
) {
return {
reason: exitMessage,
captureNextValue: false,
};
}
return null;
}
function validateByedpiStrategyToken(token, nextToken) {
const controlled = getByedpiControlledTokenInfo(token);
if (controlled) {
return {
valid: false,
reason: controlled.reason,
captureNextValue: controlled.captureNextValue,
};
}
if (/^--[^=]+=/.test(token)) {
const base = token.split("=", 1)[0];
const value = token.slice(base.length + 1);
if (BYEDPI_LONG_VALUE_OPTIONS.has(base)) {
return value.length
? { valid: true, consumeNext: false }
: {
valid: false,
reason: _("ByeDPI option requires a value: %s").format(base),
captureNextValue: false,
};
}
if (BYEDPI_LONG_FLAG_OPTIONS.has(base)) {
return {
valid: false,
reason: _("ByeDPI option does not accept a value: %s").format(base),
captureNextValue: false,
};
}
return {
valid: false,
reason: _("Unknown ByeDPI option: %s").format(base),
captureNextValue: false,
};
}
if (/^--.+/.test(token)) {
if (BYEDPI_LONG_VALUE_OPTIONS.has(token)) {
return nextToken && !byedpiTokenLooksLikeOption(nextToken)
? { valid: true, consumeNext: true }
: {
valid: false,
reason: _("ByeDPI option requires a value: %s").format(token),
captureNextValue: false,
};
}
if (BYEDPI_LONG_FLAG_OPTIONS.has(token)) {
return { valid: true, consumeNext: false };
}
return {
valid: false,
reason: _("Unknown ByeDPI option: %s").format(token),
captureNextValue: false,
};
}
if (/^-./.test(token)) {
if (token === "-") {
return {
valid: false,
reason: _("Unexpected ByeDPI strategy argument: %s").format(token),
captureNextValue: false,
};
}
const short = getByedpiShortOptionName(token);
const compactValue = token.slice(short.length);
if (BYEDPI_SHORT_VALUE_OPTIONS.has(short)) {
if (token === short) {
return nextToken && !byedpiTokenLooksLikeOption(nextToken)
? { valid: true, consumeNext: true }
: {
valid: false,
reason: _("ByeDPI option requires a value: %s").format(short),
captureNextValue: false,
};
}
return compactValue.length
? { valid: true, consumeNext: false }
: {
valid: false,
reason: _("ByeDPI option requires a value: %s").format(short),
captureNextValue: false,
};
}
if (BYEDPI_SHORT_FLAG_OPTIONS.has(short)) {
return token === short
? { valid: true, consumeNext: false }
: {
valid: false,
reason: _(
"ByeDPI option does not accept a compact value: %s",
).format(short),
captureNextValue: false,
};
}
return {
valid: false,
reason: _("Unknown ByeDPI option: %s").format(short),
captureNextValue: false,
};
}
return {
valid: false,
reason: _("Unexpected ByeDPI strategy argument: %s").format(token),
captureNextValue: false,
};
}
function buildByedpiLocalAnalysis(value) {
const text = value ? `${value}` : "";
if (!text.trim().length) {
return {
valid: false,
message: _("ByeDPI strategy cannot be empty"),
annotations: [],
};
}
const tokens = parseNfqwsRuntimeTokens(text);
const annotationMap = new Map();
const errors = [];
for (let index = 0; index < tokens.length; ) {
const token = tokens[index];
const nextToken = tokens[index + 1] || null;
const tokenValidation = validateByedpiStrategyToken(
token.value,
nextToken ? nextToken.value : null,
);
if (tokenValidation.valid) {
index += tokenValidation.consumeNext ? 2 : 1;
continue;
}
addAnnotationIssue(annotationMap, token, tokenValidation.reason);
let displayToken = token.value;
if (
tokenValidation.captureNextValue &&
nextToken &&
!nextToken.value.startsWith("-")
) {
addAnnotationIssue(annotationMap, nextToken, tokenValidation.reason);
displayToken = `${displayToken} ${nextToken.value}`;
index += 2;
} else {
index += 1;
}
errors.push(`${displayToken}: ${tokenValidation.reason}`);
}
if (!errors.length) {
return { valid: true, message: "", annotations: [] };
}
return {
valid: false,
message: [getValidationHeaderText(), ...errors].join("\n"),
annotations: finalizeAnnotations(annotationMap),
};
}
function analyzeByedpiStrategy(value) {
const localAnalysis = buildByedpiLocalAnalysis(value);
if (!localAnalysis.valid) {
return localAnalysis;
}
const remoteValidation = getCachedByedpiRemoteValidation(value);
if (!remoteValidation || remoteValidation.valid) {
return localAnalysis;
}
const text = value ? `${value}` : "";
const tokens = parseNfqwsRuntimeTokens(text);
const annotationMap = new Map();
localAnalysis.annotations.forEach((annotation) =>
addAnnotationIssue(annotationMap, annotation, annotation.message),
);
addNfqwsRemoteValidationAnnotations(annotationMap, tokens, remoteValidation);
return {
valid: false,
message: [getValidationHeaderText(), remoteValidation.message].join("\n"),
annotations: finalizeAnnotations(annotationMap),
};
}
function configureTextareaOption(option, analyzer, remoteValidationAttacher) {
const originalRenderWidget = option.renderWidget;
option.renderWidget = function (section_id, option_index, cfgvalue) {
const node = originalRenderWidget.call(
this,
section_id,
option_index,
cfgvalue,
);
const textarea =
node && typeof node.querySelector === "function"
? node.querySelector("textarea")
: node;
if (textarea) {
applyTextareaInputAttributes(textarea);
if (typeof analyzer === "function") {
attachAnnotatedTextarea(textarea, analyzer);
}
if (typeof remoteValidationAttacher === "function") {
remoteValidationAttacher(this, section_id, textarea);
}
}
return node;
};
}
function getOptionTextarea(option, section_id) {
const field =
typeof option.map.findElement === "function"
? option.map.findElement("data-field", option.cbid(section_id))
: null;
if (field && typeof field.querySelector === "function") {
return field.querySelector("textarea");
}
const elem =
typeof option.getUIElement === "function"
? option.getUIElement(section_id)
: null;
const node = elem && elem.node ? elem.node : null;
if (node && node.nodeName === "TEXTAREA") {
return node;
}
return node && typeof node.querySelector === "function"
? node.querySelector("textarea")
: null;
}
function rejectStrategyValidation(option, section_id, message) {
const title = option.stripTags(option.title).trim();
const error = message || option.getValidationError(section_id) || "";
return Promise.reject(
new TypeError(
`${_('Option "%s" contains an invalid input value.').format(title || option.option)} ${error}`,
),
);
}
function parseStrategyWithRemoteValidation(section_id, config) {
const active = this.isActive(section_id);
if (active) {
if (typeof this.triggerValidation === "function") {
this.triggerValidation(section_id);
}
if (!this.isValid(section_id)) {
return rejectStrategyValidation(
this,
section_id,
this.getValidationError(section_id),
);
}
const cval = this.cfgvalue(section_id);
const fval = this.formvalue(section_id);
const cvalString = cval == null ? "" : `${cval}`;
const fvalString = fval == null ? "" : `${fval}`;
const shouldWrite = this.forcewrite || cvalString !== fvalString;
if (!shouldWrite) {
return Promise.resolve();
}
return config.remoteValidate(fvalString).then((result) => {
const textarea = getOptionTextarea(this, section_id);
if (textarea) {
refreshAnnotatedTextareaValidation(this, section_id, textarea);
}
if (typeof this.triggerValidation === "function") {
this.triggerValidation(section_id);
}
if (!result || result.valid !== true) {
return rejectStrategyValidation(
this,
section_id,
result && result.message ? result.message : config.invalidMessage,
);
}
return Promise.resolve(this.write(section_id, fvalString));
});
}
if (!this.retain) {
return Promise.resolve(this.remove(section_id));
}
return Promise.resolve();
}
function parseNfqwsStrategyOnSave(section_id) {
return parseStrategyWithRemoteValidation.call(this, section_id, {
remoteValidate: validateNfqwsStrategyRemotely,
invalidMessage: _(
"Unable to validate the NFQWS strategy through the backend parser.",
),
});
}
function parseNfqws2StrategyOnSave(section_id) {
return parseStrategyWithRemoteValidation.call(this, section_id, {
remoteValidate: validateNfqws2StrategyRemotely,
invalidMessage: _(
"Unable to validate the NFQWS2 strategy through the backend parser.",
),
});
}
function addDynamicConditionField(section, config) {
const o = section.taboption(
"conditions",
form.DynamicList,
config.key,
config.label,
config.description,
);
o.modalonly = true;
if (config.placeholder) {
o.placeholder = config.placeholder;
}
if (config.dynamicValidate) {
o.validate = config.dynamicValidate;
}
o.load = function (section_id) {
const values = getConfigListValues(section_id, config.key);
if (values.length) {
return values;
}
const legacyText = uci.get(UCI_PACKAGE, section_id, `${config.key}_text`);
return legacyText ? main.parseValueList(legacyText) : [];
};
o.write = function (section_id, value) {
writeListOption(section_id, config.key, value);
uci.unset(UCI_PACKAGE, section_id, `${config.key}_text`);
uci.unset(UCI_PACKAGE, section_id, `${config.key}_text_mode`);
};
return o;
}
function addLocalDeviceSubnetDynamicField(section, config) {
const o = section.taboption(
"conditions",
form.DynamicList,
config.key,
config.label,
config.description,
);
o.modalonly = true;
o.placeholder = _("Device or IP");
o.validate = function (_section_id, value) {
if (!value || value.length === 0) {
return true;
}
const validation = main.validateSubnet(value);
return validation.valid ? true : validation.message;
};
o.load = function (section_id) {
const values = getConfigListValues(section_id, config.key);
if (values.length) {
return values;
}
const legacyText = uci.get(UCI_PACKAGE, section_id, `${config.key}_text`);
return legacyText ? main.parseValueList(legacyText) : [];
};
o.write = function (section_id, value) {
writeListOption(section_id, config.key, value);
uci.unset(UCI_PACKAGE, section_id, `${config.key}_text`);
uci.unset(UCI_PACKAGE, section_id, `${config.key}_text_mode`);
};
o.renderWidget = function (section_id, _option_index, cfgvalue) {
return localDevices.createLocalDeviceDynamicListWidget(
this,
section_id,
cfgvalue,
);
};
return o;
}
function addTextConditionField(section, config) {
const optionName = config.optionName || `${config.key}_text`;
const legacyTextOptionName =
config.legacyTextOptionName || `${config.key}_text`;
const o = section.taboption(
"conditions",
form.TextValue,
optionName,
config.label,
config.description,
);
o.rows = 8;
o.wrap = "soft";
o.textarea = true;
o.modalonly = true;
if (config.textAnalyze) {
o.validate = function (_section_id, value) {
const analysis = config.textAnalyze(value);
return analysis.valid ? true : analysis.message;
};
} else if (config.textValidate) {
o.validate = config.textValidate;
}
configureTextareaOption(o, config.textAnalyze);
o.load = function (section_id) {
if (typeof config.loadText === "function") {
return config.loadText(section_id);
}
const textValue =
uci.get(UCI_PACKAGE, section_id, optionName) ||
uci.get(UCI_PACKAGE, section_id, legacyTextOptionName);
if (textValue) {
return valuesToText(textValue);
}
return valuesToText(uci.get(UCI_PACKAGE, section_id, config.key));
};
o.write = function (section_id, value) {
const normalized = value ? `${value}`.trim() : "";
if (normalized.length) {
uci.set(UCI_PACKAGE, section_id, optionName, normalized);
} else {
uci.unset(UCI_PACKAGE, section_id, optionName);
}
if (config.key !== optionName) {
uci.unset(UCI_PACKAGE, section_id, config.key);
}
if (legacyTextOptionName !== optionName) {
uci.unset(UCI_PACKAGE, section_id, legacyTextOptionName);
}
uci.unset(UCI_PACKAGE, section_id, `${config.key}_text_mode`);
if (typeof config.afterWrite === "function") {
config.afterWrite(section_id);
}
};
return o;
}
function loadRulesetValues(option) {
delete option.keylist;
delete option.vallist;
Object.entries(main.DOMAIN_LIST_OPTIONS).forEach(([key, label]) => {
option.value(key, _(label));
});
}
function isBuiltinRulesetValue(value) {
return Object.prototype.hasOwnProperty.call(main.DOMAIN_LIST_OPTIONS, value);
}
function normalizeReferenceForExtensionCheck(value) {
return `${value || ""}`.split(/[?#]/, 1)[0].toLowerCase();
}
function hasAllowedReferenceExtension(value, extensions) {
const normalized = normalizeReferenceForExtensionCheck(value);
return extensions.some((extension) => normalized.endsWith(extension));
}
function validateFileReference(value, extensions, errorMessage, options = {}) {
if (!value || value.length === 0) {
return true;
}
if (value.startsWith("http://") || value.startsWith("https://")) {
const validation = main.validateUrl(value);
if (
validation.valid &&
(options.allowRemoteWithoutExtension ||
hasAllowedReferenceExtension(value, extensions))
) {
return true;
}
return errorMessage;
}
if (value.startsWith("/")) {
const validation = main.validatePath(value);
if (validation.valid && hasAllowedReferenceExtension(value, extensions)) {
return true;
}
return errorMessage;
}
return errorMessage;
}
function validateCustomRulesetReference(value) {
return validateFileReference(
value,
[".srs", ".json"],
_("Rule set must be an HTTP(S) URL or a local .srs / .json path"),
{ allowRemoteWithoutExtension: true },
);
}
function validatePlainListReference(value) {
return validateFileReference(
value,
[".lst"],
_("List must be an HTTP(S) URL or a local .lst path"),
{ allowRemoteWithoutExtension: true },
);
}
function getRulesetReferences(section_id) {
return getConfigListValues(section_id, "rule_set");
}
function getBuiltInRulesetReferences(section_id) {
const values = getConfigListValues(section_id, "community_lists").filter(
(value) => isBuiltinRulesetValue(value),
);
return values.filter(
(value, index, values) =>
isBuiltinRulesetValue(value) && values.indexOf(value) === index,
);
}
function getCustomRulesetReferences(section_id) {
return uniqueDynamicListItems([
...getRulesetReferences(section_id).filter(
(value) => !isBuiltinRulesetValue(value),
),
...getConfigListValues(section_id, "rule_set_with_subnets"),
]);
}
function writeBuiltInRulesetReferences(section_id, values) {
const refs = normalizeDynamicListItems(values).filter((value) =>
isBuiltinRulesetValue(value),
);
writeListOption(section_id, "community_lists", refs);
}
function writeCustomRulesetReferences(section_id, values) {
const refs = uniqueDynamicListItems(values);
if (getRuleResolvedAction(section_id) === "dns") {
writeDnsRulesetReferences(section_id, refs);
return;
}
const subnetRefs = getConfigListValues(
section_id,
"rule_set_with_subnets",
).filter((value) => refs.includes(value));
const subnetRefSet = new Set(subnetRefs);
writeListOption(
section_id,
"rule_set",
refs.filter((value) => !subnetRefSet.has(value)),
);
writeListOption(section_id, "rule_set_with_subnets", subnetRefs);
uci.unset(UCI_PACKAGE, section_id, RULE_SET_ITEM_SETTINGS_KEY);
}
function writeDnsRulesetReferences(section_id, values) {
writeListOption(section_id, "rule_set", uniqueDynamicListItems(values));
uci.unset(UCI_PACKAGE, section_id, "rule_set_with_subnets");
uci.unset(UCI_PACKAGE, section_id, RULE_SET_ITEM_SETTINGS_KEY);
}
function createSectionContent(section) {
let o;
section.tab("settings", _("Settings"));
section.tab("conditions", _("Conditions"));
o = section.taboption("settings", form.Flag, "enabled", _("Enable"));
o.default = "1";
o.rmempty = false;
o.editable = true;
o.width = "6rem";
o = section.taboption(
"settings",
form.DummyValue,
"_action_display",
_("Action"),
);
o.modalonly = false;
o.rawhtml = true;
o.cfgvalue = function (section_id) {
return getRuleActionDisplayMarkup(section_id);
};
o.textvalue = function (section_id) {
return getRuleActionDisplayValue(section_id);
};
o.width = "7rem";
o = section.taboption(
"settings",
form.Value,
"label",
_("Section name"),
_("Visible name of this section"),
);
o.rmempty = false;
o.modalonly = true;
o.load = function (section_id) {
return uci.get(UCI_PACKAGE, section_id, "label") || section_id;
};
o = section.taboption(
"settings",
form.ListValue,
"action",
_("Action"),
_("What Tachyon should do when this section matches"),
);
populateActionOptionValues(o);
o.default = "connection";
o.rmempty = false;
o.modalonly = true;
o.cfgvalue = function (section_id) {
return getRuleConfiguredAction(section_id);
};
o.load = function (section_id) {
return ensureActionProvidersAvailabilityLoaded().then(() => {
populateActionOptionValues(this);
return this.cfgvalue(section_id);
});
};
o = section.taboption(
"settings",
form.ListValue,
"dns_type",
_("DNS protocol"),
_("DNS protocol used by the resolver"),
);
o.depends("action", "dns");
dnsTypeChoices().forEach((choice) => o.value(choice.value, choice.label));
o.default = "udp";
o.rmempty = false;
o.modalonly = true;
o = section.taboption(
"settings",
form.Value,
"dns_server",
_("DNS server"),
_("DNS server used by the resolver"),
);
o.depends("action", "dns");
o.rmempty = false;
o.modalonly = true;
o.validate = function (_section_id, value) {
const normalized = `${value || ""}`.trim();
if (!normalized) {
return _("DNS server address cannot be empty");
}
const validation = main.validateDNS(normalized);
return validation.valid ? true : _("Enter a valid DNS server address");
};
o = section.taboption(
"settings",
form.Flag,
"dns_detour_enabled",
_("DNS through section"),
_("Route requests to this DNS server through another section."),
);
o.depends("action", "dns");
o.default = "0";
o.rmempty = false;
o.modalonly = true;
o = section.taboption(
"settings",
form.ListValue,
"dns_detour_section",
_("DNS requests through section"),
);
o.depends({ action: "dns", dns_detour_enabled: "1" });
o.rmempty = false;
o.modalonly = true;
o.load = function (section_id) {
refreshDnsDetourSectionOptionValues(this, section_id);
return uci.get(UCI_PACKAGE, section_id, "dns_detour_section") || "";
};
o.validate = function (_section_id, value) {
return value ? true : _("Select a section");
};
o = section.taboption(
"settings",
form.TextValue,
"nfqws_opt",
_("NFQWS Strategy"),
);
o.depends("action", "zapret");
o.rows = 6;
o.wrap = "soft";
o.textarea = true;
o.modalonly = true;
o.load = function (section_id) {
const value = uci.get(UCI_PACKAGE, section_id, "nfqws_opt");
if (!value || value === ZAPRET_LEGACY_DEFAULT_NFQWS_OPT) {
return ZAPRET_DEFAULT_NFQWS_OPT;
}
return value;
};
o.write = function (section_id, value) {
const normalized = normalizeNfqwsStrategyValue(value);
const nextValue =
!normalized.length || normalized === ZAPRET_LEGACY_DEFAULT_NFQWS_OPT
? ZAPRET_DEFAULT_NFQWS_OPT
: normalized;
return validateNfqwsStrategyRemotely(nextValue).then((result) => {
if (!result || result.valid !== true) {
throw new TypeError(
result && result.message
? result.message
: _(
"Unable to validate the NFQWS strategy through the backend parser.",
),
);
}
uci.set(UCI_PACKAGE, section_id, "nfqws_opt", nextValue);
});
};
o.validate = function (_section_id, value) {
const analysis = analyzeNfqwsStrategy(value);
return analysis.valid ? true : analysis.message;
};
o.parse = parseNfqwsStrategyOnSave;
configureTextareaOption(o, analyzeNfqwsStrategy, attachNfqwsRemoteValidation);
o = section.taboption(
"settings",
form.TextValue,
"nfqws2_opt",
_("NFQWS2 Strategy"),
);
o.depends("action", "zapret2");
o.rows = 6;
o.wrap = "soft";
o.textarea = true;
o.modalonly = true;
o.load = function (section_id) {
return (
uci.get(UCI_PACKAGE, section_id, "nfqws2_opt") ||
ZAPRET2_DEFAULT_NFQWS2_OPT
);
};
o.write = function (section_id, value) {
const normalized = normalizeNfqws2StrategyValue(value);
return validateNfqws2StrategyRemotely(normalized).then((result) => {
if (!result || result.valid !== true) {
throw new TypeError(
result && result.message
? result.message
: _("Invalid NFQWS2 strategy"),
);
}
uci.set(UCI_PACKAGE, section_id, "nfqws2_opt", normalized);
});
};
o.validate = function (_section_id, value) {
const analysis = analyzeNfqws2Strategy(value);
return analysis.valid ? true : analysis.message;
};
o.parse = parseNfqws2StrategyOnSave;
configureTextareaOption(
o,
analyzeNfqws2Strategy,
attachNfqws2RemoteValidation,
);
o = section.taboption(
"settings",
form.TextValue,
"byedpi_cmd_opts",
_("ByeDPI Strategy"),
);
o.depends("action", "byedpi");
o.rows = 6;
o.wrap = "soft";
o.textarea = true;
o.modalonly = true;
o.load = function (section_id) {
return (
uci.get(UCI_PACKAGE, section_id, "byedpi_cmd_opts") ||
BYEDPI_DEFAULT_CMD_OPTS
);
};
o.write = function (section_id, value) {
const normalized = normalizeByedpiStrategyValue(value);
return validateByedpiStrategyRemotely(normalized).then((result) => {
if (!result || result.valid !== true) {
throw new TypeError(
result && result.message
? result.message
: _("Invalid ByeDPI strategy"),
);
}
uci.set(UCI_PACKAGE, section_id, "byedpi_cmd_opts", normalized);
});
};
o.validate = function (_section_id, value) {
const analysis = analyzeByedpiStrategy(value);
return analysis.valid ? true : analysis.message;
};
configureTextareaOption(o, analyzeByedpiStrategy);
o = section.taboption(
"settings",
form.Button,
"_generate_warp",
_("AmneziaWG Generator"),
_("Automatically generate a Cloudflare WARP account and configure all AmneziaWG settings.")
);
o.modalonly = true;
o.inputtitle = _("Generate WARP Config");
o.depends("action", "awg");
// Custom render: beautiful gradient button with loading / success / error states
o.renderWidget = function(section_id) {
if (!document.getElementById("tachyon-warp-btn-css")) {
const s = document.createElement("style");
s.id = "tachyon-warp-btn-css";
s.textContent = `
.twg-spinner {
width: 13px; height: 13px;
border: 1.5px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
display: none;
flex-shrink: 0;
animation: twg-spin 0.7s linear infinite;
opacity: 0.6;
}
.twg-loading .twg-spinner { display: inline-block; }
.twg-loading .twg-icon { display: none; }
.twg-icon { flex-shrink: 0; }
.twg-btn.twg-loading { opacity: 0.65; }
.twg-btn.twg-success { color: #4caf50 !important; border-color: #4caf50 !important; }
.twg-btn.twg-error { color: #ef5350 !important; border-color: #ef5350 !important; animation: twg-shake 0.4s ease; }
@keyframes twg-spin { to { transform: rotate(360deg); } }
@keyframes twg-shake {
0%,100% { transform: translateX(0); }
20% { transform: translateX(-5px); }
60% { transform: translateX(5px); }
80% { transform: translateX(-3px); }
}
`;
document.head.appendChild(s);
}
const spinner = E("span", { "class": "twg-spinner" });
const icon = E("span", { "class": "twg-icon" }, ["↺"]);
const label = E("span", { "class": "twg-label" }, [_("Generate WARP Config")]);
const btn = E("button", {
"class": "btn cbi-button cbi-button-neutral twg-btn",
"type": "button",
"style": "display:inline-flex;align-items:center;gap:8px;"
}, [spinner, icon, label]);
btn.addEventListener("click", (ev) => this.onclick && this.onclick.call(this, ev, section_id));
return btn;
};
o.onclick = function(ev, section_id) {
const btn = ev.target.closest(".twg-btn");
const label = btn ? btn.querySelector(".twg-label") : null;
const origText = _("Generate WARP Config");
const setState = (state, text) => {
if (!btn) return;
btn.className = "btn cbi-button cbi-button-neutral twg-btn" + (state ? " twg-" + state : "");
btn.disabled = (state === "loading");
if (label && text) label.textContent = text;
};
const showStatusModal = (title, msg, isError) =>
ui.addNotification(title, E("p", {}, msg), isError ? "danger" : "success");
const resetAfter = (ms) =>
setTimeout(() => setState("", origText), ms);
setState("loading", _("Generating…"));
fs.exec("/usr/bin/tachyon", ["generate_warp"])
.then(function(response) {
if (!response || (response.code ?? 0) !== 0 || !response.stdout) {
setState("error", _("Error"));
resetAfter(2500);
showStatusModal(_("Error"), _("Failed to generate WARP config: ") + (response?.stderr || "Unknown error"), true);
return;
}
try {
const data = JSON.parse(response.stdout);
if (!data.success) {
setState("error", _("Error"));
resetAfter(2500);
showStatusModal(_("Error"), _("Failed to generate WARP config: ") + (data.message || "Unknown error"), true);
return;
}
const getControlWidget = (opt) => {
const widget = document.getElementById(`widget.cbid.${UCI_PACKAGE}.${section_id}.${opt}`);
if (!widget) return null;
if (/^(INPUT|SELECT|TEXTAREA)$/.test(widget.tagName)) return widget;
return widget.querySelector("input:not([type='hidden']), select, textarea");
};
const stripUciBinary = (val) => {
if (typeof val !== "string") return val;
return val.replace(/<b 0x/g, "").replace(/>/g, "").replace(/\s/g, "");
};
const setWidgetValue = (opt, val) => {
const widget = getControlWidget(opt);
if (widget) {
widget.value = val;
widget.dispatchEvent(new Event("input", { bubbles: true }));
widget.dispatchEvent(new Event("change", { bubbles: true }));
}
uci.set(UCI_PACKAGE, section_id, opt, val);
};
setWidgetValue("awg_local_address", data.local_address);
setWidgetValue("awg_private_key", data.private_key);
setWidgetValue("awg_peer_public_key", data.peer_public_key);
setWidgetValue("awg_server_address", data.server_address);
setWidgetValue("awg_server_port", data.server_port);
setWidgetValue("awg_jc", `${data.awg_jc}`);
setWidgetValue("awg_jmin", `${data.awg_jmin}`);
setWidgetValue("awg_jmax", `${data.awg_jmax}`);
setWidgetValue("awg_s1", `${data.awg_s1}`);
setWidgetValue("awg_s2", `${data.awg_s2}`);
setWidgetValue("awg_h1", `${data.awg_h1}`);
setWidgetValue("awg_h2", `${data.awg_h2}`);
setWidgetValue("awg_h3", `${data.awg_h3}`);
setWidgetValue("awg_h4", `${data.awg_h4}`);
setWidgetValue("awg_s3", `${data.awg_s3}`);
setWidgetValue("awg_s4", `${data.awg_s4}`);
setWidgetValue("awg_i1", stripUciBinary(data.awg_i1));
setWidgetValue("awg_i2", stripUciBinary(data.awg_i2));
setWidgetValue("awg_i3", stripUciBinary(data.awg_i3));
setWidgetValue("awg_i4", stripUciBinary(data.awg_i4));
setWidgetValue("awg_i5", stripUciBinary(data.awg_i5));
setState("success", _("Generated!"));
resetAfter(2500);
showStatusModal(_("Success"), _("WARP Configuration generated and loaded successfully!"), false);
} catch (err) {
setState("error", _("Error"));
resetAfter(2500);
showStatusModal(_("Error"), _("Failed to parse WARP config: ") + err.message, true);
}
})
.catch(function(error) {
setState("error", _("Error"));
resetAfter(2500);
showStatusModal(_("Error"), _("Error running generator: ") + error.message, true);
});
};
o = section.taboption(
"settings",
form.Value,
"awg_local_address",
_("Local Address"),
);
o.modalonly = true;
o.rmempty = false;
o.depends("action", "awg");
o.validate = validateRequiredText;
o = section.taboption(
"settings",
form.Value,
"awg_private_key",
_("Private Key"),
);
o.modalonly = true;
o.rmempty = false;
o.depends("action", "awg");
o.validate = validateRequiredText;
o = section.taboption(
"settings",
form.Value,
"awg_peer_public_key",
_("Peer Public Key"),
);
o.modalonly = true;
o.rmempty = false;
o.depends("action", "awg");
o.validate = validateRequiredText;
o = section.taboption(
"settings",
form.Value,
"awg_server_address",
_("Server Address"),
);
o.modalonly = true;
o.rmempty = false;
o.depends("action", "awg");
o.validate = validateRequiredText;
o = section.taboption(
"settings",
form.Value,
"awg_server_port",
_("Server Port"),
);
o.modalonly = true;
o.rmempty = false;
o.datatype = "port";
o.depends("action", "awg");
o.validate = validateRequiredText;
o = section.taboption(
"settings",
form.Value,
"awg_preshared_key",
_("Preshared Key"),
);
o.modalonly = true;
o.rmempty = true;
o.depends("action", "awg");
o = section.taboption("settings", form.Value, "awg_jc", "Jc");
o.default = "120";
o.modalonly = true;
o.rmempty = false;
o.depends("action", "awg");
o = section.taboption("settings", form.Value, "awg_jmin", "Jmin");
o.default = "23";
o.modalonly = true;
o.rmempty = false;
o.depends("action", "awg");
o = section.taboption("settings", form.Value, "awg_jmax", "Jmax");
o.default = "911";
o.modalonly = true;
o.rmempty = false;
o.depends("action", "awg");
o = section.taboption("settings", form.Value, "awg_s1", "S1");
o.default = "0";
o.modalonly = true;
o.rmempty = false;
o.depends("action", "awg");
o = section.taboption("settings", form.Value, "awg_s2", "S2");
o.default = "0";
o.modalonly = true;
o.rmempty = false;
o.depends("action", "awg");
o = section.taboption("settings", form.Value, "awg_h1", "H1");
o.default = "1";
o.modalonly = true;
o.rmempty = false;
o.depends("action", "awg");
o = section.taboption("settings", form.Value, "awg_h2", "H2");
o.default = "2";
o.modalonly = true;
o.rmempty = false;
o.depends("action", "awg");
o = section.taboption("settings", form.Value, "awg_h3", "H3");
o.default = "3";
o.modalonly = true;
o.rmempty = false;
o.depends("action", "awg");
o = section.taboption("settings", form.Value, "awg_h4", "H4");
o.default = "4";
o.modalonly = true;
o.rmempty = false;
o.depends("action", "awg");
o = section.taboption("settings", form.Value, "awg_s3", "S3");
o.default = "0";
o.modalonly = true;
o.rmempty = false;
o.depends("action", "awg");
o = section.taboption("settings", form.Value, "awg_s4", "S4");
o.default = "0";
o.modalonly = true;
o.rmempty = false;
o.depends("action", "awg");
o = section.taboption("settings", form.Value, "awg_i1", "I1");
o.default = "0";
o.modalonly = true;
o.rmempty = false;
o.depends("action", "awg");
o = section.taboption("settings", form.Value, "awg_i2", "I2");
o.default = "0";
o.modalonly = true;
o.rmempty = false;
o.depends("action", "awg");
o = section.taboption("settings", form.Value, "awg_i3", "I3");
o.default = "0";
o.modalonly = true;
o.rmempty = false;
o.depends("action", "awg");
o = section.taboption("settings", form.Value, "awg_i4", "I4");
o.default = "0";
o.modalonly = true;
o.rmempty = false;
o.depends("action", "awg");
o = section.taboption("settings", form.Value, "awg_i5", "I5");
o.default = "0";
o.modalonly = true;
o.rmempty = false;
o.depends("action", "awg");
// ── WARP (Cloudflare WARP via sing-box-extended) ──────────────────────────
o = section.taboption(
"settings",
form.Value,
"warp_private_key",
_("WARP Private Key"),
_("WireGuard private key from your Cloudflare WARP account"),
);
o.modalonly = true;
o.rmempty = false;
o.depends("action", "warp");
o.validate = validateRequiredText;
o = section.taboption(
"settings",
form.Value,
"warp_account_id",
_("WARP Account ID"),
_("Cloudflare WARP account / device ID"),
);
o.modalonly = true;
o.rmempty = false;
o.depends("action", "warp");
o.validate = validateRequiredText;
o = section.taboption(
"settings",
form.Value,
"warp_access_token",
_("WARP Access Token"),
_("Cloudflare WARP API access token"),
);
o.modalonly = true;
o.rmempty = true;
o.depends("action", "warp");
o = section.taboption(
"settings",
form.ListValue,
"warp_detour",
_("Registration Detour"),
_("Proxy to use for WARP API registration (api.cloudflareclient.com)"),
);
o.modalonly = true;
o.rmempty = true;
o.depends("action", "warp");
o.load = function (itemId) {
const choices = subscriptionDownloadTargetChoices(itemId);
choices.unshift({ value: "", label: _("Direct (Default)") });
refreshOptionChoices(this, choices);
return optionMapValue(this, itemId, "warp_detour") || "";
};
// ── AnyTLS (sing-box-extended) ────────────────────────────────────────────
o = section.taboption("settings", form.Value, "anytls_server", _("Server Address"));
o.modalonly = true;
o.rmempty = false;
o.depends("action", "anytls");
o.validate = validateRequiredText;
o = section.taboption("settings", form.Value, "anytls_server_port", _("Server Port"));
o.modalonly = true;
o.rmempty = false;
o.datatype = "port";
o.depends("action", "anytls");
o = section.taboption("settings", form.Value, "anytls_password", _("Password"));
o.modalonly = true;
o.rmempty = false;
o.password = true;
o.depends("action", "anytls");
o.validate = validateRequiredText;
o = section.taboption(
"settings",
form.Value,
"anytls_sni",
_("TLS SNI"),
_("Server Name Indication — leave empty to use server address"),
);
o.modalonly = true;
o.rmempty = true;
o.depends("action", "anytls");
o = section.taboption(
"settings",
form.Flag,
"anytls_insecure",
_("Allow Insecure TLS"),
_("Skip TLS certificate verification (not recommended)"),
);
o.modalonly = true;
o.rmempty = true;
o.depends("action", "anytls");
// ── Snell (sing-box-extended) ─────────────────────────────────────────────
o = section.taboption("settings", form.Value, "snell_server", _("Server Address"));
o.modalonly = true;
o.rmempty = false;
o.depends("action", "snell");
o.validate = validateRequiredText;
o = section.taboption("settings", form.Value, "snell_server_port", _("Server Port"));
o.modalonly = true;
o.rmempty = false;
o.datatype = "port";
o.depends("action", "snell");
o = section.taboption("settings", form.Value, "snell_psk", _("PSK"), _("Pre-shared key"));
o.modalonly = true;
o.rmempty = false;
o.password = true;
o.depends("action", "snell");
o.validate = validateRequiredText;
o = section.taboption("settings", form.ListValue, "snell_version", _("Snell Version"));
o.modalonly = true;
o.rmempty = true;
o.value("4", "v4");
o.value("3", "v3");
o.value("2", "v2");
o.value("1", "v1");
o.default = "4";
o.depends("action", "snell");
// ── Mieru (sing-box-extended) ─────────────────────────────────────────────
o = section.taboption("settings", form.Value, "mieru_server", _("Server Address"));
o.modalonly = true;
o.rmempty = false;
o.depends("action", "mieru");
o.validate = validateRequiredText;
o = section.taboption("settings", form.Value, "mieru_server_port", _("Server Port"));
o.modalonly = true;
o.rmempty = false;
o.datatype = "port";
o.depends("action", "mieru");
o = section.taboption("settings", form.ListValue, "mieru_transport", _("Transport"));
o.modalonly = true;
o.rmempty = true;
o.value("TCP", "TCP");
o.value("UDP", "UDP");
o.default = "TCP";
o.depends("action", "mieru");
o = section.taboption("settings", form.Value, "mieru_username", _("Username"));
o.modalonly = true;
o.rmempty = false;
o.depends("action", "mieru");
o.validate = validateRequiredText;
o = section.taboption("settings", form.Value, "mieru_password", _("Password"));
o.modalonly = true;
o.rmempty = false;
o.password = true;
o.depends("action", "mieru");
o.validate = validateRequiredText;
// ── Sudoku (sing-box-extended) ────────────────────────────────────────────
o = section.taboption("settings", form.Value, "sudoku_server", _("Server Address"));
o.modalonly = true;
o.rmempty = false;
o.depends("action", "sudoku");
o.validate = validateRequiredText;
o = section.taboption("settings", form.Value, "sudoku_server_port", _("Server Port"));
o.modalonly = true;
o.rmempty = false;
o.datatype = "port";
o.depends("action", "sudoku");
o = section.taboption("settings", form.Value, "sudoku_key", _("Key"));
o.modalonly = true;
o.rmempty = false;
o.password = true;
o.depends("action", "sudoku");
o.validate = validateRequiredText;
o = section.taboption(
"settings",
form.Value,
"sudoku_aead_method",
_("AEAD Method"),
_("Optional — leave empty for default"),
);
o.modalonly = true;
o.rmempty = true;
o.depends("action", "sudoku");
// ── MASQUE (Cloudflare MASQUE via sing-box-extended) ──────────────────────
o = section.taboption(
"settings",
form.Value,
"masque_private_key",
_("MASQUE Private Key"),
_("WireGuard private key from your Cloudflare account"),
);
o.modalonly = true;
o.rmempty = false;
o.depends("action", "masque");
o.validate = validateRequiredText;
o = section.taboption(
"settings",
form.Value,
"masque_account_id",
_("MASQUE Account ID"),
_("Cloudflare account / device ID"),
);
o.modalonly = true;
o.rmempty = false;
o.depends("action", "masque");
o.validate = validateRequiredText;
o = section.taboption(
"settings",
form.Value,
"masque_access_token",
_("MASQUE Access Token"),
_("Cloudflare API access token"),
);
o.modalonly = true;
o.rmempty = false;
o.depends("action", "masque");
o.validate = validateRequiredText;
// ── OpenVPN (sing-box-extended) ───────────────────────────────────────────
o = section.taboption("settings", form.Value, "openvpn_server", _("Server Address"));
o.modalonly = true;
o.rmempty = false;
o.depends("action", "openvpn");
o.validate = validateRequiredText;
o = section.taboption("settings", form.Value, "openvpn_server_port", _("Server Port"));
o.modalonly = true;
o.rmempty = false;
o.datatype = "port";
o.placeholder = "1194";
o.depends("action", "openvpn");
o = section.taboption("settings", form.ListValue, "openvpn_proto", _("Protocol"));
o.modalonly = true;
o.rmempty = true;
o.value("udp", "UDP");
o.value("tcp", "TCP");
o.default = "udp";
o.depends("action", "openvpn");
o = section.taboption(
"settings",
form.Value,
"openvpn_cipher",
_("Cipher"),
_("e.g. AES-256-GCM — leave empty for default"),
);
o.modalonly = true;
o.rmempty = true;
o.depends("action", "openvpn");
o = section.taboption(
"settings",
form.Value,
"openvpn_auth",
_("Auth Digest"),
_("e.g. SHA256 — leave empty for default"),
);
o.modalonly = true;
o.rmempty = true;
o.depends("action", "openvpn");
o = section.taboption(
"settings",
form.TextValue,
"openvpn_ca",
_("CA Certificate"),
_("PEM-encoded CA certificate"),
);
o.modalonly = true;
o.rmempty = true;
o.rows = 6;
o.depends("action", "openvpn");
o = section.taboption(
"settings",
form.TextValue,
"openvpn_cert",
_("Client Certificate"),
_("PEM-encoded client certificate"),
);
o.modalonly = true;
o.rmempty = true;
o.rows = 6;
o.depends("action", "openvpn");
o = section.taboption(
"settings",
form.TextValue,
"openvpn_key",
_("Client Key"),
_("PEM-encoded client private key"),
);
o.modalonly = true;
o.rmempty = true;
o.rows = 6;
o.depends("action", "openvpn");
o = section.taboption(
"settings",
form.TextValue,
"openvpn_tls_auth",
_("TLS Auth/Crypt Key"),
_("Optional tls-auth or tls-crypt key"),
);
o.modalonly = true;
o.rmempty = true;
o.rows = 6;
o.depends("action", "openvpn");
o = section.taboption(
"settings",
form.DynamicList,
"selector_proxy_links",
_("Connection URL"),
_(
"vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links",
),
);
o.depends("action", "connection");
o.rmempty = true;
o.modalonly = true;
o.validate = function (_section_id, value) {
if (!value || value.length === 0) {
return true;
}
const validation = main.validateProxyUrl(value);
return validation.valid ? true : validation.message;
};
o.onchange = function (_event, section_id) {
refreshDashboardFilterChoiceWidgets(section_id);
};
outboundNameSourceOptions.set("selector_proxy_links", o);
o = section.taboption(
"settings",
SettingsDynamicList,
"subscription_url",
_("Subscription URL"),
_("Enter the subscription URL"),
);
o.depends("action", "connection");
o.rmempty = true;
o.modalonly = true;
o.childType = "subscription_url";
o.childValueOption = "url";
o.childDefaults = defaultSubscriptionUrlSettings();
o.renderItemSettingsModal = showSubscriptionUrlSettingsModal;
o.hasItemSettings = function (section_id, value) {
const normalized = `${value || ""}`.trim();
if (isExistingChildItem(section_id, normalized, "subscription_url")) {
return true;
}
return this.validate(section_id, normalized) === true;
};
o.stagedChildSettings = function (section_id, value) {
const inputValue = childItemInputValue(
section_id,
value,
"subscription_url",
"url",
);
const store = childPendingSettingsStore(this, section_id);
return store[inputValue] ? Object.assign({}, store[inputValue]) : null;
};
o.clearStagedChildSettings = function (section_id) {
if (this.pendingChildSettings) {
delete this.pendingChildSettings[section_id];
}
};
o.renderListItemLabel = function (section_id, itemId) {
return childItemInputValue(section_id, itemId, "subscription_url", "url");
};
o.validate = validateSubscriptionUrlEntry;
o.validate = function (section_id, value) {
return validateSubscriptionUrlEntry(
section_id,
childItemInputValue(section_id, value, "subscription_url", "url"),
);
};
o = section.taboption(
"settings",
InterfaceSettingsDynamicList,
"interfaces",
_("Network Interface"),
_("Select network interface for VPN connection"),
);
o.depends("action", "connection");
o.rmempty = true;
o.modalonly = true;
o.placeholder = _("Select a network interface");
o.childType = "section_interface";
o.childValueOption = "name";
o.childDefaults = defaultInterfaceSettings();
o.renderItemSettingsModal = showInterfaceSettingsModal;
o.hasItemSettings = function (section_id, value) {
const normalized = `${value || ""}`.trim();
if (isExistingChildItem(section_id, normalized, "section_interface")) {
return true;
}
return this.validate(section_id, normalized) === true;
};
o.stagedChildSettings = function (section_id, value) {
const inputValue = childItemInputValue(
section_id,
value,
"section_interface",
"name",
);
const store = childPendingSettingsStore(this, section_id);
return store[inputValue] ? Object.assign({}, store[inputValue]) : null;
};
o.clearStagedChildSettings = function (section_id) {
if (this.pendingChildSettings) {
delete this.pendingChildSettings[section_id];
}
};
o.onListChange = refreshDashboardFilterChoiceWidgets;
outboundNameSourceOptions.set("interfaces", o);
o = section.taboption(
"settings",
ButtonAddSettingsDynamicList,
"outbound_jsons",
_("JSON outbound"),
_("Custom outbound configurations in JSON format"),
);
o.depends("action", "connection");
o.rmempty = true;
o.modalonly = true;
o.addButtonLabel = _("+ Add JSON outbound");
o.renderItemSettingsModal = showOutboundJsonSettingsModal;
o.renderListItemLabel = function (_section_id, value) {
return outboundJsonListItemLabel(value);
};
o.validateItemsOnSave = validateOutboundJsonItemsBeforeSave;
o.validate = function (_section_id, value) {
if (!value || value.length === 0) {
return true;
}
const validation = main.validateOutboundJson(value);
return validation.valid ? true : validation.message;
};
o.onListChange = refreshDashboardFilterChoiceWidgets;
outboundNameSourceOptions.set("outbound_jsons", o);
o = section.taboption(
"settings",
ButtonAddSettingsDynamicList,
"urltest",
_("URLTest"),
_("Server group for automatic lowest-latency selection"),
);
o.depends("action", "connection");
o.rmempty = true;
o.modalonly = true;
o.addButtonLabel = _("+ Add URLTest");
o.childType = "urltest";
o.childValueOption = "name";
o.childDefaults = urlTestChildDefaults();
o.renderItemSettingsModal = showUrlTestSettingsModal;
o.validateItemsOnSave = function (section_id, values) {
return validateUrlTestItemsBeforeSave(section_id, values, this);
};
o.hasItemSettings = function (section_id, value) {
const normalized = `${value || ""}`.trim();
if (isExistingChildItem(section_id, normalized, "urltest")) {
return true;
}
return normalized.length > 0;
};
o.inputValueForItem = function (section_id, value) {
const inputValue = childItemInputValue(
section_id,
value,
"urltest",
"name",
);
const store = childPendingSettingsStore(this, section_id);
return store[inputValue] && store[inputValue].name
? store[inputValue].name
: inputValue;
};
o.stagedChildSettings = function (section_id, value) {
const id = childItemInputValue(section_id, value, "urltest", "name");
const store = childPendingSettingsStore(this, section_id);
return store[id] ? Object.assign({}, store[id]) : null;
};
o.clearStagedChildSettings = function (section_id) {
if (this.pendingChildSettings) {
delete this.pendingChildSettings[section_id];
}
};
o.renderListItemLabel = function (section_id, itemId) {
return E(
"span",
{ class: "fkp-dynlist-label" },
this.inputValueForItem(section_id, itemId),
);
};
o.onListChange = refreshDashboardFilterChoiceWidgets;
sectionGroupSourceOptions.set("urltest", o);
o = section.taboption(
"settings",
ButtonAddSettingsDynamicList,
"priority_group",
_("Priority"),
_("Server group for priority failover"),
);
o.depends("action", "connection");
o.rmempty = true;
o.modalonly = true;
o.addButtonLabel = _("+ Add priority");
o.childType = "priority_group";
o.childValueOption = "name";
o.childDefaults = priorityGroupChildDefaults();
o.createId = () => randomPriorityGroupId();
o.renderItemSettingsModal = showPriorityGroupSettingsModal;
o.validateItemsOnSave = validatePriorityGroupItemsBeforeSave;
o.hasItemSettings = function (section_id, value) {
const normalized = `${value || ""}`.trim();
if (isExistingChildItem(section_id, normalized, "priority_group")) {
return true;
}
return normalized.length > 0;
};
o.inputValueForItem = function (section_id, value) {
return childItemInputValue(section_id, value, "priority_group", "name");
};
o.renderListItemLabel = function (section_id, itemId) {
return E(
"span",
{ class: "fkp-dynlist-label" },
this.inputValueForItem(section_id, itemId),
);
};
o.onListChange = refreshDashboardFilterChoiceWidgets;
sectionGroupSourceOptions.set("priority_group", o);
o = section.taboption(
"settings",
form.Flag,
"outbound_detour_enabled",
_("Cascade connection"),
_(
"Use another section as an intermediate hop to connect to servers in this section. Does not apply to network interfaces or JSON outbounds.",
),
);
o.default = "0";
o.rmempty = false;
o.depends("action", "connection");
o.modalonly = true;
o.write = function (section_id, value) {
if (value === "1") {
const currentValue =
uci.get(UCI_PACKAGE, section_id, "outbound_detour_section") || "";
const targetSections = getOutboundDetourTargetSections(section_id);
const currentIsValid = targetSections.some(
(targetSection) => getUciSectionName(targetSection) === currentValue,
);
const selectedValue = currentIsValid
? currentValue
: getDefaultOutboundDetourSection(section_id);
if (selectedValue) {
uci.set(
UCI_PACKAGE,
section_id,
"outbound_detour_section",
selectedValue,
);
}
}
return form.Flag.prototype.write.apply(this, arguments);
};
o = section.taboption(
"settings",
form.ListValue,
"outbound_detour_section",
_("Connect through"),
_("Select a transit section"),
);
o.rmempty = false;
o.depends({ action: "connection", outbound_detour_enabled: "1" });
o.modalonly = true;
o.load = function (section_id) {
refreshOutboundDetourSectionOptionValues(this, section_id);
return Promise.resolve(
uci.get(UCI_PACKAGE, section_id, "outbound_detour_section") || "",
);
};
o.validate = function (section_id, value) {
if (!value) {
return _("Select an intermediate section");
}
if (value === section_id) {
return _("Current section cannot be used as its own transit section");
}
return true;
};
o = section.taboption(
"settings",
form.Flag,
"sort_by_latency",
_("Sort by latency"),
_("Sorts servers in this section by lowest latency in the dashboard."),
);
o.default = "0";
o.rmempty = false;
o.depends("action", "connection");
o.modalonly = true;
o = section.taboption(
"settings",
form.Flag,
"mixed_proxy_enabled",
_("Enable Mixed Proxy"),
_("Expose this section as a local HTTP+SOCKS proxy"),
);
o.default = "0";
o.rmempty = false;
o.depends("action", "connection");
o.depends("action", "byedpi");
o.depends("action", "zapret");
o.depends("action", "zapret2");
o.modalonly = true;
o = section.taboption(
"settings",
form.Value,
"mixed_proxy_port",
_("Mixed Proxy Port"),
_("Port for the local mixed proxy of this section"),
);
o.rmempty = false;
o.depends({ action: "connection", mixed_proxy_enabled: "1" });
o.depends({ action: "byedpi", mixed_proxy_enabled: "1" });
o.depends({ action: "zapret", mixed_proxy_enabled: "1" });
o.depends({ action: "zapret2", mixed_proxy_enabled: "1" });
o.modalonly = true;
o.validate = function (_section_id, value) {
if (!value || value.length === 0) {
return _("Port cannot be empty");
}
const parsed = parseInt(value, 10);
if (!isNaN(parsed) && parsed >= 1 && parsed <= 65535) {
return true;
}
return _("Invalid port number. Must be between 1 and 65535");
};
o = section.taboption(
"settings",
form.Flag,
"mixed_proxy_auth_enabled",
_("Enable Mixed Proxy Authentication"),
_("Require a username and password for the local mixed proxy"),
);
o.default = "0";
o.rmempty = false;
o.depends({ action: "connection", mixed_proxy_enabled: "1" });
o.depends({ action: "byedpi", mixed_proxy_enabled: "1" });
o.depends({ action: "zapret", mixed_proxy_enabled: "1" });
o.depends({ action: "zapret2", mixed_proxy_enabled: "1" });
o.modalonly = true;
o = section.taboption(
"settings",
form.Value,
"mixed_proxy_username",
_("Mixed Proxy Username"),
);
o.rmempty = false;
o.depends({
action: "connection",
mixed_proxy_enabled: "1",
mixed_proxy_auth_enabled: "1",
});
o.depends({
action: "byedpi",
mixed_proxy_enabled: "1",
mixed_proxy_auth_enabled: "1",
});
o.depends({
action: "zapret",
mixed_proxy_enabled: "1",
mixed_proxy_auth_enabled: "1",
});
o.depends({
action: "zapret2",
mixed_proxy_enabled: "1",
mixed_proxy_auth_enabled: "1",
});
o.modalonly = true;
o.validate = function (_section_id, value) {
if (!value || value.length === 0) {
return _("Username cannot be empty");
}
return true;
};
o = section.taboption(
"settings",
form.Value,
"mixed_proxy_password",
_("Mixed Proxy Password"),
);
o.rmempty = false;
o.depends({
action: "connection",
mixed_proxy_enabled: "1",
mixed_proxy_auth_enabled: "1",
});
o.depends({
action: "byedpi",
mixed_proxy_enabled: "1",
mixed_proxy_auth_enabled: "1",
});
o.depends({
action: "zapret",
mixed_proxy_enabled: "1",
mixed_proxy_auth_enabled: "1",
});
o.depends({
action: "zapret2",
mixed_proxy_enabled: "1",
mixed_proxy_auth_enabled: "1",
});
o.modalonly = true;
o.validate = function (_section_id, value) {
if (!value || value.length === 0) {
return _("Password cannot be empty");
}
return true;
};
o = section.taboption(
"settings",
form.Flag,
"resolve_real_ip_for_routing",
_("Resolve real IP for routing"),
_(
"Resolve domain names before routing so sing-box can use real destination IPs.",
),
);
o.default = "0";
o.rmempty = false;
o.depends("action", "connection");
o.modalonly = true;
o.cfgvalue = function (section_id) {
const value = uci.get(
UCI_PACKAGE,
section_id,
"resolve_real_ip_for_routing",
);
if (value !== null && value !== undefined && value !== "") {
return value;
}
return getRuleResolvedAction(section_id) === "byedpi" ? "1" : "0";
};
addTextConditionField(section, {
key: "domain_suffix",
optionName: "domain",
legacyTextOptionName: "domain_suffix_text",
label: _("Domains"),
description: _(
"The rule applies to the domain and all its subdomains. Use full:, keyword:, or regex: prefixes for exact match, keyword match, or regular expression.",
),
textAnalyze: analyzeDomainSuffixText,
loadText: loadCombinedDomainText,
afterWrite: function (section_id) {
[
"domain_suffix",
"domain_suffix_text",
"domain_suffix_text_mode",
"domain_keyword",
"domain_regex",
"domain_text",
"domain_keyword_text",
"domain_regex_text",
"domain_text_mode",
"domain_keyword_text_mode",
"domain_regex_text_mode",
].forEach((key) => {
uci.unset(UCI_PACKAGE, section_id, key);
});
},
});
addTextConditionField(section, {
key: "user_domains",
optionName: "user_domains",
legacyTextOptionName: "user_domains_text",
label: _("Auto-detected Domains"),
description: _(
"Domains automatically detected by Smart Detect or added via Telegram bot. You can manually edit or remove them here.",
),
textAnalyze: analyzeDomainSuffixText,
});
const ipConditionOption = addTextConditionField(section, {
key: "ip_cidr",
optionName: "ip_cidr",
legacyTextOptionName: "ip_cidr_text",
label: _("IPs"),
description: _("Match destination IPs or subnets"),
textAnalyze: analyzeIpCidrText,
});
dependsOnRoutingAction(ipConditionOption);
const builtInRulesetOption = section.taboption(
"conditions",
form.DynamicList,
"community_lists",
_("Built-in rule sets"),
_("Select a predefined domain list"),
);
builtInRulesetOption.modalonly = true;
builtInRulesetOption.placeholder = _("Service list");
builtInRulesetOption.load = function (section_id) {
loadRulesetValues(this);
return getBuiltInRulesetReferences(section_id);
};
builtInRulesetOption.write = function (section_id, values) {
writeBuiltInRulesetReferences(section_id, values);
};
builtInRulesetOption.remove = function (section_id) {
uci.unset(UCI_PACKAGE, section_id, "community_lists");
};
let isProcessingBuiltIns = false;
builtInRulesetOption.onchange = function (_ev, section_id, value) {
if (isProcessingBuiltIns) {
return;
}
isProcessingBuiltIns = true;
try {
const values = Array.isArray(value)
? value.filter(Boolean)
: value
? [value]
: [];
let newValues = [...values];
const notifications = [];
const selectedRegionalOptions = main.REGIONAL_OPTIONS.filter((opt) =>
newValues.includes(opt),
);
if (selectedRegionalOptions.length > 1) {
const lastSelected =
selectedRegionalOptions[selectedRegionalOptions.length - 1];
const removedRegions = selectedRegionalOptions.slice(0, -1);
newValues = newValues.filter(
(v) => v === lastSelected || !main.REGIONAL_OPTIONS.includes(v),
);
notifications.push(
E("p", {}, [
E("strong", {}, _("Regional options cannot be used together")),
E("br"),
_(
"Warning: %s cannot be used together with %s. Previous selections have been removed.",
).format(removedRegions.join(", "), lastSelected),
]),
);
}
if (newValues.includes("russia_inside")) {
const removedServices = newValues.filter(
(v) => !main.ALLOWED_WITH_RUSSIA_INSIDE.includes(v),
);
if (removedServices.length > 0) {
newValues = newValues.filter((v) =>
main.ALLOWED_WITH_RUSSIA_INSIDE.includes(v),
);
notifications.push(
E("p", { class: "alert-message warning" }, [
E("strong", {}, _("Russia inside restrictions")),
E("br"),
_(
"Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.",
).format(
main.ALLOWED_WITH_RUSSIA_INSIDE.map(
(key) => main.DOMAIN_LIST_OPTIONS[key],
)
.filter((label) => label !== "Russia inside")
.join(", "),
removedServices.join(", "),
),
]),
);
}
}
if (
JSON.stringify(newValues.slice().sort()) !==
JSON.stringify(values.slice().sort())
) {
this.getUIElement(section_id).setValue(newValues);
}
notifications.forEach((notification) =>
ui.addNotification(null, notification),
);
} finally {
isProcessingBuiltIns = false;
}
};
const ruleSetOption = section.taboption(
"conditions",
SettingsDynamicList,
"rule_set",
_("Rule sets"),
_(
"Add URLs or local paths to .srs / .json lists. Only domain rules are supported.",
),
);
ruleSetOption.modalonly = true;
// Both widgets map to rule_set, so neither inactive view may erase shared storage.
ruleSetOption.retain = true;
dependsOnRoutingAction(ruleSetOption);
ruleSetOption.renderItemSettingsModal = showRuleSetSettingsModal;
ruleSetOption.load = function (section_id) {
return getCustomRulesetReferences(section_id);
};
ruleSetOption.write = function (section_id, value) {
writeCustomRulesetReferences(section_id, value);
};
ruleSetOption.remove = function (section_id) {
uci.unset(UCI_PACKAGE, section_id, "rule_set");
uci.unset(UCI_PACKAGE, section_id, "rule_set_with_subnets");
uci.unset(UCI_PACKAGE, section_id, RULE_SET_ITEM_SETTINGS_KEY);
};
ruleSetOption.validate = function (section_id, value) {
return validateCustomRulesetReference(value);
};
const dnsRuleSetOption = section.taboption(
"conditions",
form.DynamicList,
"_dns_rule_set",
_("Rule sets"),
_(
"Add URLs or local paths to .srs / .json lists. Only domain rules are supported.",
),
);
dnsRuleSetOption.depends("action", "dns");
dnsRuleSetOption.modalonly = true;
dnsRuleSetOption.retain = true;
dnsRuleSetOption.load = function (section_id) {
return getCustomRulesetReferences(section_id);
};
dnsRuleSetOption.write = function (section_id, value) {
writeDnsRulesetReferences(section_id, value);
};
dnsRuleSetOption.remove = function (section_id) {
writeDnsRulesetReferences(section_id, []);
};
dnsRuleSetOption.validate = function (_section_id, value) {
return validateCustomRulesetReference(value);
};
const domainIpListsOption = section.taboption(
"conditions",
form.DynamicList,
"domain_ip_lists",
_("Domain lists"),
_(
"Add URLs or local paths to .lst lists. Only domain rules are supported.",
),
);
domainIpListsOption.modalonly = true;
domainIpListsOption.load = function (section_id) {
return getConfigListValues(section_id, "domain_ip_lists");
};
domainIpListsOption.validate = function (_section_id, value) {
return validatePlainListReference(value);
};
const sourceIpOption = addLocalDeviceSubnetDynamicField(section, {
key: "source_ip_cidr",
label: _("Device filter"),
description: _(
"Apply section rules only to the specified local IP addresses",
),
});
dependsOnRoutingAction(sourceIpOption);
const fullyRoutedOption = addLocalDeviceSubnetDynamicField(section, {
key: "fully_routed_ips",
label: _("Forced device routing"),
description: _(
"All traffic from these IP addresses will be routed through the section unconditionally, ignoring all other conditions.",
),
});
dependsOnRoutingAction(fullyRoutedOption);
const excludedIpsOption = addLocalDeviceSubnetDynamicField(section, {
key: "excluded_ips",
label: _("Excluded devices"),
description: _(
"Traffic from these IP addresses will never be routed through this section, even if other conditions match.",
),
});
dependsOnRoutingAction(excludedIpsOption);
const portsOption = addDynamicConditionField(section, {
key: "ports",
label: _("Ports"),
description: _("Match destination ports. Use a single port or a range"),
dynamicValidate: validatePortCondition,
});
dependsOnRoutingAction(portsOption);
addDashboardServerFilterOptions(section);
}
function loadSectionTableOptions(sectionRef) {
const sectionIds = sectionRef.cfgsections();
const tasks = [];
for (let i = 0; i < sectionIds.length; i += 1) {
const sectionId = sectionIds[i];
for (let j = 0; j < sectionRef.children.length; j += 1) {
const option = sectionRef.children[j];
if (option.disable || option.modalonly) {
continue;
}
tasks.push(
Promise.resolve(option.load.call(option, sectionId)).then((value) => {
option.cfgvalue(sectionId, value);
}),
);
}
}
return Promise.all(tasks);
}
function configureSectionSection(sectionRef, options = {}) {
setActionProvidersAvailabilityLoader(options.loadActionProvidersAvailability);
sectionRef.load = function () {
// The table renders only non-modal fields; the cloned Add/Edit modal loads
// action/provider details when the user opens it.
return loadSectionTableOptions(this);
};
}
const EntryPoint = {
configureSectionSection,
createSectionContent,
setActionProvidersAvailabilityLoader,
};
return baseclass.extend(EntryPoint);
function validateRequiredText(_sectionId, value) {
const normalized = value ? String(value).trim() : "";
if (!normalized) {
return _("This field is required");
}
return /[\u0000-\u001F\u007F]/.test(normalized)
? _("Value must not contain control characters")
: true;
}