Simplify connection source settings and cascade

This commit is contained in:
ushan0v
2026-07-11 00:10:56 +03:00
parent 6577b341e1
commit 53b49cac06
24 changed files with 1368 additions and 1901 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+8 -35
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PODKOP PLUS\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-09 02:41+0300\n"
"PO-Revision-Date: 2026-07-09 02:41+0300\n"
"POT-Creation-Date: 2026-07-10 04:26+0300\n"
"PO-Revision-Date: 2026-07-10 04:26+0300\n"
"Last-Translator: ushan0v\n"
"Language-Team: none\n"
"Language: ru\n"
@@ -113,9 +113,6 @@ msgstr "ALPN"
msgid "Alter ID"
msgstr "Alter ID"
msgid "Applicable only for SOCKS and Shadowsocks links"
msgstr "Применимо только для ссылок SOCKS и Shadowsocks"
msgid "Apply section rules only to the specified local IP addresses"
msgstr "Применять правила секции только к указанным локальным IP-адресам"
@@ -305,9 +302,6 @@ msgstr "Подключаться через"
msgid "Connection URL"
msgstr "URL подключения"
msgid "Connection URL settings"
msgstr "Настройки URL подключения"
msgid "Connections are unavailable"
msgstr "Подключения недоступны"
@@ -413,30 +407,18 @@ msgstr "DNS через HTTPS (DoH)"
msgid "DNS over TLS (DoT)"
msgstr "DNS через TLS (DoT)"
msgid "DNS protocol"
msgstr "Протокол DNS"
msgid "DNS Protocol Type"
msgstr "Тип протокола DNS"
msgid "DNS protocol used by the resolver"
msgstr "DNS-протокол, который будет использовать резолвер"
msgid "DNS Rewrite TTL"
msgstr "Перезапись TTL для DNS"
msgid "DNS server"
msgstr "DNS-сервер"
msgid "DNS Server"
msgstr "DNS-сервер"
msgid "DNS server address cannot be empty"
msgstr "Адрес DNS-сервера не может быть пустым"
msgid "DNS server used by the resolver"
msgstr "DNS-сервер, который будет использовать резолвер"
msgid "Do not panic, everything can be fixed, just..."
msgstr "Не паникуйте, всё можно исправить, просто..."
@@ -455,9 +437,6 @@ msgstr "Не резолвится"
msgid "Domain and IP Lists"
msgstr "Списки доменов и IPs"
msgid "Domain Resolver"
msgstr "Резолвер доменов"
msgid "Domains"
msgstr "Домены"
@@ -518,9 +497,6 @@ msgstr "Включить автоматическое обновление уд
msgid "Enable autostart"
msgstr "Включить автостарт"
msgid "Enable built-in DNS resolver for domains handled by this section"
msgstr "Включить встроенный DNS-резолвер для доменов, обрабатываемых этой секцией"
msgid "Enable list updates"
msgstr "Включить обновление списков"
@@ -1100,9 +1076,6 @@ msgstr "Имя, которое отображается в списке уров
msgid "Network Interface"
msgstr "Сетевой интерфейс"
msgid "Network interface settings"
msgstr "Настройки сетевого интерфейса"
msgid "NFQUEUE range is available"
msgstr "Диапазон NFQUEUE свободен"
@@ -1337,6 +1310,9 @@ msgstr "Порты"
msgid "Preferred IP"
msgstr "Предпочитаемый IP"
msgid "Priority"
msgstr "Приоритет"
msgid "Priority details"
msgstr "Детали Priority"
@@ -1581,7 +1557,7 @@ msgid "Server group for automatic lowest-latency selection"
msgstr "Группа серверов для автоматического выбора по наименьшей задержке"
msgid "Server group for priority failover"
msgstr "Группа серверов для приоритетного failover"
msgstr "Группа автоматически выбирает рабочий узел на основе установленного приоритета."
msgid "Server information"
msgstr "Информация о сервере"
@@ -1877,9 +1853,6 @@ msgstr "Тип"
msgid "UDP (Unprotected DNS)"
msgstr "UDP (Незащищённый DNS)"
msgid "UDP over TCP"
msgstr "UDP через TCP"
msgid "Unable to validate the ByeDPI strategy through the backend parser."
msgstr "Не удалось проверить стратегию ByeDPI через backend-парсер."
@@ -2000,8 +1973,8 @@ msgstr "Используйте IP-адрес"
msgid "Use an IP listen address"
msgstr "Используйте IP-адрес для прослушивания"
msgid "Use another section as an intermediate hop for connecting to this one"
msgstr "Использовать другую секцию как промежуточный узел для подключения к этой"
msgid "Use another section as an intermediate hop to connect to servers in this section. Does not apply to network interfaces or JSON outbounds."
msgstr "Использовать другую секцию как промежуточный узел подключения к серверам этой секции. Не применяется к сетевым интерфейсам и JSON outbound."
msgid "Use CIDR prefixes like 192.168.1.0/24 or 2001:db8::/32"
msgstr "Используйте CIDR-префиксы, например 192.168.1.0/24 или 2001:db8::/32"
@@ -124,9 +124,7 @@ type PriorityConfig = {
};
type ChildType =
| 'connection_url'
| 'subscription_url'
| 'section_interface'
| 'urltest'
| 'priority_group'
| 'priority_level';
@@ -223,9 +221,7 @@ function compactSettingsMap(settings: Record<string, ItemSettings>) {
}
function hydrateConfigSections(configSections: Podkop.ConfigSection[]) {
const connectionUrls = childSections(configSections, 'connection_url');
const subscriptionUrls = childSections(configSections, 'subscription_url');
const interfaces = childSections(configSections, 'section_interface');
const urltests = childSections(configSections, 'urltest');
const priorityGroups = childSections(configSections, 'priority_group');
const priorityLevels = childSections(configSections, 'priority_level');
@@ -236,30 +232,10 @@ function hydrateConfigSections(configSections: Podkop.ConfigSection[]) {
}
const next: Podkop.ConfigSection = { ...section };
const connectionUrlItems = ownedChildSections(next, connectionUrls);
const subscriptionUrlItems = ownedChildSections(next, subscriptionUrls);
const interfaceItems = ownedChildSections(next, interfaces);
const urltestItems = ownedChildSections(next, urltests);
const priorityGroupItems = ownedChildSections(next, priorityGroups);
if (connectionUrlItems.length) {
const settings: Record<string, ItemSettings> = {};
next.selector_proxy_links = connectionUrlItems
.map((item) => item.url || '')
.filter(Boolean);
connectionUrlItems.forEach((item) => {
if (!item.url) {
return;
}
settings[item.url] = {
outbound_detour_enabled: item.outbound_detour_enabled,
outbound_detour_section: item.outbound_detour_section,
enable_udp_over_tcp: item.enable_udp_over_tcp,
};
});
next.connection_url_settings = compactSettingsMap(settings);
}
if (subscriptionUrlItems.length) {
const settings: Record<string, ItemSettings> = {};
next.subscription_urls = subscriptionUrlItems
@@ -287,24 +263,6 @@ function hydrateConfigSections(configSections: Podkop.ConfigSection[]) {
next.subscription_url_settings = compactSettingsMap(settings);
}
if (interfaceItems.length) {
const settings: Record<string, ItemSettings> = {};
next.interfaces = interfaceItems
.map((item) => item.name || '')
.filter(Boolean);
interfaceItems.forEach((item) => {
if (!item.name) {
return;
}
settings[item.name] = {
domain_resolver_enabled: item.domain_resolver_enabled,
domain_resolver_dns_type: item.domain_resolver_dns_type,
domain_resolver_dns_server: item.domain_resolver_dns_server,
};
});
next.interface_settings = compactSettingsMap(settings);
}
if (urltestItems.length) {
const settings: Record<string, ItemSettings> = {};
next.urltests = urltestItems.map((item) => item['.name']);
@@ -58,7 +58,6 @@ const PODKOP_MASK_AFTER_TOKEN_SPACE = [
'list fully_routed_ips',
'option dns_server',
'option bootstrap_dns_server',
'option domain_resolver_dns_server',
'option listen',
'option listen_port',
'option public_host',
@@ -157,11 +156,6 @@ function maskGlobalCheckLine(line: string) {
}
maskedLine = maskOptionPath(maskedLine, "option dns_server '");
maskedLine = maskOptionPath(
maskedLine,
"option domain_resolver_dns_server '",
);
return maskedLine;
}
-8
View File
@@ -236,9 +236,7 @@ export namespace Podkop {
| 'ruleset'
| 'section'
| 'server'
| 'connection_url'
| 'subscription_url'
| 'section_interface'
| 'urltest'
| 'priority_group'
| 'priority_level';
@@ -262,9 +260,7 @@ export namespace Podkop {
subscription_urls?: string[];
interfaces?: string[];
outbound_jsons?: string[];
connection_url_settings?: string;
subscription_url_settings?: string;
interface_settings?: string;
urltests?: string[];
urltest_settings?: string;
priority_groups?: string[];
@@ -323,7 +319,6 @@ export namespace Podkop {
regex?: string[];
outbound_detour_enabled?: '0' | '1';
outbound_detour_section?: string;
enable_udp_over_tcp?: '0' | '1';
download_via_proxy_enabled?: '0' | '1';
download_via_proxy_section?: string;
auto_user_agent?: '0' | '1';
@@ -334,9 +329,6 @@ export namespace Podkop {
include_urltest_groups?: '0' | '1';
hide_urltest_group_outbounds?: '0' | '1';
hide_detour_outbounds?: '0' | '1';
domain_resolver_enabled?: '0' | '1';
domain_resolver_dns_type?: string;
domain_resolver_dns_server?: string;
yacd_secret_key?: string;
}
@@ -3215,9 +3215,7 @@ function compactSettingsMap(settings) {
return Object.keys(settings).length ? JSON.stringify(settings) : void 0;
}
function hydrateConfigSections(configSections) {
const connectionUrls = childSections(configSections, "connection_url");
const subscriptionUrls = childSections(configSections, "subscription_url");
const interfaces = childSections(configSections, "section_interface");
const urltests = childSections(configSections, "urltest");
const priorityGroups = childSections(configSections, "priority_group");
const priorityLevels = childSections(configSections, "priority_level");
@@ -3226,26 +3224,9 @@ function hydrateConfigSections(configSections) {
return section;
}
const next = { ...section };
const connectionUrlItems = ownedChildSections(next, connectionUrls);
const subscriptionUrlItems = ownedChildSections(next, subscriptionUrls);
const interfaceItems = ownedChildSections(next, interfaces);
const urltestItems = ownedChildSections(next, urltests);
const priorityGroupItems = ownedChildSections(next, priorityGroups);
if (connectionUrlItems.length) {
const settings = {};
next.selector_proxy_links = connectionUrlItems.map((item) => item.url || "").filter(Boolean);
connectionUrlItems.forEach((item) => {
if (!item.url) {
return;
}
settings[item.url] = {
outbound_detour_enabled: item.outbound_detour_enabled,
outbound_detour_section: item.outbound_detour_section,
enable_udp_over_tcp: item.enable_udp_over_tcp
};
});
next.connection_url_settings = compactSettingsMap(settings);
}
if (subscriptionUrlItems.length) {
const settings = {};
next.subscription_urls = subscriptionUrlItems.map((item) => item.url || "").filter(Boolean);
@@ -3270,21 +3251,6 @@ function hydrateConfigSections(configSections) {
});
next.subscription_url_settings = compactSettingsMap(settings);
}
if (interfaceItems.length) {
const settings = {};
next.interfaces = interfaceItems.map((item) => item.name || "").filter(Boolean);
interfaceItems.forEach((item) => {
if (!item.name) {
return;
}
settings[item.name] = {
domain_resolver_enabled: item.domain_resolver_enabled,
domain_resolver_dns_type: item.domain_resolver_dns_type,
domain_resolver_dns_server: item.domain_resolver_dns_server
};
});
next.interface_settings = compactSettingsMap(settings);
}
if (urltestItems.length) {
const settings = {};
next.urltests = urltestItems.map((item) => item[".name"]);
@@ -9494,7 +9460,6 @@ var PODKOP_MASK_AFTER_TOKEN_SPACE = [
"list fully_routed_ips",
"option dns_server",
"option bootstrap_dns_server",
"option domain_resolver_dns_server",
"option listen",
"option listen_port",
"option public_host",
@@ -9567,10 +9532,6 @@ function maskGlobalCheckLine(line) {
maskedLine = maskAfterTokenSpace(maskedLine, token);
}
maskedLine = maskOptionPath(maskedLine, "option dns_server '");
maskedLine = maskOptionPath(
maskedLine,
"option domain_resolver_dns_server '"
);
return maskedLine;
}
function maskMultilineContinuation(line) {
@@ -790,6 +790,26 @@ function ensureConnectionsDynamicListStyles() {
width: 100%;
}
.pdk-interface-dynlist-label {
align-items: center;
display: inline-flex;
gap: 0.25em;
max-width: 100%;
vertical-align: middle;
}
.pdk-interface-dynlist-label > img {
flex: 0 0 auto;
height: 1.35em;
width: auto;
}
.pdk-interface-dynlist-label > span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pdk-button-add-dynlist > .add-item {
display: block;
margin-top: 4px;
@@ -827,26 +847,6 @@ function ensureConnectionsDynamicListStyles() {
outline: 0;
}
.pdk-interface-dynlist-label {
align-items: center;
display: inline-flex;
gap: 0.25em;
max-width: 100%;
vertical-align: middle;
}
.pdk-interface-dynlist-label > img {
flex: 0 0 auto;
height: 1.35em;
width: auto;
}
.pdk-interface-dynlist-label > span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
body.modal-overlay-active > #modal_overlay > .modal.cbi-modal > .cbi-map.flash {
animation: none !important;
}
@@ -1377,24 +1377,6 @@ function currentOutboundNameChoices(section_id, values) {
return result.sort((a, b) => a.label.localeCompare(b.label));
}
function connectionUrlSupportsUdpOverTcp(value) {
const normalized = `${value || ""}`.trim().toLowerCase();
return (
normalized.startsWith("socks4://") ||
normalized.startsWith("socks4a://") ||
normalized.startsWith("socks5://") ||
normalized.startsWith("ss://") ||
normalized.startsWith("shadowsocks://")
);
}
function connectionTargetChoices(section_id) {
return getOutboundDetourTargetSections(section_id).map((section) => ({
value: getUciSectionName(section),
label: getUciSectionLabel(section),
}));
}
function isDownloadThroughTargetSection(section, currentSectionId) {
const sectionName = getUciSectionName(section);
const action = (section && section.action) || "";
@@ -1435,24 +1417,6 @@ function subscriptionDownloadTargetChoices(section_id) {
}));
}
function dnsTypeChoices() {
return [
{ value: "doh", label: _("DNS over HTTPS (DoH)") },
{ value: "dot", label: _("DNS over TLS (DoT)") },
{ value: "udp", label: "UDP" },
];
}
function dnsServerDatalist(inputId) {
return E(
"datalist",
{ id: inputId },
Object.entries(main.DNS_SERVER_OPTIONS).map(([key, label]) =>
E("option", { value: key }, _(label)),
),
);
}
function isConnectionNetworkInterfaceAllowed(deviceName, device) {
if (CONNECTIONS_BLOCKED_INTERFACES.includes(deviceName)) {
return false;
@@ -1523,7 +1487,29 @@ function refreshNetworkInterfaceOptionValues(option) {
});
}
const InterfaceSettingsDynamicList = SettingsDynamicList.extend({
const InterfaceUIDynamicList = ui.DynamicList.extend({
render() {
ensureConnectionsDynamicListStyles();
return ui.DynamicList.prototype.render.apply(this, arguments);
},
addItem(dl, value, text, flash) {
const itemText =
typeof this.options.itemLabel === "function"
? this.options.itemLabel(value, text)
: text;
return ui.DynamicList.prototype.addItem.call(
this,
dl,
value,
itemText,
flash,
);
},
});
const InterfaceDynamicList = form.DynamicList.extend({
load(section_id) {
return network.getDevices().then(
L.bind(function (devices) {
@@ -1535,9 +1521,32 @@ const InterfaceSettingsDynamicList = SettingsDynamicList.extend({
);
},
validate(section_id, value) {
value = childItemInputValue(section_id, value, "section_interface", "name");
renderWidget(section_id, _option_index, cfgvalue) {
const value = cfgvalue != null ? cfgvalue : this.default;
const choices = this.transformChoices();
const items = L.toArray(value);
const widget = new InterfaceUIDynamicList(items, 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,
itemLabel: (itemValue, itemText) =>
renderNetworkInterfaceListItem(
this.interfaceDeviceMap
? this.interfaceDeviceMap[itemValue]
: null,
itemValue || itemText,
),
});
return widget.render();
},
validate(_section_id, value) {
if (!value || value.length === 0) {
return true;
}
@@ -1548,15 +1557,6 @@ const InterfaceSettingsDynamicList = SettingsDynamicList.extend({
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() {
@@ -1624,22 +1624,6 @@ function optionMapValue(option, section_id, key) {
return value == null ? "" : value;
}
function connectionUrlSettingsKeys() {
return [
"outbound_detour_enabled",
"outbound_detour_section",
"enable_udp_over_tcp",
];
}
function defaultConnectionUrlSettings() {
return {
outbound_detour_enabled: "0",
outbound_detour_section: "",
enable_udp_over_tcp: "0",
};
}
function subscriptionUrlSettingsKeys() {
return [
"subscription_update_enabled",
@@ -1694,22 +1678,6 @@ function subscriptionUserAgentChoices() {
];
}
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 urlTestSettingsKeys() {
return [
"name",
@@ -1848,57 +1816,6 @@ function randomPriorityGroupId() {
return `pg_${Date.now().toString(16)}`;
}
function addConnectionUrlItemOptions(itemSection, options = {}) {
const parentSectionForItem =
typeof options.parentSectionId === "function"
? options.parentSectionId
: parentSectionIdForItem;
let o = itemSection.option(
form.Flag,
"outbound_detour_enabled",
_("Cascade connection"),
_("Use another section as an intermediate hop for connecting to this one"),
);
o.default = "0";
o.rmempty = false;
o = itemSection.option(
form.ListValue,
"outbound_detour_section",
_("Connect through"),
_("Select a transit section"),
);
o.depends("outbound_detour_enabled", "1");
o.load = function (itemId) {
const sectionId = parentSectionForItem(itemId);
refreshOptionChoices(this, connectionTargetChoices(sectionId));
return optionMapValue(this, itemId, "outbound_detour_section") || "";
};
o.validate = function (itemId, value) {
const sectionId = parentSectionForItem(itemId);
if (
optionMapValue(this, itemId, "outbound_detour_enabled") === "1" &&
!value
) {
return _("Select an intermediate section");
}
if (value === sectionId) {
return _("Current section cannot be used as its own transit section");
}
return true;
};
o = itemSection.option(
form.Flag,
"enable_udp_over_tcp",
_("UDP over TCP"),
_("Applicable only for SOCKS and Shadowsocks links"),
);
o.default = "0";
o.rmempty = false;
}
function addSubscriptionUrlItemOptions(itemSection, options = {}) {
const parentSectionForItem =
typeof options.parentSectionId === "function"
@@ -2057,43 +1974,6 @@ function addSubscriptionUrlItemOptions(itemSection, options = {}) {
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"
@@ -2913,17 +2793,6 @@ function ruleSetIncludesSubnets(section_id, value) {
);
}
function showConnectionUrlSettingsModal(_section_id, itemValue, option) {
return showChildItemSettingsModal(_section_id, itemValue, option, {
typeName: "connection_url",
valueOption: "url",
keys: connectionUrlSettingsKeys(),
defaults: defaultConnectionUrlSettings(),
addOptions: addConnectionUrlItemOptions,
title: () => _("Connection URL settings"),
});
}
function showSubscriptionUrlSettingsModal(_section_id, itemValue, option) {
return showChildItemSettingsModal(_section_id, itemValue, option, {
typeName: "subscription_url",
@@ -3187,17 +3056,6 @@ function validateOutboundJsonItemsBeforeSave(_section_id, values) {
return true;
}
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 showRuleSetSettingsModal(section_id, itemValue, option, widget) {
const data = {
settings: {
@@ -6473,47 +6331,17 @@ function createSectionContent(section) {
o = section.taboption(
"settings",
SettingsDynamicList,
"connection_url",
form.DynamicList,
"selector_proxy_links",
_("Connection URL"),
_(
"vless://, vmess://, ss://, trojan://, socks4/5://, http(s)://, hy2/hysteria2:// links",
),
);
o.depends("action", "connection");
o.rmempty = true;
o.modalonly = true;
o.childType = "connection_url";
o.childValueOption = "url";
o.childDefaults = defaultConnectionUrlSettings();
o.renderItemSettingsModal = showConnectionUrlSettingsModal;
o.hasItemSettings = function (section_id, value) {
const normalized = `${value || ""}`.trim();
if (isExistingChildItem(section_id, normalized, "connection_url")) {
return true;
}
return this.validate(section_id, normalized) === true;
};
o.stagedChildSettings = function (section_id, value) {
const inputValue = childItemInputValue(
section_id,
value,
"connection_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, "connection_url", "url");
};
o.validate = function (_section_id, value) {
value = childItemInputValue(_section_id, value, "connection_url", "url");
if (!value || value.length === 0) {
return true;
}
@@ -6572,8 +6400,8 @@ function createSectionContent(section) {
o = section.taboption(
"settings",
InterfaceSettingsDynamicList,
"interface",
InterfaceDynamicList,
"interfaces",
_("Network Interface"),
_("Select network interface for VPN connection"),
);
@@ -6581,33 +6409,6 @@ function createSectionContent(section) {
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 = section.taboption(
"settings",
@@ -6695,7 +6496,7 @@ function createSectionContent(section) {
"settings",
ButtonAddSettingsDynamicList,
"priority_group",
"Priority",
_("Priority"),
_("Server group for priority failover"),
);
o.depends("action", "connection");
@@ -6728,6 +6529,70 @@ function createSectionContent(section) {
);
};
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,
+8 -35
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PODKOP PLUS\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-09 02:41+0300\n"
"PO-Revision-Date: 2026-07-09 02:41+0300\n"
"POT-Creation-Date: 2026-07-10 04:26+0300\n"
"PO-Revision-Date: 2026-07-10 04:26+0300\n"
"Last-Translator: ushan0v\n"
"Language-Team: none\n"
"Language: ru\n"
@@ -113,9 +113,6 @@ msgstr "ALPN"
msgid "Alter ID"
msgstr "Alter ID"
msgid "Applicable only for SOCKS and Shadowsocks links"
msgstr "Применимо только для ссылок SOCKS и Shadowsocks"
msgid "Apply section rules only to the specified local IP addresses"
msgstr "Применять правила секции только к указанным локальным IP-адресам"
@@ -305,9 +302,6 @@ msgstr "Подключаться через"
msgid "Connection URL"
msgstr "URL подключения"
msgid "Connection URL settings"
msgstr "Настройки URL подключения"
msgid "Connections are unavailable"
msgstr "Подключения недоступны"
@@ -413,30 +407,18 @@ msgstr "DNS через HTTPS (DoH)"
msgid "DNS over TLS (DoT)"
msgstr "DNS через TLS (DoT)"
msgid "DNS protocol"
msgstr "Протокол DNS"
msgid "DNS Protocol Type"
msgstr "Тип протокола DNS"
msgid "DNS protocol used by the resolver"
msgstr "DNS-протокол, который будет использовать резолвер"
msgid "DNS Rewrite TTL"
msgstr "Перезапись TTL для DNS"
msgid "DNS server"
msgstr "DNS-сервер"
msgid "DNS Server"
msgstr "DNS-сервер"
msgid "DNS server address cannot be empty"
msgstr "Адрес DNS-сервера не может быть пустым"
msgid "DNS server used by the resolver"
msgstr "DNS-сервер, который будет использовать резолвер"
msgid "Do not panic, everything can be fixed, just..."
msgstr "Не паникуйте, всё можно исправить, просто..."
@@ -455,9 +437,6 @@ msgstr "Не резолвится"
msgid "Domain and IP Lists"
msgstr "Списки доменов и IPs"
msgid "Domain Resolver"
msgstr "Резолвер доменов"
msgid "Domains"
msgstr "Домены"
@@ -518,9 +497,6 @@ msgstr "Включить автоматическое обновление уд
msgid "Enable autostart"
msgstr "Включить автостарт"
msgid "Enable built-in DNS resolver for domains handled by this section"
msgstr "Включить встроенный DNS-резолвер для доменов, обрабатываемых этой секцией"
msgid "Enable list updates"
msgstr "Включить обновление списков"
@@ -1100,9 +1076,6 @@ msgstr "Имя, которое отображается в списке уров
msgid "Network Interface"
msgstr "Сетевой интерфейс"
msgid "Network interface settings"
msgstr "Настройки сетевого интерфейса"
msgid "NFQUEUE range is available"
msgstr "Диапазон NFQUEUE свободен"
@@ -1337,6 +1310,9 @@ msgstr "Порты"
msgid "Preferred IP"
msgstr "Предпочитаемый IP"
msgid "Priority"
msgstr "Приоритет"
msgid "Priority details"
msgstr "Детали Priority"
@@ -1581,7 +1557,7 @@ msgid "Server group for automatic lowest-latency selection"
msgstr "Группа серверов для автоматического выбора по наименьшей задержке"
msgid "Server group for priority failover"
msgstr "Группа серверов для приоритетного failover"
msgstr "Группа автоматически выбирает рабочий узел на основе установленного приоритета."
msgid "Server information"
msgstr "Информация о сервере"
@@ -1877,9 +1853,6 @@ msgstr "Тип"
msgid "UDP (Unprotected DNS)"
msgstr "UDP (Незащищённый DNS)"
msgid "UDP over TCP"
msgstr "UDP через TCP"
msgid "Unable to validate the ByeDPI strategy through the backend parser."
msgstr "Не удалось проверить стратегию ByeDPI через backend-парсер."
@@ -2000,8 +1973,8 @@ msgstr "Используйте IP-адрес"
msgid "Use an IP listen address"
msgstr "Используйте IP-адрес для прослушивания"
msgid "Use another section as an intermediate hop for connecting to this one"
msgstr "Использовать другую секцию как промежуточный узел для подключения к этой"
msgid "Use another section as an intermediate hop to connect to servers in this section. Does not apply to network interfaces or JSON outbounds."
msgstr "Использовать другую секцию как промежуточный узел подключения к серверам этой секции. Не применяется к сетевым интерфейсам и JSON outbound."
msgid "Use CIDR prefixes like 192.168.1.0/24 or 2001:db8::/32"
msgstr "Используйте CIDR-префиксы, например 192.168.1.0/24 или 2001:db8::/32"
File diff suppressed because it is too large Load Diff
+4 -6
View File
@@ -29,18 +29,16 @@ config settings 'settings'
# option label 'Subscription'
# option enabled '1'
# option action 'connection'
# list selector_proxy_links 'vless://example'
# list interfaces 'awg0'
# list community_lists 'russia_inside'
# list rule_set 'https://example.com/rules.srs'
# list rule_set_with_subnets 'https://example.com/rules-with-subnets.json'
# option outbound_detour_enabled '0'
# #option outbound_detour_section 'main'
# option sort_by_latency '0'
# #list fully_routed_ips '192.168.1.2'
#config connection_url
# option section 'subscription_example'
# option url 'vless://example'
# option outbound_detour_enabled '0'
# option enable_udp_over_tcp '0'
#config subscription_url
# option section 'subscription_example'
# option url 'https://example.com/api/sub'
+1 -73
View File
@@ -8,9 +8,7 @@ let object_or_empty = common.object_or_empty;
const CONFIG_NAME = getenv("PODKOP_CONFIG_NAME") || "podkop-plus";
const ITEM_TYPES = [
"connection_url",
"subscription_url",
"section_interface",
"urltest",
"priority_group",
"priority_level"
@@ -310,7 +308,7 @@ function action(section) {
}
function connection_urls(section) {
return child_values(section, "connection_url", "url", "selector_proxy_links");
return whitespace_list_value(section, "selector_proxy_links");
}
function subscription_urls(section) {
@@ -318,10 +316,6 @@ function subscription_urls(section) {
}
function interfaces(section) {
let items = child_values(section, "section_interface", "name", "");
if (length(items) > 0)
return items;
let result = list_value(section, "interfaces");
if (length(result) == 0) {
let value = option(section, "interface", "");
@@ -442,21 +436,11 @@ function has_connection_sources(section) {
length(outbound_jsons(section)) > 0;
}
function connection_url_settings(section, value) {
let child = child_item_by_value(section, "connection_url", "url", value);
return child != null ? child : item_settings(section, "connection_url_settings", value);
}
function subscription_url_settings(section, value) {
let child = child_item_by_value(section, "subscription_url", "url", value);
return child != null ? child : item_settings(section, "subscription_url_settings", value);
}
function interface_settings(section, value) {
let child = child_item_by_value(section, "section_interface", "name", value);
return child != null ? child : item_settings(section, "interface_settings", value);
}
function urltest_child(section, value) {
let child = owned_child_section(section, "urltest", value);
if (child != null)
@@ -577,54 +561,6 @@ function subscription_download_section(section, value) {
return item_option(section, "subscription_url_settings", value, "download_via_proxy_section", "");
}
function connection_detour_enabled(section, value) {
let child = child_item_by_value(section, "connection_url", "url", value);
if (child != null)
return child_bool(child, "outbound_detour_enabled", false);
return item_bool(section, "connection_url_settings", value, "outbound_detour_enabled",
bool_option(section, "outbound_detour_enabled", false));
}
function connection_detour_section(section, value) {
let child = child_item_by_value(section, "connection_url", "url", value);
if (child != null)
return child_option(child, "outbound_detour_section", "");
return item_option(section, "connection_url_settings", value, "outbound_detour_section",
option(section, "outbound_detour_section", ""));
}
function connection_udp_over_tcp(section, value) {
let child = child_item_by_value(section, "connection_url", "url", value);
if (child != null)
return child_bool(child, "enable_udp_over_tcp", false);
return item_bool(section, "connection_url_settings", value, "enable_udp_over_tcp",
bool_option(section, "enable_udp_over_tcp", false));
}
function interface_domain_resolver_enabled(section, value) {
let child = child_item_by_value(section, "section_interface", "name", value);
if (child != null)
return child_bool(child, "domain_resolver_enabled", false);
return item_bool(section, "interface_settings", value, "domain_resolver_enabled",
bool_option(section, "domain_resolver_enabled", false));
}
function interface_domain_resolver_dns_type(section, value) {
let child = child_item_by_value(section, "section_interface", "name", value);
if (child != null)
return child_option(child, "domain_resolver_dns_type", "udp");
return item_option(section, "interface_settings", value, "domain_resolver_dns_type",
option(section, "domain_resolver_dns_type", "udp") || "udp");
}
function interface_domain_resolver_dns_server(section, value) {
let child = child_item_by_value(section, "section_interface", "name", value);
if (child != null)
return child_option(child, "domain_resolver_dns_server", "8.8.8.8");
return item_option(section, "interface_settings", value, "domain_resolver_dns_server",
option(section, "domain_resolver_dns_server", "8.8.8.8") || "8.8.8.8");
}
function urltest_check_interval(section, value) {
let child = urltest_child(section, value);
if (child != null)
@@ -938,9 +874,7 @@ return {
rule_sets_value,
rule_sets_with_subnets_value,
has_connection_sources,
connection_url_settings,
subscription_url_settings,
interface_settings,
urltest_settings,
subscription_update_enabled,
subscription_update_interval,
@@ -953,12 +887,6 @@ return {
subscription_user_agent,
subscription_hwid,
subscription_download_section,
connection_detour_enabled,
connection_detour_section,
connection_udp_over_tcp,
interface_domain_resolver_enabled,
interface_domain_resolver_dns_type,
interface_domain_resolver_dns_server,
urltest_check_interval,
urltest_tolerance,
urltest_testing_url,
+35 -69
View File
@@ -30,9 +30,7 @@ const PODKOP_RUNTIME_CACHE_FORMAT = getenv("PODKOP_RUNTIME_CACHE_FORMAT") || "7"
const SERVER_COUNTRY_METHOD_FLAG_EMOJI = "flag_emoji";
const SERVER_COUNTRY_METHOD_COUNTRY_IS = "country_is";
const CHILD_ITEM_TYPES = [
"connection_url",
"subscription_url",
"section_interface",
"urltest"
];
@@ -714,34 +712,6 @@ function migrate_zapret_nfqws_default(ctx, section, constants) {
set_option(ctx, section, "nfqws_opt", constants.zapret_default_nfqws_opt);
}
function migrate_connection_url_item_settings(ctx, section) {
let values = whitespace_list_values(section, "selector_proxy_links");
let udp_over_tcp_enabled = bool_option(section, "enable_udp_over_tcp", false);
let detour_enabled = bool_option(section, "outbound_detour_enabled", false);
let detour_section = option(section, "outbound_detour_section", "");
let index = 1;
for (let value in values) {
value = as_string(value);
if (value == "")
continue;
let child = create_child_for_section(ctx, section, "connection_url");
set_option(ctx, child, "url", value);
if (detour_enabled) {
set_option(ctx, child, "outbound_detour_enabled", "1");
if (detour_section != "")
set_option(ctx, child, "outbound_detour_section", detour_section);
}
if (udp_over_tcp_enabled)
set_option(ctx, child, "enable_udp_over_tcp", "1");
index++;
}
delete_option(ctx, section, "selector_proxy_links");
delete_option(ctx, section, "connection_url_settings");
}
function migrate_subscription_url_item_settings(ctx, section) {
let values = whitespace_list_values(section, "subscription_urls");
let seen_values = {};
@@ -808,55 +778,51 @@ function migrate_urltest_item_settings(ctx, section, constants) {
delete_option(ctx, section, "urltest_settings");
}
function migrate_interface_item_settings(ctx, section) {
let values = option_list_values(section, "interfaces");
let legacy_interface = option(section, "interface", "");
if (legacy_interface != "")
push(values, legacy_interface);
let seen_values = {};
let resolver_enabled = bool_option(section, "domain_resolver_enabled", false);
let dns_type = option(section, "domain_resolver_dns_type", "");
let dns_server = option(section, "domain_resolver_dns_server", "");
let index = 1;
for (let value in values) {
value = as_string(value);
if (value == "" || seen_values[value])
continue;
seen_values[value] = true;
let child = create_child_for_section(ctx, section, "section_interface");
set_option(ctx, child, "name", value);
if (resolver_enabled) {
set_option(ctx, child, "domain_resolver_enabled", "1");
set_option(ctx, child, "domain_resolver_dns_type", dns_type != "" ? dns_type : "udp");
set_option(ctx, child, "domain_resolver_dns_server", dns_server != "" ? dns_server : "8.8.8.8");
}
index++;
}
delete_option(ctx, section, "interface");
delete_option(ctx, section, "interfaces");
delete_option(ctx, section, "interface_settings");
function migrate_interface_list(ctx, section) {
normalize_connections_list(ctx, section, "interface", "interfaces");
}
function migrate_outbound_json_list(ctx, section) {
function migrate_legacy_outbound_json_detour(ctx, section, legacy_connection_kind) {
if (legacy_connection_kind != "outbound" ||
!bool_option(section, "outbound_detour_enabled", false))
return;
let detour_section = option(section, "outbound_detour_section", "");
let outbound_json = option(section, "outbound_json", "");
if (detour_section == "" || outbound_json == "")
return;
let outbound;
try {
outbound = json(outbound_json);
}
catch (e) {
return;
}
if (type(outbound) != "object")
return;
if (as_string(outbound.detour || "") == "")
outbound.detour = singbox_constants_module.outbound_tag(detour_section);
set_option(ctx, section, "outbound_json", sprintf("%J", outbound));
delete_option(ctx, section, "outbound_detour_enabled");
delete_option(ctx, section, "outbound_detour_section");
}
function migrate_outbound_json_list(ctx, section, legacy_connection_kind) {
migrate_legacy_outbound_json_detour(ctx, section, legacy_connection_kind);
normalize_connections_list(ctx, section, "outbound_json", "outbound_jsons");
}
function migrate_connection_section(ctx, section, constants) {
migrate_connection_url_item_settings(ctx, section);
function migrate_connection_section(ctx, section, constants, legacy_connection_kind) {
migrate_subscription_url_item_settings(ctx, section);
migrate_urltest_item_settings(ctx, section, constants);
migrate_interface_item_settings(ctx, section);
migrate_outbound_json_list(ctx, section);
migrate_interface_list(ctx, section);
migrate_outbound_json_list(ctx, section, legacy_connection_kind);
delete_option(ctx, section, "subscription_update_enabled");
delete_option(ctx, section, "subscription_update_interval");
delete_option(ctx, section, "enable_udp_over_tcp");
delete_option(ctx, section, "outbound_detour_enabled");
delete_option(ctx, section, "outbound_detour_section");
delete_option(ctx, section, "domain_resolver_enabled");
delete_option(ctx, section, "domain_resolver_dns_type");
delete_option(ctx, section, "domain_resolver_dns_server");
@@ -1042,7 +1008,7 @@ function migrate_rule(ctx, section, converted_from_rule, constants) {
delete_option(ctx, section, "urltest_check_interval_disabled");
delete_option(ctx, section, "subscription_update_interval_disabled");
}
migrate_connection_section(ctx, section, constants);
migrate_connection_section(ctx, section, constants, legacy_connection_kind);
if (converted_from_rule && subscription_urls != "")
delete_subscription_cache(ctx, section_name(section));
}
+3 -71
View File
@@ -425,18 +425,6 @@ function valid_inbound() {
return type(value) == "object" && type(value.type) == "string";
}
function outbound_detour_supported() {
let value = read_stdin_json();
if (type(value) != "object" || type(value.type) != "string")
return false;
let outbound_type = lc(as_string(value.type));
return outbound_type != "selector" &&
outbound_type != "urltest" &&
outbound_type != "block" &&
outbound_type != "dns";
}
function bool_flag(value) {
value = as_string(value);
return value == "1" || value == "true";
@@ -452,24 +440,6 @@ function outbound_detour_target_action(action) {
return connections.is_connections_action(action);
}
function outbound_detour_supported_json(value) {
try {
value = json(as_string(value));
}
catch (e) {
return false;
}
if (type(value) != "object" || type(value.type) != "string")
return false;
let outbound_type = lc(as_string(value.type));
return outbound_type != "selector" &&
outbound_type != "urltest" &&
outbound_type != "block" &&
outbound_type != "dns";
}
function outbound_detour_rows() {
let rows = [];
@@ -484,8 +454,7 @@ function outbound_detour_rows() {
enabled: bool_flag(fields[1]),
action: as_string(fields[2]),
detour_enabled: bool_flag(fields[3]),
detour_section: as_string(fields[4]),
outbound_json: as_string(fields[5])
detour_section: as_string(fields[4])
};
push(rows, row);
@@ -576,9 +545,6 @@ function validate_outbound_detours_rows(rows) {
fail_outbound_detour("Outbound cascade for rule '" + row.section + "' creates a cycle through '" +
row.detour_section + "'. Aborted.");
if (row.action == "outbound" && !outbound_detour_supported_json(row.outbound_json))
fail_outbound_detour("JSON outbound rule '" + row.section +
"' cannot use outbound cascade because its sing-box outbound type does not support Dial Fields. Aborted.");
}
}
@@ -812,7 +778,7 @@ function fixture_get_section(section_name) {
if (section_name == "settings" && type(fixture.settings) == "object")
return fixture.settings;
for (let type_name in [ "settings", "server", "section", "connection_url", "subscription_url", "section_interface", "urltest", "priority_group", "priority_level" ]) {
for (let type_name in [ "settings", "server", "section", "subscription_url", "urltest", "priority_group", "priority_level" ]) {
for (let section in fixture_section_list(type_name)) {
if (as_string(section[".name"]) == section_name)
return section;
@@ -1372,8 +1338,7 @@ function detour_rows_from_sections(sections) {
enabled: section_enabled(section),
action: rule_action(section),
detour_enabled: bool_option(section, "outbound_detour_enabled", false),
detour_section: option(section, "outbound_detour_section", ""),
outbound_json: option(section, "outbound_json", "")
detour_section: option(section, "outbound_detour_section", "")
});
}
return rows;
@@ -1415,36 +1380,6 @@ function basic_rows_by_section(rows) {
return result;
}
function validate_connection_url_detours(sections) {
let by_section = basic_rows_by_section(basic_rows_from_sections(sections));
for (let section in sections) {
section = object_or_empty(section);
if (!section_enabled(section))
continue;
let name = section_name(section);
for (let entry in connections.connection_urls(section)) {
if (!connections.connection_detour_enabled(section, entry))
continue;
let target_name = connections.connection_detour_section(section, entry);
if (target_name == "")
fail_validation("Connection URL cascade is enabled in rule '" + name + "', but no intermediate rule is selected. Aborted.");
if (target_name == name)
fail_validation("Connection URL cascade in rule '" + name + "' cannot point to itself. Aborted.");
let target = by_section[target_name];
if (type(target) != "object")
fail_validation("Connection URL cascade in rule '" + name + "' references missing rule '" + target_name + "'. Aborted.");
if (!target.enabled)
fail_validation("Connection URL cascade in rule '" + name + "' references disabled rule '" + target_name + "'. Aborted.");
if (!outbound_detour_target_action(target.action))
fail_validation("Connection URL cascade in rule '" + name + "' references rule '" + target_name + "', but it is not a Connection rule. Aborted.");
}
}
}
function validate_server_routing_sections(sections) {
let by_section = basic_rows_by_section(basic_rows_from_sections(sections));
@@ -1541,7 +1476,6 @@ function validate_runtime_config(context) {
}
validate_outbound_detours_rows(detour_rows_from_sections(sections));
validate_connection_url_detours(sections);
validate_subscription_download_sections(sections, context);
validate_server_routing_sections(sections);
@@ -1953,8 +1887,6 @@ else if (mode == "valid-outbound")
exit(valid_outbound() ? 0 : 1);
else if (mode == "valid-inbound")
exit(valid_inbound() ? 0 : 1);
else if (mode == "outbound-detour-supported")
exit(outbound_detour_supported() ? 0 : 1);
else if (mode == "validate-outbound-detours")
validate_outbound_detours();
else if (mode == "validate-download-section")
@@ -361,7 +361,6 @@ function podkop_config_masked_line(line) {
line = mask_after_token(line, "list server_users");
line = mask_after_token_space(line, "option dns_server");
line = mask_after_token_space(line, "option bootstrap_dns_server");
line = mask_after_token_space(line, "option domain_resolver_dns_server");
line = mask_after_token_space(line, "option listen");
line = mask_after_token_space(line, "option listen_port");
line = mask_after_token_space(line, "option public_host");
@@ -388,7 +387,6 @@ function podkop_config_masked_line(line) {
line = mask_after_token_space(line, "option mixed_proxy_username");
line = mask_after_token_space(line, "option mixed_proxy_password");
line = mask_option_path(line, "option dns_server '");
line = mask_option_path(line, "option domain_resolver_dns_server '");
line = mask_after_token(line, "option yacd_secret_key");
return line;
+3 -20
View File
@@ -829,16 +829,7 @@ function section_subscription_update_interval(section) {
}
function connection_urls_signature(section) {
let result = [];
for (let entry in connections.connection_urls(section)) {
push(result, {
url: entry,
outbound_detour_enabled: connections.connection_detour_enabled(section, entry) ? "1" : "0",
outbound_detour_section: connections.connection_detour_section(section, entry),
enable_udp_over_tcp: connections.connection_udp_over_tcp(section, entry) ? "1" : "0"
});
}
return sprintf("%J", result);
return sprintf("%J", connections.connection_urls(section));
}
function subscription_urls_signature(section) {
@@ -863,16 +854,7 @@ function subscription_urls_signature(section) {
}
function interfaces_signature(section) {
let result = [];
for (let entry in connections.interfaces(section)) {
push(result, {
name: entry,
domain_resolver_enabled: connections.interface_domain_resolver_enabled(section, entry) ? "1" : "0",
domain_resolver_dns_type: connections.interface_domain_resolver_dns_type(section, entry),
domain_resolver_dns_server: connections.interface_domain_resolver_dns_server(section, entry)
});
}
return sprintf("%J", result);
return sprintf("%J", connections.interfaces(section));
}
function urltests_signature(section) {
@@ -1126,6 +1108,7 @@ function append_sing_box_rule_signature_body(body, section, sections) {
body = signature_add_value(body, prefix + ".urltest_include_outbounds", option(section, "urltest_include_outbounds", ""));
body = signature_add_value(body, prefix + ".urltest_include_regex", option(section, "urltest_include_regex", ""));
body = signature_add_value(body, prefix + ".subscription_update_interval", section_subscription_update_interval(section));
body = signature_add_outbound_detour_body(body, section, prefix);
body = signature_add_mixed_proxy_body(body, section, prefix);
body = signature_add_value(body, prefix + ".resolve_real_ip_for_routing", bool_option_value(section, "resolve_real_ip_for_routing", false));
}
@@ -68,10 +68,6 @@ function outbound_tag(section_name) {
return tag(section_name, "out");
}
function domain_resolver_tag(section_name) {
return tag(section_name, "domain-resolver");
}
function server_inbound_tag(section_name) {
return tag("server-" + as_string(section_name), "in");
}
@@ -117,7 +113,6 @@ return {
tag,
inbound_tag,
outbound_tag,
domain_resolver_tag,
server_inbound_tag,
tailscale_dns_server_tag
};
+33 -70
View File
@@ -103,7 +103,7 @@ function fixture_get_section(section_name) {
if (section_name == "settings" && type(fixture.settings) == "object")
return fixture.settings;
for (let type_name in [ "settings", "server", "section", "connection_url", "subscription_url", "section_interface", "urltest", "priority_group", "priority_level" ]) {
for (let type_name in [ "settings", "server", "section", "subscription_url", "urltest", "priority_group", "priority_level" ]) {
for (let section in fixture_section_list(type_name)) {
if (as_string(section[".name"]) == section_name)
return section;
@@ -1125,22 +1125,29 @@ function outbound_detour_tag_for_section(section) {
return detour_section == "" ? "" : outbound_tag(detour_section);
}
function apply_detour_to_outbound(config, tag_name, detour_tag) {
function apply_section_detour_to_connection_outbounds(config, start_index, detour_tag) {
if (detour_tag == "")
return;
for (let outbound in array_or_empty(config.outbounds)) {
if (type(outbound) == "object" && outbound.tag == tag_name)
let outbounds = array_or_empty(config.outbounds);
for (let i = int(start_index || 0); i < length(outbounds); i++) {
let outbound = outbounds[i];
if (type(outbound) != "object")
continue;
let outbound_type = lc(as_string(outbound.type || ""));
if (outbound_type == "" ||
outbound_type == "selector" ||
outbound_type == "urltest" || outbound_type == "dns" ||
outbound_type == "block")
continue;
// Preserve subscription chains: only their terminal dial outbound receives the section detour.
if (as_string(outbound.detour || "") == "")
outbound.detour = detour_tag;
}
}
function apply_detour_to_outbounds(config, tag_names, detour_tag) {
if (detour_tag == "")
return;
for (let tag_name in tag_names)
apply_detour_to_outbound(config, tag_name, detour_tag);
}
function mixed_proxy_enabled_action(action) {
return action == "connection" || action == "proxy" || action == "outbound" || action == "vpn" ||
action == "byedpi" || action == "zapret" || action == "zapret2";
@@ -1451,7 +1458,7 @@ function manual_http_outbound(link, tag_name) {
return outbound;
}
function manual_socks_outbound(link, tag_name, udp_over_tcp) {
function manual_socks_outbound(link, tag_name) {
let scheme = url_scheme(link);
let host = url_host(link);
let port = parse_port(url_port(link));
@@ -1474,12 +1481,10 @@ function manual_socks_outbound(link, tag_name, udp_over_tcp) {
outbound.password = substr(userinfo, colon + 1);
}
}
if (udp_over_tcp)
outbound.udp_over_tcp = { enabled: true, version: 2 };
return outbound;
}
function manual_shadowsocks_outbound(link, tag_name, udp_over_tcp) {
function manual_shadowsocks_outbound(link, tag_name) {
let raw = url_strip_fragment_value(url_decode(link));
let body = substr(raw, 5);
let question = index(body, "?");
@@ -1534,8 +1539,6 @@ function manual_shadowsocks_outbound(link, tag_name, udp_over_tcp) {
outbound.plugin = as_string(query.plugin);
if (as_string(query["plugin-opts"] || "") != "")
outbound.plugin_opts = as_string(query["plugin-opts"]);
if (udp_over_tcp)
outbound.udp_over_tcp = { enabled: true, version: 2 };
return outbound;
}
@@ -1696,7 +1699,7 @@ function manual_hysteria2_outbound(link, tag_name) {
return outbound;
}
function manual_link_outbound(link, tag_name, udp_over_tcp) {
function manual_link_outbound(link, tag_name) {
let scheme = url_scheme(link);
if (scheme == "vmess")
return manual_vmess_outbound(link, tag_name);
@@ -1706,9 +1709,9 @@ function manual_link_outbound(link, tag_name, udp_over_tcp) {
if (scheme == "http" || scheme == "https")
return manual_http_outbound(link, tag_name);
if (scheme == "socks4" || scheme == "socks4a" || scheme == "socks5")
return manual_socks_outbound(link, tag_name, udp_over_tcp);
return manual_socks_outbound(link, tag_name);
if (scheme == "ss")
return manual_shadowsocks_outbound(link, tag_name, udp_over_tcp);
return manual_shadowsocks_outbound(link, tag_name);
if (scheme == "vless")
return manual_vless_outbound(link, tag_name);
if (scheme == "trojan")
@@ -1718,13 +1721,13 @@ function manual_link_outbound(link, tag_name, udp_over_tcp) {
runtime_generate_unsupported("manual proxy link scheme is not supported by sing-box config generation yet");
}
function add_manual_proxy_link(config, state, section_name, manual_index, link, udp_over_tcp, taken, selector_tags, urltest_candidate_tags) {
function add_manual_proxy_link(config, state, section_name, manual_index, link, taken, selector_tags, urltest_candidate_tags) {
let tag_name = outbound_tag(section_name + "-" + manual_index);
if (taken[tag_name])
tag_name = unique_tag(tag_name, taken);
taken[tag_name] = true;
let outbound = manual_link_outbound(link, tag_name, udp_over_tcp);
let outbound = manual_link_outbound(link, tag_name);
push(config.outbounds, outbound);
push(selector_tags, tag_name);
push(urltest_candidate_tags, tag_name);
@@ -1743,31 +1746,21 @@ function connection_item_tag(section_name, kind, item_index) {
return outbound_tag(section_name + "-" + as_string(kind) + "-" + item_index);
}
function connection_detour_tag(section, link) {
if (!connections.connection_detour_enabled(section, link))
return "";
let detour_section = connections.connection_detour_section(section, link);
return detour_section == "" ? "" : outbound_tag(detour_section);
}
function add_connection_manual_links(config, state, section, taken, selector_tags, urltest_candidate_tags) {
let section_name = section[".name"];
let manual_links = connections.connection_urls(section);
for (let i = 0; i < length(manual_links); i++) {
let link = manual_links[i];
let tag_name = add_manual_proxy_link(
add_manual_proxy_link(
config,
state,
section_name,
i + 1,
link,
connections.connection_udp_over_tcp(section, link),
taken,
selector_tags,
urltest_candidate_tags
);
apply_detour_to_outbound(config, tag_name, connection_detour_tag(section, link));
}
}
@@ -1798,29 +1791,12 @@ function add_interface_connection_outbound(config, state, section, interface_ind
tag_name = unique_tag(tag_name, taken);
taken[tag_name] = true;
let domain_resolver = "";
if (connections.interface_domain_resolver_enabled(section, interface_name)) {
domain_resolver = runtime_constants.domain_resolver_tag(section_name + "-interface-" + interface_index);
let dns_server = runtime_dns.server_from_options(
domain_resolver,
connections.interface_domain_resolver_dns_type(section, interface_name),
connections.interface_domain_resolver_dns_server(section, interface_name),
tag_name
);
if (dns_server.unsupported)
runtime_generate_unsupported(dns_server.unsupported);
push(config.dns.servers, dns_server);
}
let outbound = {
type: "direct",
tag: tag_name,
bind_interface: interface_name,
domain_resolver,
routing_mark: runtime_constants.OUTBOUND_MARK
};
if (domain_resolver == "")
delete outbound.domain_resolver;
push(config.outbounds, outbound);
push(selector_tags, tag_name);
@@ -1876,9 +1852,16 @@ function add_connections_outbound(config, section, taken) {
let selector_tags = [];
let urltest_candidate_tags = [];
let state = runtime_subscription.new_section_state(section_name);
let cascade_start = length(array_or_empty(config.outbounds));
add_connection_manual_links(config, state, section, taken, selector_tags, urltest_candidate_tags);
add_connection_subscriptions(config, state, section, taken, selector_tags, urltest_candidate_tags);
// Apply before interface and JSON items are added: those source kinds are intentionally excluded.
apply_section_detour_to_connection_outbounds(
config,
cascade_start,
outbound_detour_tag_for_section(section)
);
add_connection_interfaces(config, state, section, taken, selector_tags, urltest_candidate_tags);
add_connection_json_outbounds(config, state, section, taken, selector_tags, urltest_candidate_tags);
@@ -1923,9 +1906,6 @@ function add_json_outbound(config, section) {
runtime_generate_unsupported("JSON outbound is not an object");
outbound.tag = outbound_tag(section[".name"]);
let detour_tag = outbound_detour_tag_for_section(section);
if (detour_tag != "")
outbound.detour = detour_tag;
push(config.outbounds, outbound);
}
@@ -1934,29 +1914,12 @@ function add_vpn_outbound(config, section) {
if (interface_name == "")
runtime_generate_unsupported("VPN interface is not set");
let domain_resolver = "";
if (bool_option(section, "domain_resolver_enabled", false)) {
domain_resolver = runtime_constants.domain_resolver_tag(section[".name"]);
let dns_server = runtime_dns.server_from_options(
domain_resolver,
option(section, "domain_resolver_dns_type", ""),
option(section, "domain_resolver_dns_server", ""),
outbound_tag(section[".name"])
);
if (dns_server.unsupported)
runtime_generate_unsupported(dns_server.unsupported);
push(config.dns.servers, dns_server);
}
push(config.outbounds, {
type: "direct",
tag: outbound_tag(section[".name"]),
bind_interface: interface_name,
domain_resolver,
routing_mark: runtime_constants.OUTBOUND_MARK
});
if (domain_resolver == "")
delete config.outbounds[length(config.outbounds) - 1].domain_resolver;
}
function enabled_action_index(sections, target_section, action_name) {
+31 -26
View File
@@ -43,6 +43,9 @@ const fixture = {
connection_type: 'proxy',
proxy_config_type: 'url',
proxy_string: 'vless://one\n//commented\n ss://two ',
enable_udp_over_tcp: '1',
outbound_detour_enabled: '1',
outbound_detour_section: 'legacy-sub',
urltest_check_interval_disabled: '1',
domain: [ 'Example.COM', 'full:Already.EXAMPLE' ],
domain_keyword_text_mode: '1',
@@ -78,7 +81,7 @@ const fixture = {
{
'.name': 'legacy-list-sub',
'.type': 'section',
action: 'connection',
action: 'proxy',
subscription_urls: [
'https://example.com/list.txt | ListAgent/2.0',
'https://example.com/auto.txt'
@@ -113,6 +116,14 @@ const fixture = {
domain_resolver_enabled: '1',
domain_resolver_dns_type: 'doh',
domain_resolver_dns_server: 'https://dns.example/dns-query'
},
{
'.name': 'legacy-outbound',
'.type': 'section',
action: 'outbound',
outbound_json: '{"type":"socks","server":"127.0.0.1","server_port":1080,"version":"5"}',
outbound_detour_enabled: '1',
outbound_detour_section: 'legacy-url'
}
]
};
@@ -128,9 +139,7 @@ const out = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
const config = out.config;
const sections = Object.fromEntries(config.section.map(section => [section['.name'], section]));
const childByType = type => config[type] || [];
const connectionUrls = childByType('connection_url');
const subscriptionUrls = childByType('subscription_url');
const interfaces = childByType('section_interface');
const urltests = childByType('urltest');
function assert(condition, message) {
@@ -175,7 +184,10 @@ absent(config.settings, 'routing_excluded_ips', 'settings');
const legacyUrl = sections['legacy-url'];
assert(legacyUrl['.type'] === 'section', 'legacy rule should become section');
assert(legacyUrl.action === 'connection', 'legacy-url action');
assert(JSON.stringify(childValues(legacyUrl, connectionUrls, 'url')) === JSON.stringify(['vless://one', 'ss://two']), 'proxy_string links');
assert(JSON.stringify(legacyUrl.selector_proxy_links) === JSON.stringify(['vless://one', 'ss://two']), 'proxy_string links');
assert(legacyUrl.outbound_detour_enabled === '1', 'section cascade enabled preserved');
assert(legacyUrl.outbound_detour_section === 'legacy-sub', 'section cascade target preserved');
absent(legacyUrl, 'enable_udp_over_tcp', 'legacy-url');
const legacyUrlDomains = (legacyUrl.domain || '').split(/\s+/).filter(Boolean);
assert(legacyUrlDomains.includes('full:Example.COM'), 'full domain list value migrated');
assert(legacyUrlDomains.includes('full:Already.EXAMPLE'), 'prefixed full domain list value migrated without duplicate prefix');
@@ -187,13 +199,11 @@ assert(legacyUrlDomains.includes('regex:^cdn[.]example$'), 'second regex migrate
assert(JSON.stringify(legacyUrl.rule_set) === JSON.stringify(['https://example.com/domains.srs']), 'domain rule set preserved');
assert(JSON.stringify(legacyUrl.rule_set_with_subnets) === JSON.stringify(['https://example.com/mixed.srs']), 'rule set subnet list preserved');
absent(legacyUrl, 'proxy_string', 'legacy-url');
absent(legacyUrl, 'selector_proxy_links', 'legacy-url');
absent(legacyUrl, 'proxy_config_type', 'legacy-url');
absent(legacyUrl, 'connection_type', 'legacy-url');
absent(legacyUrl, 'urltest_enabled', 'legacy-url');
absent(legacyUrl, 'rule_set_settings', 'legacy-url');
absent(legacyUrl, 'domain_suffix', 'legacy-url');
absent(legacyUrl, 'connection_url_items', 'legacy-url');
absent(legacyUrl, 'rule_set_items', 'legacy-url');
absent(legacyUrl, 'domain_keyword_text', 'legacy-url');
absent(legacyUrl, 'domain_regex_text', 'legacy-url');
@@ -251,7 +261,7 @@ absent(legacyListSub, 'subscription_url_items', 'legacy-list-sub');
const legacyUrltest = sections['legacy-urltest'];
assert(legacyUrltest.action === 'connection', 'legacy-urltest action');
assert(JSON.stringify(childValues(legacyUrltest, connectionUrls, 'url')) === JSON.stringify(['vmess://a', 'trojan://b']), 'urltest links deduped');
assert(JSON.stringify(legacyUrltest.selector_proxy_links) === JSON.stringify(['vmess://a', 'trojan://b']), 'urltest links deduped');
absent(legacyUrltest, 'urltests', 'legacy-urltest');
const legacyUrltestConfig = urltestByOwner(legacyUrltest);
assert(Boolean(legacyUrltestConfig), 'legacy-urltest URLTest child migrated');
@@ -262,8 +272,6 @@ assert(legacyUrltestConfig.filter_mode === 'exclude', 'legacy-urltest URLTest fi
assert(legacyUrltestConfig.detect_server_country === 'flag_emoji', 'legacy-urltest detect server country normalized');
assert(JSON.stringify(legacyUrltestConfig.exclude_regex) === JSON.stringify(['bad.*']), 'legacy-urltest exclude regex migrated');
absent(legacyUrltest, 'urltest_proxy_links', 'legacy-urltest');
absent(legacyUrltest, 'selector_proxy_links', 'legacy-urltest');
absent(legacyUrltest, 'connection_url_items', 'legacy-urltest');
absent(legacyUrltest, 'urltest_enabled', 'legacy-urltest');
absent(legacyUrltest, 'urltest_filter_mode', 'legacy-urltest');
absent(legacyUrltest, 'detect_server_country', 'legacy-urltest');
@@ -283,17 +291,20 @@ absent(legacyZap, 'cmd_opts', 'legacy-zap');
const legacyVpn = sections['legacy-vpn'];
assert(legacyVpn.action === 'connection', 'vpn action inferred');
assert(JSON.stringify(childValues(legacyVpn, interfaces, 'name')) === JSON.stringify(['awg0']), 'vpn interface migrated to interface item');
const legacyVpnInterface = childObjects(legacyVpn, interfaces)[0];
assert(legacyVpnInterface.domain_resolver_enabled === '1', 'vpn domain resolver enabled migrated');
assert(legacyVpnInterface.domain_resolver_dns_type === 'doh', 'vpn domain resolver type migrated');
assert(legacyVpnInterface.domain_resolver_dns_server === 'https://dns.example/dns-query', 'vpn domain resolver server migrated');
assert(JSON.stringify(legacyVpn.interfaces) === JSON.stringify(['awg0']), 'vpn interface migrated to section list');
absent(legacyVpn, 'proxy_config_type', 'legacy-vpn');
absent(legacyVpn, 'interface', 'legacy-vpn');
absent(legacyVpn, 'interfaces', 'legacy-vpn');
absent(legacyVpn, 'interface_items', 'legacy-vpn');
absent(legacyVpn, 'interface_settings', 'legacy-vpn');
absent(legacyVpn, 'domain_resolver_enabled', 'legacy-vpn');
absent(legacyVpn, 'domain_resolver_dns_type', 'legacy-vpn');
absent(legacyVpn, 'domain_resolver_dns_server', 'legacy-vpn');
const legacyOutbound = sections['legacy-outbound'];
assert(legacyOutbound.action === 'connection', 'outbound action inferred');
assert(Array.isArray(legacyOutbound.outbound_jsons) && legacyOutbound.outbound_jsons.length === 1, 'legacy JSON outbound migrated to list');
assert(JSON.parse(legacyOutbound.outbound_jsons[0]).detour === 'legacy-url-out', 'legacy JSON cascade embedded in outbound');
absent(legacyOutbound, 'outbound_detour_enabled', 'legacy-outbound');
absent(legacyOutbound, 'outbound_detour_section', 'legacy-outbound');
assert(out.removed_caches.includes('/tmp/sing-box/subscriptions/legacy-sub.json'), 'subscription runtime cache removal');
assert(out.removed_caches.includes('/var/run/podkop-plus/section-cache/legacy-sub.json'), 'section cache removal');
@@ -329,16 +340,10 @@ grep -Fxq 'podkop-plus.legacy.action=connection' "$WORK_DIR/runtime-migrate.stat
grep -Fxq 'podkop-plus.old_direct.action=bypass' "$WORK_DIR/runtime-migrate.state" ||
fail "runtime migration must convert direct action to bypass through core.uci"
grep -Eq '^podkop-plus\\.cfg[0-9a-f]+=$' "$WORK_DIR/runtime-migrate.state" && fail "anonymous fixture section should include a type"
child_section="$(awk -F= '$2 == "connection_url" { sub(/^podkop-plus[.]/, "", $1); print $1; exit }' "$WORK_DIR/runtime-migrate.state")"
if [ -z "$child_section" ]; then
fail "runtime migration must create connection_url child section through core.uci"
fi
grep -Fxq "podkop-plus.${child_section}.section=legacy" "$WORK_DIR/runtime-migrate.state" ||
fail "runtime migration must link anonymous connection_url child through section option"
grep -Fxq "podkop-plus.${child_section}.url=vless://one" "$WORK_DIR/runtime-migrate.state" ||
fail "runtime migration must write connection_url child value through core.uci"
if grep -Fq 'podkop-plus.legacy.connection_url_items=' "$WORK_DIR/runtime-migrate.state"; then
fail "runtime migration must not write parent child-reference lists"
grep -Fxq 'podkop-plus.legacy.selector_proxy_links=vless://one' "$WORK_DIR/runtime-migrate.state" ||
fail "runtime migration must keep connection URLs in the parent section"
if grep -Fq '=connection_url' "$WORK_DIR/runtime-migrate.state"; then
fail "runtime migration must not create connection_url child sections"
fi
if grep -Fq 'podkop-plus.legacy.urltest_enabled=' "$WORK_DIR/runtime-migrate.state"; then
fail "runtime migration must delete migrated urltest_enabled through core.uci"
@@ -11,7 +11,7 @@ fail() {
}
rows() {
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$@"
printf '%s\t%s\t%s\t%s\t%s\n' "$@"
}
assert_rejects() {
@@ -29,39 +29,30 @@ assert_rejects() {
}
rows \
source 1 proxy 1 target '' \
target 1 vpn 0 '' '' |
ucode -L "$PODKOP_LIB" "$VALIDATOR" validate-outbound-detours
rows \
source 1 outbound 1 target '{"type":"direct"}' \
target 1 proxy 0 '' '' |
source 1 connection 1 target \
target 1 connection 0 '' |
ucode -L "$PODKOP_LIB" "$VALIDATOR" validate-outbound-detours
assert_rejects "unsupported source action" "supported only for Connection rules" \
source 1 zapret 1 target '' \
target 1 proxy 0 '' ''
source 1 zapret 1 target \
target 1 connection 0 ''
assert_rejects "missing target" "references missing rule 'missing'" \
source 1 proxy 1 missing ''
source 1 connection 1 missing
assert_rejects "disabled target" "references disabled rule 'target'" \
source 1 proxy 1 target '' \
target 0 proxy 0 '' ''
source 1 connection 1 target \
target 0 connection 0 ''
assert_rejects "wrong target action" "but it is not a Connection rule" \
source 1 proxy 1 target '' \
target 1 zapret 0 '' ''
source 1 connection 1 target \
target 1 zapret 0 ''
assert_rejects "self target" "cannot point to itself" \
source 1 proxy 1 source ''
source 1 connection 1 source
assert_rejects "cycle" "creates a cycle through 'second'" \
first 1 proxy 1 second '' \
second 1 proxy 1 first ''
assert_rejects "unsupported json outbound" "does not support Dial Fields" \
source 1 outbound 1 target '{"type":"selector"}' \
target 1 proxy 0 '' ''
first 1 connection 1 second \
second 1 connection 1 first
printf 'config validator detour regression checks passed\n'
@@ -0,0 +1,134 @@
#!/usr/bin/env bash
set -eo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
PODKOP_LIB="$ROOT_DIR/podkop/files/usr/lib"
GENERATOR="$PODKOP_LIB/singbox/generator.uc"
WORK_DIR="$(mktemp -d)"
cleanup() {
rm -rf "$WORK_DIR"
}
trap cleanup EXIT
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
mkdir -p "$WORK_DIR/subscriptions" "$WORK_DIR/output.json.section-cache"
cat >"$WORK_DIR/subscriptions/source-subscription-1.json" <<'JSON'
{
"outbounds": [
{
"type": "vless",
"tag": "nested-entry",
"server": "192.0.2.10",
"server_port": 443,
"uuid": "00000000-0000-4000-8000-000000000001",
"detour": "nested-relay"
},
{
"type": "socks",
"tag": "nested-relay",
"server": "192.0.2.11",
"server_port": 1080,
"version": "5"
},
{
"type": "trojan",
"tag": "independent",
"server": "192.0.2.12",
"server_port": 443,
"password": "secret"
},
{
"type": "urltest",
"tag": "provider-group",
"outbounds": [ "nested-entry", "independent" ],
"url": "https://www.gstatic.com/generate_204",
"interval": "3m",
"__podkop_allow_group": true
}
]
}
JSON
printf '%s\n' 'https://subscription.example/source' >"$WORK_DIR/subscriptions/source-subscription-1.url"
: >"$WORK_DIR/subscriptions/source-subscription-1.user_agent"
cat >"$WORK_DIR/fixture.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1"
},
"section": [
{
".name": "hop",
".type": "section",
"enabled": "1",
"action": "connection",
"selector_proxy_links": [ "socks5://127.0.0.1:1080#Hop" ]
},
{
".name": "source",
".type": "section",
"enabled": "1",
"action": "connection",
"selector_proxy_links": [ "socks5://127.0.0.1:1081#Manual" ],
"interfaces": [ "wg0" ],
"outbound_jsons": [
"{\"type\":\"socks\",\"tag\":\"JSON plain\",\"server\":\"127.0.0.1\",\"server_port\":1082,\"version\":\"5\"}",
"{\"type\":\"socks\",\"tag\":\"JSON explicit\",\"server\":\"127.0.0.1\",\"server_port\":1083,\"version\":\"5\",\"detour\":\"direct-out\"}"
],
"outbound_detour_enabled": "1",
"outbound_detour_section": "hop"
}
],
"subscription_url": [
{
".name": "source_subscription",
".type": "subscription_url",
"section": "source",
"url": "https://subscription.example/source"
}
]
}
JSON
TMP_SUBSCRIPTION_FOLDER="$WORK_DIR/subscriptions" \
ucode -L "$PODKOP_LIB" "$GENERATOR" generate-config-fixture \
"$WORK_DIR/fixture.json" "$WORK_DIR/output.json" "127.0.0.1"
ucode -e '
let fs = require("fs");
let config = json(fs.readfile(ARGV[0]));
function outbound(tag) {
for (let item in config.outbounds || [])
if (item && item.tag == tag)
return item;
return null;
}
function assert(condition, message) {
if (!condition)
die(message + "\n");
}
assert(outbound("source-1-out").detour == "hop-out", "manual URL did not receive the section detour");
assert(outbound("nested-entry").detour == "nested-relay", "subscription detour was overwritten");
assert(outbound("nested-relay").detour == "hop-out", "section detour was not appended to the subscription chain");
assert(outbound("independent").detour == "hop-out", "independent subscription outbound did not receive the section detour");
assert(outbound("provider-group").detour == null, "subscription URLTest group must not receive Dial Fields");
assert(outbound("source-interface-1-out").bind_interface == "wg0", "interface binding was not preserved");
assert(outbound("source-interface-1-out").detour == null, "network interface must be excluded from section cascade");
assert(outbound("source-json-1-out").detour == null, "plain JSON outbound must be excluded from section cascade");
assert(outbound("source-json-2-out").detour == "direct-out", "explicit JSON detour was not preserved");
assert(outbound("source-out").detour == null, "section selector must not receive its own detour");
' "$WORK_DIR/output.json" || fail "section cascade generation"
printf 'connection cascade regression checks passed\n'
@@ -314,7 +314,6 @@ cat >"$WORK_DIR/sing-box-signature.json" <<'JSON'
"action": "proxy",
"selector_proxy_links": [ "vless://one", "vless://two" ],
"subscription_urls": [ "https://example.com/sub.txt" ],
"enable_udp_over_tcp": "yes",
"urltest_enabled": "1",
"detect_server_country": "1",
"urltest_tolerance": "75",
@@ -367,9 +366,6 @@ cat >"$WORK_DIR/sing-box-signature.json" <<'JSON'
"enabled": "1",
"action": "vpn",
"interface": "wg0",
"domain_resolver_enabled": "1",
"domain_resolver_dns_type": "udp",
"domain_resolver_dns_server": "8.8.8.8",
"mixed_proxy_enabled": "0",
"resolve_real_ip_for_routing": "yes"
}
@@ -434,7 +430,7 @@ secret
[rule.proxy1.action]
proxy
[rule.proxy1.connection_urls]
[ { "url": "vless://one", "outbound_detour_enabled": "1", "outbound_detour_section": "out1", "enable_udp_over_tcp": "1" }, { "url": "vless://two", "outbound_detour_enabled": "1", "outbound_detour_section": "out1", "enable_udp_over_tcp": "1" } ]
[ "vless://one", "vless://two" ]
[rule.proxy1.subscription_urls]
[ { "url": "https://example.com/sub.txt", "subscription_update_enabled": "1", "subscription_update_interval": "1h", "download_via_proxy_section": "", "auto_user_agent": "1", "user_agent": "", "auto_hwid": "1", "hwid": "", "show_dashboard_metadata": "1", "include_urltest_groups": "1", "hide_urltest_group_outbounds": "1", "hide_detour_outbounds": "1" } ]
[rule.proxy1.interfaces]
@@ -475,6 +471,10 @@ node-a
[rule.proxy1.subscription_update_interval]
1h
[rule.proxy1.outbound_detour_enabled]
1
[rule.proxy1.outbound_detour_section]
out1
[rule.proxy1.mixed_proxy_enabled]
1
[rule.proxy1.mixed_proxy_port]
@@ -555,6 +555,8 @@ disabled
[rule.out1.subscription_update_interval]
[rule.out1.outbound_detour_enabled]
0
[rule.out1.mixed_proxy_enabled]
0
[rule.out1.resolve_real_ip_for_routing]
@@ -622,7 +624,7 @@ vpn
[rule.vpn1.subscription_urls]
[ ]
[rule.vpn1.interfaces]
[ { "name": "wg0", "domain_resolver_enabled": "1", "domain_resolver_dns_type": "udp", "domain_resolver_dns_server": "8.8.8.8" } ]
[ "wg0" ]
[rule.vpn1.outbound_jsons]
[rule.vpn1.legacy_interface]
@@ -659,6 +661,8 @@ disabled
[rule.vpn1.subscription_update_interval]
[rule.vpn1.outbound_detour_enabled]
0
[rule.vpn1.mixed_proxy_enabled]
0
[rule.vpn1.resolve_real_ip_for_routing]
+5 -6
View File
@@ -270,7 +270,8 @@ cat >"$WORK_DIR/runtime-matchers-fixture.json" <<'JSON'
"selector_proxy_links": [ "socks5://127.0.0.1:1080" ],
"domain_suffix": [ "proxy.example.org", "сайт.рф", "full:пример.испытание", "keyword:пример", "regex:^сайт[.]рф$" ],
"source_ip_cidr": [ "10.0.0.2/32", "2001:db8::2/128" ],
"connection_url_settings": "{\"socks5://127.0.0.1:1080\":{\"outbound_detour_enabled\":\"1\",\"outbound_detour_section\":\"detour\"}}",
"outbound_detour_enabled": "1",
"outbound_detour_section": "detour",
"mixed_proxy_enabled": "1",
"mixed_proxy_port": "19090",
"mixed_proxy_auth_enabled": "1",
@@ -352,7 +353,7 @@ cat >"$WORK_DIR/manual-transport-fixture.json" <<'JSON'
}
JSON
cat >"$WORK_DIR/vpn-domain-resolver-fixture.json" <<'JSON'
cat >"$WORK_DIR/vpn-interface-fixture.json" <<'JSON'
{
"settings": {
".name": "settings",
@@ -368,7 +369,6 @@ cat >"$WORK_DIR/vpn-domain-resolver-fixture.json" <<'JSON'
"enabled": "1",
"action": "connection",
"interfaces": [ "tun0" ],
"interface_settings": "{\"tun0\":{\"domain_resolver_enabled\":\"1\",\"domain_resolver_dns_type\":\"doh\",\"domain_resolver_dns_server\":\"https://dns.example/dns-query\"}}",
"domain_suffix": [ "vpn.example" ]
}
]
@@ -681,7 +681,7 @@ generate_config "$WORK_DIR/runtime-matchers-fixture.json" "$WORK_DIR/matchers.js
generate_config "$WORK_DIR/urltest-filter-fixture.json" "$WORK_DIR/urltest.json"
generate_config "$WORK_DIR/provider-actions-fixture.json" "$WORK_DIR/providers.json"
generate_config "$WORK_DIR/manual-transport-fixture.json" "$WORK_DIR/manual.json"
generate_config "$WORK_DIR/vpn-domain-resolver-fixture.json" "$WORK_DIR/vpn.json"
generate_config "$WORK_DIR/vpn-interface-fixture.json" "$WORK_DIR/vpn.json"
generate_config "$WORK_DIR/download-via-proxy-fixture.json" "$WORK_DIR/download.json"
generate_config "$WORK_DIR/fully-routed-fixture.json" "$WORK_DIR/fully-routed.json"
generate_config "$WORK_DIR/mwan3-auto-fixture.json" "$WORK_DIR/mwan3-auto.json" 1
@@ -844,8 +844,7 @@ assert(outbound(manual, "proxy-3-out").type == "shadowsocks", "manual Shadowsock
let vpn = cfg("vpn");
assert(outbound(vpn, "renamed_awg-interface-1-out").bind_interface == "tun0", "renamed VPN interface outbound");
assert(outbound(vpn, "renamed_awg-interface-1-out").domain_resolver == "renamed_awg-interface-1-domain-resolver", "renamed VPN domain resolver tag");
assert(dns_server(vpn, r => r.tag == "renamed_awg-interface-1-domain-resolver") != null, "renamed VPN domain resolver DNS server");
assert(outbound(vpn, "renamed_awg-interface-1-out").detour == null, "interface outbound must not receive a detour");
assert(route_rule(vpn, r => r.outbound == "renamed_awg-out" && contains(r.domain_suffix, "vpn.example")) != null, "renamed VPN custom domain route");
assert(dns_rule(vpn, r => contains(r.domain_suffix, "vpn.example")) != null, "renamed VPN custom domain DNS rule");
@@ -189,8 +189,7 @@ cat >"$WORK_DIR/hide-added-fixture.json" <<'JSON'
{
"settings": {
"interface": "lan",
"routing_mode": "proxy",
"domain_resolver_enabled": "0"
"routing_mode": "proxy"
},
"section": [
{