Restore per-interface domain resolvers
This commit is contained in:
@@ -340,6 +340,12 @@ EOF
|
||||
|
||||
cat > "$control_dir/conffiles" <<'EOF'
|
||||
/etc/config/forkop
|
||||
EOF
|
||||
|
||||
cat > "$control_dir/postinst" <<'EOF'
|
||||
#!/bin/sh
|
||||
[ -n "${IPKG_INSTROOT}" ] && exit 0
|
||||
FORKOP_LIB=/usr/lib/forkop ucode -L /usr/lib/forkop /usr/lib/forkop/config/migration.uc migrate
|
||||
EOF
|
||||
|
||||
cat > "$control_dir/prerm" <<'EOF'
|
||||
@@ -351,7 +357,7 @@ if (getenv("IPKG_INSTROOT") == null || getenv("IPKG_INSTROOT") == "")
|
||||
exit(0);
|
||||
EOF
|
||||
|
||||
chmod 0755 "$control_dir/prerm"
|
||||
chmod 0755 "$control_dir/postinst" "$control_dir/prerm"
|
||||
}
|
||||
|
||||
write_app_ipk_control() {
|
||||
@@ -487,6 +493,8 @@ EOF
|
||||
|
||||
cat > "$scripts_dir/backend-post-install.sh" <<'EOF'
|
||||
#!/usr/bin/ucode
|
||||
if (getenv("IPKG_INSTROOT") == null || getenv("IPKG_INSTROOT") == "")
|
||||
exit(system("FORKOP_LIB=/usr/lib/forkop ucode -L /usr/lib/forkop /usr/lib/forkop/config/migration.uc migrate"));
|
||||
exit(0);
|
||||
EOF
|
||||
|
||||
@@ -506,6 +514,8 @@ EOF
|
||||
|
||||
cat > "$scripts_dir/backend-post-upgrade.sh" <<'EOF'
|
||||
#!/usr/bin/ucode
|
||||
if (getenv("IPKG_INSTROOT") == null || getenv("IPKG_INSTROOT") == "")
|
||||
exit(system("FORKOP_LIB=/usr/lib/forkop ucode -L /usr/lib/forkop /usr/lib/forkop/config/migration.uc migrate"));
|
||||
exit(0);
|
||||
EOF
|
||||
|
||||
|
||||
+360
-311
File diff suppressed because it is too large
Load Diff
+338
-310
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: FORKOP\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-11 19:00+0300\n"
|
||||
"PO-Revision-Date: 2026-07-11 19:00+0300\n"
|
||||
"POT-Creation-Date: 2026-07-12 20:23+0300\n"
|
||||
"PO-Revision-Date: 2026-07-12 20:23+0300\n"
|
||||
"Last-Translator: ushan0v\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
@@ -431,18 +431,30 @@ 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 requests through section"
|
||||
msgstr "DNS-запросы через секцию"
|
||||
|
||||
msgid "DNS Rewrite TTL"
|
||||
msgstr "Перезапись TTL для 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 "DNS server used to obtain IP addresses for upstream DNS and proxies. If multiple servers are selected, a timeout switches to a backup."
|
||||
msgstr "DNS-сервер для получения IP вышестоящих DNS и прокси. Если выбрать несколько, то при таймауте произойдёт переключение на резервный."
|
||||
|
||||
@@ -476,6 +488,9 @@ msgstr "Не резолвится"
|
||||
msgid "Domain and IP Lists"
|
||||
msgstr "Списки доменов и IPs"
|
||||
|
||||
msgid "Domain Resolver"
|
||||
msgstr "Резолвер доменов"
|
||||
|
||||
msgid "Domains"
|
||||
msgstr "Домены"
|
||||
|
||||
@@ -539,6 +554,9 @@ msgstr "Включить автоматическое обновление уд
|
||||
msgid "Enable autostart"
|
||||
msgstr "Включить автостарт"
|
||||
|
||||
msgid "Enable built-in DNS resolver for domains handled by this section"
|
||||
msgstr "Включить встроенный DNS-резолвер для доменов, обрабатываемых этой секцией"
|
||||
|
||||
msgid "Enable list updates"
|
||||
msgstr "Включить обновление списков"
|
||||
|
||||
@@ -1184,6 +1202,9 @@ msgstr "Имя, которое отображается в списке уров
|
||||
msgid "Network Interface"
|
||||
msgstr "Сетевой интерфейс"
|
||||
|
||||
msgid "Network interface settings"
|
||||
msgstr "Настройки сетевого интерфейса"
|
||||
|
||||
msgid "New version %s is available for %s"
|
||||
msgstr "Доступна новая версия %s для %s"
|
||||
|
||||
|
||||
@@ -48,6 +48,13 @@ if (getenv("IPKG_INSTROOT") == null || getenv("IPKG_INSTROOT") == "")
|
||||
exit(0);
|
||||
endef
|
||||
|
||||
define Package/forkop/postinst
|
||||
#!/bin/sh
|
||||
|
||||
[ -n "$${IPKG_INSTROOT}" ] && exit 0
|
||||
FORKOP_LIB=/usr/lib/forkop ucode -L /usr/lib/forkop /usr/lib/forkop/config/migration.uc migrate
|
||||
endef
|
||||
|
||||
define Package/forkop/conffiles
|
||||
/etc/config/forkop
|
||||
endef
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
config settings 'settings'
|
||||
option config_version '1.0.2'
|
||||
list applied_migrations 'interface_sections'
|
||||
list applied_migrations 'enable_component_checks'
|
||||
option dns_type 'udp'
|
||||
list dns_server '77.88.8.8'
|
||||
list bootstrap_dns_server '77.88.8.8'
|
||||
@@ -38,7 +41,6 @@ config settings 'settings'
|
||||
# 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'
|
||||
@@ -47,6 +49,13 @@ config settings 'settings'
|
||||
# option sort_by_latency '0'
|
||||
# #list fully_routed_ips '192.168.1.2'
|
||||
|
||||
#config section_interface
|
||||
# option section 'subscription_example'
|
||||
# option name 'awg0'
|
||||
# option domain_resolver_enabled '0'
|
||||
# option domain_resolver_dns_type 'udp'
|
||||
# option domain_resolver_dns_server '8.8.8.8'
|
||||
|
||||
#config subscription_url
|
||||
# option section 'subscription_example'
|
||||
# option url 'https://example.com/api/sub'
|
||||
|
||||
@@ -9,6 +9,7 @@ let object_or_empty = common.object_or_empty;
|
||||
const CONFIG_NAME = getenv("FORKOP_CONFIG_NAME") || "forkop";
|
||||
const ITEM_TYPES = [
|
||||
"subscription_url",
|
||||
"section_interface",
|
||||
"urltest",
|
||||
"priority_group",
|
||||
"priority_level"
|
||||
@@ -316,6 +317,10 @@ 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", "");
|
||||
@@ -441,6 +446,35 @@ function subscription_url_settings(section, 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 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_child(section, value) {
|
||||
let child = owned_child_section(section, "urltest", value);
|
||||
if (child != null)
|
||||
@@ -893,6 +927,7 @@ return {
|
||||
rule_sets_with_subnets_value,
|
||||
has_connection_sources,
|
||||
subscription_url_settings,
|
||||
interface_settings,
|
||||
urltest_settings,
|
||||
subscription_update_enabled,
|
||||
subscription_update_interval,
|
||||
@@ -907,6 +942,9 @@ return {
|
||||
subscription_user_agent,
|
||||
subscription_hwid,
|
||||
subscription_download_section,
|
||||
interface_domain_resolver_enabled,
|
||||
interface_domain_resolver_dns_type,
|
||||
interface_domain_resolver_dns_server,
|
||||
urltest_check_interval,
|
||||
urltest_tolerance,
|
||||
urltest_testing_url,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -766,7 +766,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", "subscription_url", "urltest", "priority_group", "priority_level" ]) {
|
||||
for (let type_name in [ "settings", "server", "section", "subscription_url", "section_interface", "urltest", "priority_group", "priority_level" ]) {
|
||||
for (let section in fixture_section_list(type_name)) {
|
||||
if (as_string(section[".name"]) == section_name)
|
||||
return section;
|
||||
|
||||
@@ -936,7 +936,16 @@ function subscription_urls_signature(section) {
|
||||
}
|
||||
|
||||
function interfaces_signature(section) {
|
||||
return sprintf("%J", connections.interfaces(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);
|
||||
}
|
||||
|
||||
function urltests_signature(section) {
|
||||
|
||||
@@ -68,6 +68,10 @@ 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");
|
||||
}
|
||||
@@ -113,6 +117,7 @@ return {
|
||||
tag,
|
||||
inbound_tag,
|
||||
outbound_tag,
|
||||
domain_resolver_tag,
|
||||
server_inbound_tag,
|
||||
tailscale_dns_server_tag
|
||||
};
|
||||
|
||||
@@ -104,7 +104,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", "subscription_url", "urltest", "priority_group", "priority_level" ]) {
|
||||
for (let type_name in [ "settings", "server", "section", "subscription_url", "section_interface", "urltest", "priority_group", "priority_level" ]) {
|
||||
for (let section in fixture_section_list(type_name)) {
|
||||
if (as_string(section[".name"]) == section_name)
|
||||
return section;
|
||||
@@ -1967,12 +1967,29 @@ 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);
|
||||
|
||||
+1
-1420
File diff suppressed because it is too large
Load Diff
@@ -1441,8 +1441,13 @@ function currentDraftOutboundNames(section_id) {
|
||||
names.push(name || `${section_id}-${index + 1}-out`);
|
||||
},
|
||||
);
|
||||
currentSourceOptionValues(section_id, "interfaces").forEach((name) => {
|
||||
const normalized = `${name || ""}`.trim();
|
||||
currentSourceOptionValues(section_id, "interfaces").forEach((itemId) => {
|
||||
const normalized = childItemInputValue(
|
||||
section_id,
|
||||
itemId,
|
||||
"section_interface",
|
||||
"name",
|
||||
).trim();
|
||||
if (normalized) {
|
||||
names.push(normalized);
|
||||
}
|
||||
@@ -1497,6 +1502,14 @@ 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 isConnectionNetworkInterfaceAllowed(deviceName, device) {
|
||||
if (CONNECTIONS_BLOCKED_INTERFACES.includes(deviceName)) {
|
||||
return false;
|
||||
@@ -1567,29 +1580,7 @@ function refreshNetworkInterfaceOptionValues(option) {
|
||||
});
|
||||
}
|
||||
|
||||
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({
|
||||
const InterfaceSettingsDynamicList = SettingsDynamicList.extend({
|
||||
load(section_id) {
|
||||
return network.getDevices().then(
|
||||
L.bind(function (devices) {
|
||||
@@ -1601,30 +1592,9 @@ const InterfaceDynamicList = form.DynamicList.extend({
|
||||
);
|
||||
},
|
||||
|
||||
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,
|
||||
),
|
||||
});
|
||||
validate(section_id, value) {
|
||||
value = childItemInputValue(section_id, value, "section_interface", "name");
|
||||
|
||||
return widget.render();
|
||||
},
|
||||
|
||||
validate(_section_id, value) {
|
||||
if (!value || value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
@@ -1635,6 +1605,15 @@ const InterfaceDynamicList = form.DynamicList.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() {
|
||||
@@ -1740,6 +1719,22 @@ function defaultSubscriptionUrlSettings() {
|
||||
};
|
||||
}
|
||||
|
||||
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",
|
||||
@@ -2071,6 +2066,43 @@ 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"
|
||||
@@ -2901,6 +2933,17 @@ function showSubscriptionUrlSettingsModal(_section_id, itemValue, option) {
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -6533,7 +6576,7 @@ function createSectionContent(section) {
|
||||
|
||||
o = section.taboption(
|
||||
"settings",
|
||||
InterfaceDynamicList,
|
||||
InterfaceSettingsDynamicList,
|
||||
"interfaces",
|
||||
_("Network Interface"),
|
||||
_("Select network interface for VPN connection"),
|
||||
@@ -6542,6 +6585,33 @@ 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];
|
||||
}
|
||||
};
|
||||
outboundNameSourceOptions.set("interfaces", o);
|
||||
|
||||
o = section.taboption(
|
||||
|
||||
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: FORKOP\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-11 19:00+0300\n"
|
||||
"PO-Revision-Date: 2026-07-11 19:00+0300\n"
|
||||
"POT-Creation-Date: 2026-07-12 20:23+0300\n"
|
||||
"PO-Revision-Date: 2026-07-12 20:23+0300\n"
|
||||
"Last-Translator: ushan0v\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
@@ -431,18 +431,30 @@ 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 requests through section"
|
||||
msgstr "DNS-запросы через секцию"
|
||||
|
||||
msgid "DNS Rewrite TTL"
|
||||
msgstr "Перезапись TTL для 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 "DNS server used to obtain IP addresses for upstream DNS and proxies. If multiple servers are selected, a timeout switches to a backup."
|
||||
msgstr "DNS-сервер для получения IP вышестоящих DNS и прокси. Если выбрать несколько, то при таймауте произойдёт переключение на резервный."
|
||||
|
||||
@@ -476,6 +488,9 @@ msgstr "Не резолвится"
|
||||
msgid "Domain and IP Lists"
|
||||
msgstr "Списки доменов и IPs"
|
||||
|
||||
msgid "Domain Resolver"
|
||||
msgstr "Резолвер доменов"
|
||||
|
||||
msgid "Domains"
|
||||
msgstr "Домены"
|
||||
|
||||
@@ -539,6 +554,9 @@ msgstr "Включить автоматическое обновление уд
|
||||
msgid "Enable autostart"
|
||||
msgstr "Включить автостарт"
|
||||
|
||||
msgid "Enable built-in DNS resolver for domains handled by this section"
|
||||
msgstr "Включить встроенный DNS-резолвер для доменов, обрабатываемых этой секцией"
|
||||
|
||||
msgid "Enable list updates"
|
||||
msgstr "Включить обновление списков"
|
||||
|
||||
@@ -1184,6 +1202,9 @@ msgstr "Имя, которое отображается в списке уров
|
||||
msgid "Network Interface"
|
||||
msgstr "Сетевой интерфейс"
|
||||
|
||||
msgid "Network interface settings"
|
||||
msgstr "Настройки сетевого интерфейса"
|
||||
|
||||
msgid "New version %s is available for %s"
|
||||
msgstr "Доступна новая версия %s для %s"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+152
-15
@@ -6,7 +6,8 @@ FORKOP_LIB="$ROOT_DIR/forkop/files/usr/lib"
|
||||
RUNTIME_MIGRATION="$FORKOP_LIB/config/migration.uc"
|
||||
INSTALLER="$ROOT_DIR/install.sh"
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
MIGRATION="$WORK_DIR/config-migration.uc"
|
||||
MIGRATION="$RUNTIME_MIGRATION"
|
||||
MIGRATIONS_DIR="$FORKOP_LIB/config/migrations"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$WORK_DIR"
|
||||
@@ -18,24 +19,23 @@ fail() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
awk '
|
||||
/cat > "\$helper_path" <<'\''FORKOP_CONFIG_MIGRATION_EOF'\''/ { capture = 1; next }
|
||||
capture && /^FORKOP_CONFIG_MIGRATION_EOF$/ { exit }
|
||||
capture { print }
|
||||
' "$INSTALLER" > "$MIGRATION"
|
||||
[ -s "$MIGRATION" ] || fail "failed to extract config migration from install.sh"
|
||||
|
||||
grep -Fq 'Reserved for future Forkop configuration migrations.' "$RUNTIME_MIGRATION" ||
|
||||
fail "runtime config/migration.uc must remain a placeholder"
|
||||
if grep -n -E 'require\("core\.uci"\)|function migrate_|migrate_runtime|migrate-fixture' "$RUNTIME_MIGRATION" >/dev/null 2>&1; then
|
||||
fail "runtime config/migration.uc must not contain migration logic"
|
||||
if grep -n -E 'FORKOP_CONFIG_MIGRATION_EOF|installer_config_migration_path' "$INSTALLER" >/dev/null 2>&1; then
|
||||
fail "install.sh must not embed configuration migration logic"
|
||||
fi
|
||||
[ -s "$MIGRATION" ] || fail "runtime configuration migration module is missing"
|
||||
[ ! -e "$MIGRATIONS_DIR" ] || fail "configuration migrations must stay in one migration.uc file"
|
||||
|
||||
if grep -n -E 'require\("uci"\)\.cursor|uci -q|uci", "-q"' "$MIGRATION" >/dev/null 2>&1; then
|
||||
fail "installer config migration must use core.uci instead of direct UCI cursor or CLI access"
|
||||
fi
|
||||
grep -Fq 'require("core.uci")' "$MIGRATION" ||
|
||||
fail "installer config migration must import core.uci"
|
||||
grep -Fq '{ id: "interface_sections", run: migrate_interface_sections }' "$MIGRATION" ||
|
||||
fail "interface section migration must have a stable named marker"
|
||||
grep -Fq '{ id: "enable_component_checks", run: migrate_enable_component_checks }' "$MIGRATION" ||
|
||||
fail "component check migration must have a stable named marker"
|
||||
grep -Fq 'release_at_most(ctx, "1.0.1")' "$MIGRATION" ||
|
||||
fail "release-specific migrations must use the source release only as a condition"
|
||||
|
||||
eval "$(ucode -L "$FORKOP_LIB" "$FORKOP_LIB/core/constants.uc" shell-env)"
|
||||
export ZAPRET_LEGACY_DEFAULT_NFQWS_OPT ZAPRET_DEFAULT_NFQWS_OPT
|
||||
@@ -155,7 +155,7 @@ const fixture = {
|
||||
process.stdout.write(`${JSON.stringify(fixture, null, 2)}\n`);
|
||||
NODE
|
||||
|
||||
FORKOP_LIB="$FORKOP_LIB" ucode -L "$FORKOP_LIB" "$MIGRATION" migrate-fixture "$WORK_DIR/fixture.json" >"$WORK_DIR/output.json"
|
||||
FORKOP_LIB="$FORKOP_LIB" ucode -L "$FORKOP_LIB" "$MIGRATION" migrate-fixture "$WORK_DIR/fixture.json" podkop >"$WORK_DIR/output.json"
|
||||
|
||||
node - "$WORK_DIR/output.json" <<'NODE'
|
||||
const fs = require('fs');
|
||||
@@ -164,10 +164,14 @@ const config = out.config;
|
||||
const sections = Object.fromEntries(config.section.map(section => [section['.name'], section]));
|
||||
const childByType = type => config[type] || [];
|
||||
const subscriptionUrls = childByType('subscription_url');
|
||||
const interfaces = childByType('section_interface');
|
||||
const urltests = childByType('urltest');
|
||||
|
||||
assert(JSON.stringify(config.settings.dns_server) === JSON.stringify(['9.9.9.9']), 'legacy main DNS scalar migrated to ordered list');
|
||||
assert(JSON.stringify(config.settings.bootstrap_dns_server) === JSON.stringify(['1.1.1.1']), 'legacy Bootstrap DNS scalar migrated to ordered list');
|
||||
assert(config.settings.config_version === '1.0.2', 'legacy config should be marked at the current schema version');
|
||||
assert(config.settings.component_update_check_enabled === '1', 'component update checks should be enabled during migration');
|
||||
assert(JSON.stringify(config.settings.applied_migrations) === JSON.stringify(['interface_sections', 'enable_component_checks']), 'legacy config should record named migrations');
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
@@ -319,9 +323,14 @@ absent(legacyZap, 'cmd_opts', 'legacy-zap');
|
||||
|
||||
const legacyVpn = sections['legacy-vpn'];
|
||||
assert(legacyVpn.action === 'connection', 'vpn action inferred');
|
||||
assert(JSON.stringify(legacyVpn.interfaces) === JSON.stringify(['awg0']), 'vpn interface migrated to section list');
|
||||
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');
|
||||
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, 'domain_resolver_enabled', 'legacy-vpn');
|
||||
absent(legacyVpn, 'domain_resolver_dns_type', 'legacy-vpn');
|
||||
@@ -344,6 +353,83 @@ assert(out.removed_caches.includes('/tmp/sing-box/subscriptions/legacy-sub.json'
|
||||
assert(out.removed_caches.includes('/var/run/forkop/section-cache/legacy-sub.json'), 'section cache removal');
|
||||
NODE
|
||||
|
||||
cat >"$WORK_DIR/forkop-1.0.1.json" <<'JSON'
|
||||
{
|
||||
"settings": {
|
||||
".name": "settings",
|
||||
".type": "settings",
|
||||
"config_version": "1.0.1",
|
||||
"component_update_check_enabled": "0"
|
||||
},
|
||||
"section": [
|
||||
{
|
||||
".name": "main",
|
||||
".type": "section",
|
||||
"action": "connection",
|
||||
"interfaces": [ "awg0", "tun0" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
JSON
|
||||
|
||||
FORKOP_LIB="$FORKOP_LIB" ucode -L "$FORKOP_LIB" "$MIGRATION" migrate-fixture "$WORK_DIR/forkop-1.0.1.json" >"$WORK_DIR/forkop-1.0.2.json"
|
||||
|
||||
node - "$WORK_DIR/forkop-1.0.2.json" <<'NODE'
|
||||
const fs = require('fs');
|
||||
const out = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
||||
const section = out.config.section[0];
|
||||
const interfaces = out.config.section_interface || [];
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
console.error(message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
assert(out.changed === true, '1.0.1 config should require migration');
|
||||
assert(out.config.settings.config_version === '1.0.2', 'config schema version should advance to 1.0.2');
|
||||
assert(out.config.settings.component_update_check_enabled === '1', 'updates from 1.0.1 and below should enable component update checks');
|
||||
assert(JSON.stringify(out.config.settings.applied_migrations) === JSON.stringify(['interface_sections', 'enable_component_checks']), 'named migrations should be recorded');
|
||||
assert(!Object.prototype.hasOwnProperty.call(section, 'interfaces'), 'parent interface list should be removed');
|
||||
assert(JSON.stringify(interfaces.map(item => item.name)) === JSON.stringify(['awg0', 'tun0']), 'interfaces should keep their order');
|
||||
for (const item of interfaces) {
|
||||
assert(item.section === 'main', 'interface child should reference its parent');
|
||||
assert(item.domain_resolver_enabled === '0', 'resolver should default to disabled');
|
||||
assert(item.domain_resolver_dns_type === 'udp', 'resolver protocol default');
|
||||
assert(item.domain_resolver_dns_server === '8.8.8.8', 'resolver server default');
|
||||
}
|
||||
NODE
|
||||
|
||||
cat >"$WORK_DIR/forkop-1.0.2-disabled.json" <<'JSON'
|
||||
{
|
||||
"settings": {
|
||||
".name": "settings",
|
||||
".type": "settings",
|
||||
"config_version": "1.0.2",
|
||||
"component_update_check_enabled": "0"
|
||||
},
|
||||
"section": []
|
||||
}
|
||||
JSON
|
||||
|
||||
FORKOP_LIB="$FORKOP_LIB" ucode -L "$FORKOP_LIB" "$MIGRATION" migrate-fixture "$WORK_DIR/forkop-1.0.2-disabled.json" >"$WORK_DIR/forkop-1.0.2-disabled-output.json"
|
||||
|
||||
node - "$WORK_DIR/forkop-1.0.2-disabled-output.json" <<'NODE'
|
||||
const fs = require('fs');
|
||||
const out = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
console.error(message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
assert(out.config.settings.component_update_check_enabled === '0', '1.0.2 config must preserve an explicitly disabled component check');
|
||||
assert(JSON.stringify(out.config.settings.applied_migrations) === JSON.stringify(['interface_sections', 'enable_component_checks']), 'newer configs should mark skipped migrations');
|
||||
NODE
|
||||
|
||||
cat >"$WORK_DIR/runtime-migrate.state" <<'EOF_UCI'
|
||||
forkop.settings=settings
|
||||
forkop.settings.routing_excluded_ips=192.0.2.0/24
|
||||
@@ -355,6 +441,13 @@ forkop.legacy.proxy_string=vless://one
|
||||
forkop.old_direct=section
|
||||
forkop.old_direct.enabled=1
|
||||
forkop.old_direct.action=direct
|
||||
forkop.vpn=section
|
||||
forkop.vpn.enabled=1
|
||||
forkop.vpn.connection_type=vpn
|
||||
forkop.vpn.interface=awg0
|
||||
forkop.vpn.domain_resolver_enabled=1
|
||||
forkop.vpn.domain_resolver_dns_type=doh
|
||||
forkop.vpn.domain_resolver_dns_server=https://dns.example/dns-query
|
||||
EOF_UCI
|
||||
mkdir -p "$WORK_DIR/runtime" "$WORK_DIR/persistent-cache"
|
||||
: >"$WORK_DIR/runtime-migrate.log"
|
||||
@@ -365,7 +458,7 @@ TMP_SUBSCRIPTION_FOLDER="$WORK_DIR/tmp-subscriptions" \
|
||||
FORKOP_RUNTIME_STATE_DIR="$WORK_DIR/runtime" \
|
||||
FORKOP_PERSISTENT_SUBSCRIPTION_CACHE_DIR="$WORK_DIR/persistent-cache" \
|
||||
FORKOP_INTERNAL_CONFIG_TRIGGER_GUARD="$WORK_DIR/internal-config-change" \
|
||||
ucode -L "$FORKOP_LIB" "$MIGRATION" migrate
|
||||
ucode -L "$FORKOP_LIB" "$MIGRATION" migrate-podkop
|
||||
|
||||
grep -Fxq 'forkop.legacy=section' "$WORK_DIR/runtime-migrate.state" ||
|
||||
fail "runtime migration must convert legacy rule type through core.uci"
|
||||
@@ -379,6 +472,12 @@ grep -Fxq 'forkop.legacy.selector_proxy_links=vless://one' "$WORK_DIR/runtime-mi
|
||||
if grep -Fq '=connection_url' "$WORK_DIR/runtime-migrate.state"; then
|
||||
fail "runtime migration must not create connection_url child sections"
|
||||
fi
|
||||
grep -Eq '^forkop\.cfg[0-9a-f]+=section_interface$' "$WORK_DIR/runtime-migrate.state" ||
|
||||
fail "runtime migration must create an anonymous interface settings section"
|
||||
grep -Eq '^forkop\.cfg[0-9a-f]+\.section=vpn$' "$WORK_DIR/runtime-migrate.state" ||
|
||||
fail "runtime interface settings section must reference its parent"
|
||||
grep -Eq '^forkop\.cfg[0-9a-f]+\.name=' "$WORK_DIR/runtime-migrate.state" ||
|
||||
fail "runtime interface settings section must store the interface name"
|
||||
if grep -Fq 'forkop.legacy.urltest_enabled=' "$WORK_DIR/runtime-migrate.state"; then
|
||||
fail "runtime migration must delete migrated urltest_enabled through core.uci"
|
||||
fi
|
||||
@@ -391,6 +490,44 @@ fi
|
||||
grep -Fxq 'commit forkop' "$WORK_DIR/runtime-migrate.log" ||
|
||||
fail "runtime migration must commit through core.uci"
|
||||
|
||||
cat >"$WORK_DIR/runtime-version.state" <<'EOF_UCI'
|
||||
forkop.settings=settings
|
||||
forkop.settings.config_version=1.0.1
|
||||
forkop.settings.component_update_check_enabled=0
|
||||
forkop.main=section
|
||||
forkop.main.enabled=1
|
||||
forkop.main.action=connection
|
||||
forkop.main.interfaces=awg0
|
||||
EOF_UCI
|
||||
: >"$WORK_DIR/runtime-version.log"
|
||||
FORKOP_UCI_STATE_FILE="$WORK_DIR/runtime-version.state" \
|
||||
FORKOP_UCI_LOG_FILE="$WORK_DIR/runtime-version.log" \
|
||||
FORKOP_CONFIG_NAME="forkop" \
|
||||
FORKOP_INTERNAL_CONFIG_TRIGGER_GUARD="$WORK_DIR/internal-config-change" \
|
||||
ucode -L "$FORKOP_LIB" "$MIGRATION" migrate
|
||||
|
||||
grep -Fxq 'forkop.settings.config_version=1.0.2' "$WORK_DIR/runtime-version.state" ||
|
||||
fail "runtime version migration must advance config_version"
|
||||
grep -Fxq 'forkop.settings.component_update_check_enabled=1' "$WORK_DIR/runtime-version.state" ||
|
||||
fail "runtime version migration must enable component update checks"
|
||||
grep -Fq 'forkop.settings.applied_migrations=interface_sections enable_component_checks' "$WORK_DIR/runtime-version.state" ||
|
||||
fail "runtime migration must record stable migration names"
|
||||
grep -Eq '^forkop\.cfg[0-9a-f]+=section_interface$' "$WORK_DIR/runtime-version.state" ||
|
||||
fail "runtime version migration must create interface child settings"
|
||||
if grep -Fq 'forkop.main.interfaces=' "$WORK_DIR/runtime-version.state"; then
|
||||
fail "runtime version migration must remove the parent interface list"
|
||||
fi
|
||||
|
||||
: >"$WORK_DIR/runtime-version.log"
|
||||
FORKOP_UCI_STATE_FILE="$WORK_DIR/runtime-version.state" \
|
||||
FORKOP_UCI_LOG_FILE="$WORK_DIR/runtime-version.log" \
|
||||
FORKOP_CONFIG_NAME="forkop" \
|
||||
FORKOP_INTERNAL_CONFIG_TRIGGER_GUARD="$WORK_DIR/internal-config-change" \
|
||||
ucode -L "$FORKOP_LIB" "$MIGRATION" migrate
|
||||
if grep -Fq 'commit forkop' "$WORK_DIR/runtime-version.log"; then
|
||||
fail "completed named migrations must be idempotent"
|
||||
fi
|
||||
|
||||
: >"$WORK_DIR/uci-commit.log"
|
||||
: >"$WORK_DIR/uci-commit.state"
|
||||
FORKOP_UCI_STATE_FILE="$WORK_DIR/uci-commit.state" \
|
||||
|
||||
@@ -86,10 +86,11 @@ grep -Fq 'install_json_ucode installer-cleanup-legacy' "$INSTALLER" ||
|
||||
fail "install.sh cleanup must delegate to embedded ucode"
|
||||
grep -Fq 'install_json_ucode installer-post-install' "$INSTALLER" ||
|
||||
fail "install.sh post-install must delegate to embedded ucode"
|
||||
grep -Fq 'installer_config_migration_path()' "$INSTALLER" ||
|
||||
fail "install.sh must embed the one-time configuration migration"
|
||||
grep -Fq 'ucode -L /usr/lib/forkop "$(installer_config_migration_path)" migrate' "$INSTALLER" ||
|
||||
fail "install.sh must execute the embedded migration only during the legacy transition"
|
||||
if grep -Fq 'installer_config_migration_path()' "$INSTALLER"; then
|
||||
fail "install.sh must not embed configuration migration logic"
|
||||
fi
|
||||
grep -Fq 'ucode -L /usr/lib/forkop /usr/lib/forkop/config/migration.uc migrate-podkop' "$INSTALLER" ||
|
||||
fail "install.sh must delegate the legacy transition to the installed migration module"
|
||||
|
||||
if grep -n -E 'restore_forkop_dnsmasq_failsafe|remember_service_state|stop_conflicting_services|deactivate_original_forkop_if_present|remove_conflicting_dns_proxy|pkg_remove_if_installed|pkg_remove_matching_prefix|pkg_list_installed_names' "$INSTALLER" >/dev/null 2>&1; then
|
||||
fail "install.sh must not keep shell cleanup/remove service owners"
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SECTION_JS="$ROOT_DIR/luci-app-forkop/htdocs/luci-static/resources/view/forkop/section.js"
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
grep -Fq 'function dnsTypeChoices() {' "$SECTION_JS" ||
|
||||
fail "network interface settings must define DNS protocol choices"
|
||||
grep -Fq 'dnsTypeChoices().forEach((choice) => o.value(choice.value, choice.label));' "$SECTION_JS" ||
|
||||
fail "network interface settings must populate the DNS protocol field"
|
||||
grep -Fq 'o.renderItemSettingsModal = showInterfaceSettingsModal;' "$SECTION_JS" ||
|
||||
fail "network interfaces must keep their settings modal handler"
|
||||
|
||||
printf 'LuCI network interface settings checks passed\n'
|
||||
@@ -52,6 +52,16 @@ grep -Fq 'APK_INTERNAL_VERSION="$RELEASE_VERSION"' "$BUILD_SCRIPT" ||
|
||||
fail "build.sh must use the exact three-part release version for APK metadata"
|
||||
grep -Fq "option component_update_check_enabled '1'" "$FORKOP_CONFIG" ||
|
||||
fail "new installations must enable component update checks by default"
|
||||
grep -Fq "option config_version '1.0.2'" "$FORKOP_CONFIG" ||
|
||||
fail "new installations must start at the current configuration schema version"
|
||||
grep -Fq "list applied_migrations 'interface_sections'" "$FORKOP_CONFIG" ||
|
||||
fail "new installations must mark the interface section migration as applied"
|
||||
grep -Fq "list applied_migrations 'enable_component_checks'" "$FORKOP_CONFIG" ||
|
||||
fail "new installations must mark the component check migration as applied"
|
||||
grep -Fq '/usr/lib/forkop/config/migration.uc migrate' "$FORKOP_MAKEFILE" ||
|
||||
fail "OpenWrt package postinst must run configuration migrations"
|
||||
[ "$(grep -Fc '/usr/lib/forkop/config/migration.uc migrate' "$BUILD_SCRIPT")" -ge 3 ] ||
|
||||
fail "manual IPK/APK package scripts must run configuration migrations after install and upgrade"
|
||||
|
||||
if grep -Rqs 'require("uci")' "$FORKOP_LIB"; then
|
||||
require_package_dependency "ucode-mod-uci"
|
||||
|
||||
@@ -401,6 +401,17 @@ cat >"$WORK_DIR/sing-box-signature.json" <<'JSON'
|
||||
"resolve_real_ip_for_routing": "yes"
|
||||
}
|
||||
],
|
||||
"section_interface": [
|
||||
{
|
||||
".name": "vpn-interface",
|
||||
".type": "section_interface",
|
||||
"section": "vpn1",
|
||||
"name": "wg0",
|
||||
"domain_resolver_enabled": "1",
|
||||
"domain_resolver_dns_type": "doh",
|
||||
"domain_resolver_dns_server": "https://dns.example/dns-query"
|
||||
}
|
||||
],
|
||||
"server": [
|
||||
{
|
||||
".name": "srv_disabled",
|
||||
@@ -665,7 +676,7 @@ vpn
|
||||
[rule.vpn1.subscription_urls]
|
||||
[ ]
|
||||
[rule.vpn1.interfaces]
|
||||
[ "wg0" ]
|
||||
[ { "name": "wg0", "domain_resolver_enabled": "1", "domain_resolver_dns_type": "doh", "domain_resolver_dns_server": "https://dns.example/dns-query" } ]
|
||||
[rule.vpn1.outbound_jsons]
|
||||
|
||||
[rule.vpn1.legacy_interface]
|
||||
|
||||
@@ -409,9 +409,19 @@ cat >"$WORK_DIR/vpn-interface-fixture.json" <<'JSON'
|
||||
".type": "section",
|
||||
"enabled": "1",
|
||||
"action": "connection",
|
||||
"interfaces": [ "tun0" ],
|
||||
"domain_suffix": [ "vpn.example" ]
|
||||
}
|
||||
],
|
||||
"section_interface": [
|
||||
{
|
||||
".name": "vpn-interface",
|
||||
".type": "section_interface",
|
||||
"section": "renamed_awg",
|
||||
"name": "tun0",
|
||||
"domain_resolver_enabled": "1",
|
||||
"domain_resolver_dns_type": "doh",
|
||||
"domain_resolver_dns_server": "https://dns.example/dns-query"
|
||||
}
|
||||
]
|
||||
}
|
||||
JSON
|
||||
@@ -1105,6 +1115,8 @@ 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");
|
||||
|
||||
Reference in New Issue
Block a user