Add prioritized DNS failover

This commit is contained in:
ushan0v
2026-07-11 00:10:57 +03:00
parent ef4bb76ad3
commit bf8eb5e2f9
32 changed files with 3089 additions and 1432 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+52 -13
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PODKOP PLUS\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-10 15:25+0300\n"
"PO-Revision-Date: 2026-07-10 15:25+0300\n"
"POT-Creation-Date: 2026-07-10 22:46+0300\n"
"PO-Revision-Date: 2026-07-10 22:46+0300\n"
"Last-Translator: ushan0v\n"
"Language-Team: none\n"
"Language: ru\n"
@@ -50,15 +50,24 @@ msgstr "Действие"
msgid "Active"
msgstr "Активные"
msgid "Active Bootstrap DNS"
msgstr "Активный Bootstrap DNS"
msgid "Active Connections"
msgstr "Активные соединения"
msgid "Active Main DNS"
msgstr "Активный основной DNS"
msgid "Add a section"
msgstr "Добавить секцию"
msgid "Add a server inbound"
msgstr "Добавить серверный inbound"
msgid "Add at least one DNS server"
msgstr "Добавьте хотя бы один DNS-сервер"
msgid "Add prefix to nodes"
msgstr "Добавлять префикс к узлам"
@@ -149,11 +158,11 @@ msgstr "Доступные действия"
msgid "Backend validation failed: %s"
msgstr "Backend-проверка завершилась ошибкой: %s"
msgid "Bootsrap DNS"
msgid "Bootstrap DNS"
msgstr "Bootstrap DNS"
msgid "Bootstrap DNS server"
msgstr "Bootstrap DNS-сервер"
msgid "Bootstrap DNS Servers"
msgstr "Bootstrap DNS-серверы"
msgid "Browser FakeIP check could not be completed"
msgstr "Не удалось выполнить проверку FakeIP в браузере"
@@ -386,6 +395,9 @@ msgstr "Напрямую"
msgid "Direct connection"
msgstr "Прямое подключение"
msgid "Direct DNS servers used to resolve upstream DNS and proxy addresses. The first available server has priority."
msgstr "Прямые DNS-серверы для разрешения адресов вышестоящих DNS и прокси. Первый доступный сервер имеет приоритет."
msgid "Disable autostart"
msgstr "Отключить автостарт"
@@ -404,6 +416,9 @@ msgstr "Отмена"
msgid "Display name"
msgstr "Отображаемое имя"
msgid "DNS Check Interval"
msgstr "Период проверки DNS"
msgid "DNS on router"
msgstr "DNS на роутере"
@@ -416,15 +431,24 @@ msgstr "DNS через TLS (DoT)"
msgid "DNS Protocol Type"
msgstr "Тип протокола DNS"
msgid "DNS requests through"
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 Servers"
msgstr "DNS-серверы"
msgid "DNS through a section"
msgstr "DNS через секцию"
msgid "DNS Unavailability Timeout"
msgstr "Таймаут недоступности DNS"
msgid "Do not panic, everything can be fixed, just..."
msgstr "Не паникуйте, всё можно исправить, просто..."
@@ -677,6 +701,9 @@ msgstr "Скрывать значения"
msgid "Higher-level check interval"
msgstr "Интервал проверки высших уровней"
msgid "Higher-priority DNS Check"
msgstr "Проверка верхних DNS"
msgid "Host"
msgstr "Хост"
@@ -686,6 +713,12 @@ msgstr "Как часто проверять верхние уровни при
msgid "How often the currently selected server is checked"
msgstr "Как часто проверяется текущий выбранный сервер"
msgid "How often to check the active DNS servers."
msgstr "Как часто проверять активные DNS-серверы."
msgid "How often to check whether a higher-priority DNS server has recovered."
msgstr "Как часто проверять, восстановился ли DNS-сервер с более высоким приоритетом."
msgid "HTTP error"
msgstr "Ошибка HTTP"
@@ -1049,6 +1082,9 @@ msgstr "Совпадение по IP-адресам или подсетям на
msgid "Match destination ports. Use a single port or a range"
msgstr "Совпадение по портам назначения. Используйте одиночный порт или диапазон"
msgid "Maximum time to wait for example.com to resolve during a DNS health check."
msgstr "Максимальное время ожидания разрешения example.com при проверке доступности DNS."
msgid "Memory Usage"
msgstr "Использование памяти"
@@ -1526,8 +1562,8 @@ msgstr "Выберите сетевой интерфейс для VPN-подкл
msgid "Select or enter a User-Agent"
msgstr "Выберите или введите User-Agent"
msgid "Select or enter DNS server address"
msgstr "Выберите или введите адрес DNS-сервера"
msgid "Select or enter DNS server addresses. The first available server has priority."
msgstr "Выберите или введите адреса DNS-серверов. Первый доступный сервер имеет приоритет."
msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing"
msgstr "Выберите или введите путь к файлу кеша sing-box. Изменяйте это, ТОЛЬКО если вы знаете, что делаете"
@@ -1559,6 +1595,9 @@ msgstr "Выбранная секция"
msgid "Selector and URLTest outbounds must contain a non-empty outbounds array"
msgstr "Selector и URLTest outbound должны содержать непустой массив outbounds"
msgid "Send main DNS requests through the selected section. Bootstrap DNS remains direct."
msgstr "Отправлять запросы основного DNS через выбранную секцию. Bootstrap DNS остаётся прямым."
msgid "Server"
msgstr "Сервер"
@@ -1763,9 +1802,6 @@ msgstr "URL проверки"
msgid "The desync fwmark is managed by Podkop Plus for loop prevention and must not be overridden here."
msgstr "Desync fwmark управляется Podkop Plus для защиты от циклов и не должен переопределяться здесь."
msgid "The DNS server used to look up the IP address of an upstream DNS server"
msgstr "DNS-сервер, используемый для поиска IP-адреса вышестоящего DNS-сервера"
msgid "The NFQUEUE number is assigned by Podkop Plus for each rule and must not be overridden here."
msgstr "Номер NFQUEUE назначается Podkop Plus для каждого правила и не должен переопределяться здесь."
@@ -1997,6 +2033,9 @@ msgstr "Используйте CIDR-префиксы или IP-адреса, н
msgid "Use letters, digits, dots, underscores, or hyphens"
msgstr "Используйте буквы, цифры, точки, подчёркивания или дефисы"
msgid "Use sing-box duration format like 10s, 1m or 2m30s"
msgstr "Используйте формат длительности sing-box, например 10s, 1m или 2m30s"
msgid "Use sing-box duration format like 1d, 12h or 30m"
msgstr "Используйте формат длительности sing-box, например 1d, 12h или 30m"
@@ -46,19 +46,29 @@ export async function runDnsCheck() {
...insertIf<IDiagnosticsChecksItem>(
data.dns_type === 'doh' ||
data.dns_type === 'dot' ||
data.bootstrap_dns_server_count > 1 ||
!data.bootstrap_dns_status,
[
{
state: data.bootstrap_dns_status ? 'success' : 'error',
key: _('Bootsrap DNS'),
value: data.bootstrap_dns_server,
key:
data.bootstrap_dns_server_count > 1
? _('Active Bootstrap DNS')
: _('Bootstrap DNS'),
value:
data.bootstrap_dns_server_count > 1
? `${data.bootstrap_dns_server} (${data.bootstrap_dns_server_index + 1}/${data.bootstrap_dns_server_count})`
: data.bootstrap_dns_server,
},
],
),
{
state: data.dns_status ? 'success' : 'error',
key: _('Main DNS'),
value: `${data.dns_server} [${data.dns_type}]`,
key: data.dns_server_count > 1 ? _('Active Main DNS') : _('Main DNS'),
value:
data.dns_server_count > 1
? `${data.dns_server} [${data.dns_type}] (${data.dns_server_index + 1}/${data.dns_server_count})`
: `${data.dns_server} [${data.dns_type}]`,
},
{
state: data.dns_on_router ? 'success' : 'error',
@@ -58,6 +58,8 @@ const PODKOP_MASK_AFTER_TOKEN_SPACE = [
'list fully_routed_ips',
'option dns_server',
'option bootstrap_dns_server',
'list dns_server',
'list bootstrap_dns_server',
'option listen',
'option listen_port',
'option public_host',
@@ -156,6 +158,7 @@ function maskGlobalCheckLine(line: string) {
}
maskedLine = maskOptionPath(maskedLine, "option dns_server '");
maskedLine = maskOptionPath(maskedLine, "list dns_server '");
return maskedLine;
}
+4
View File
@@ -351,9 +351,13 @@ export namespace Podkop {
export interface DnsCheckResult {
dns_type: 'udp' | 'doh' | 'dot';
dns_server: string;
dns_server_index: number;
dns_server_count: number;
dns_status: 0 | 1;
dns_on_router: 0 | 1;
bootstrap_dns_server: string;
bootstrap_dns_server_index: number;
bootstrap_dns_server_count: number;
bootstrap_dns_status: 0 | 1;
dhcp_config_status: 0 | 1;
dont_touch_dhcp: 0 | 1;
@@ -7623,19 +7623,19 @@ async function runDnsCheck() {
state,
items: [
...insertIf(
data.dns_type === "doh" || data.dns_type === "dot" || !data.bootstrap_dns_status,
data.dns_type === "doh" || data.dns_type === "dot" || data.bootstrap_dns_server_count > 1 || !data.bootstrap_dns_status,
[
{
state: data.bootstrap_dns_status ? "success" : "error",
key: _("Bootsrap DNS"),
value: data.bootstrap_dns_server
key: data.bootstrap_dns_server_count > 1 ? _("Active Bootstrap DNS") : _("Bootstrap DNS"),
value: data.bootstrap_dns_server_count > 1 ? `${data.bootstrap_dns_server} (${data.bootstrap_dns_server_index + 1}/${data.bootstrap_dns_server_count})` : data.bootstrap_dns_server
}
]
),
{
state: data.dns_status ? "success" : "error",
key: _("Main DNS"),
value: `${data.dns_server} [${data.dns_type}]`
key: data.dns_server_count > 1 ? _("Active Main DNS") : _("Main DNS"),
value: data.dns_server_count > 1 ? `${data.dns_server} [${data.dns_type}] (${data.dns_server_index + 1}/${data.dns_server_count})` : `${data.dns_server} [${data.dns_type}]`
},
{
state: data.dns_on_router ? "success" : "error",
@@ -9500,6 +9500,8 @@ var PODKOP_MASK_AFTER_TOKEN_SPACE = [
"list fully_routed_ips",
"option dns_server",
"option bootstrap_dns_server",
"list dns_server",
"list bootstrap_dns_server",
"option listen",
"option listen_port",
"option public_host",
@@ -9572,6 +9574,7 @@ function maskGlobalCheckLine(line) {
maskedLine = maskAfterTokenSpace(maskedLine, token);
}
maskedLine = maskOptionPath(maskedLine, "option dns_server '");
maskedLine = maskOptionPath(maskedLine, "list dns_server '");
return maskedLine;
}
function maskMultilineContinuation(line) {
@@ -58,11 +58,7 @@ function refreshDownloadSectionChoices(option, capabilities) {
}
}
function configureDownloadSectionOption(
option,
sectionOption,
capabilities,
) {
function configureDownloadSectionOption(option, sectionOption, capabilities) {
option.default = "";
option.rmempty = false;
option.cfgvalue = function (section_id) {
@@ -101,6 +97,62 @@ function configureDownloadViaProxyFlag(option, sectionOption) {
};
}
function optionListValues(option, section_id) {
const formValue = option.formvalue(section_id);
const value = formValue != null ? formValue : option.cfgvalue(section_id);
return L.toArray(value)
.map((item) => `${item || ""}`.trim())
.filter(Boolean);
}
function configureDnsList(option, choices, defaultValue) {
Object.entries(choices).forEach(([key, label]) => {
option.value(key, _(label));
});
option.default = [defaultValue];
option.rmempty = false;
option.validate = function (_section_id, value) {
const normalized = `${value || ""}`.trim();
if (!normalized) {
return optionListValues(option, _section_id).length > 0
? true
: _("Add at least one DNS server");
}
const validation = main.validateDNS(normalized);
return validation.valid ? true : validation.message;
};
}
function configureDnsFailoverVisibility(option, dnsOption, bootstrapOption) {
option.depends("dns_server", "__podkop_multiple_dns__");
option.depends("bootstrap_dns_server", "__podkop_multiple_dns__");
option.retain = true;
option.checkDepends = function (section_id) {
return (
optionListValues(dnsOption, section_id).length > 1 ||
optionListValues(bootstrapOption, section_id).length > 1
);
};
}
function configureDnsDuration(
option,
defaultValue,
dnsOption,
bootstrapOption,
) {
option.default = defaultValue;
option.rmempty = false;
option.validate = function (_section_id, value) {
const normalized = `${value || ""}`.trim();
if (!normalized || !isSingBoxDuration(normalized)) {
return _("Use sing-box duration format like 10s, 1m or 2m30s");
}
return true;
};
configureDnsFailoverVisibility(option, dnsOption, bootstrapOption);
}
function createSettingsContent(section, capabilities) {
let o = section.option(
form.ListValue,
@@ -114,49 +166,55 @@ function createSettingsContent(section, capabilities) {
o.default = "udp";
o.rmempty = false;
o = section.option(
form.Value,
const dnsOption = section.option(
form.DynamicList,
"dns_server",
_("DNS Server"),
_("Select or enter DNS server address"),
);
Object.entries(main.DNS_SERVER_OPTIONS).forEach(([key, label]) => {
o.value(key, _(label));
});
o.default = "8.8.8.8";
o.rmempty = false;
o.validate = function (section_id, value) {
const validation = main.validateDNS(value);
if (validation.valid) {
return true;
}
return validation.message;
};
o = section.option(
form.Value,
"bootstrap_dns_server",
_("Bootstrap DNS server"),
_("DNS Servers"),
_(
"The DNS server used to look up the IP address of an upstream DNS server",
"Select or enter DNS server addresses. The first available server has priority.",
),
);
Object.entries(main.BOOTSTRAP_DNS_SERVER_OPTIONS).forEach(([key, label]) => {
o.value(key, _(label));
});
o.default = "77.88.8.8";
o.rmempty = false;
o.validate = function (section_id, value) {
const validation = main.validateDNS(value);
configureDnsList(dnsOption, main.DNS_SERVER_OPTIONS, "77.88.8.8");
if (validation.valid) {
return true;
}
const bootstrapOption = section.option(
form.DynamicList,
"bootstrap_dns_server",
_("Bootstrap DNS Servers"),
_(
"Direct DNS servers used to resolve upstream DNS and proxy addresses. The first available server has priority.",
),
);
configureDnsList(
bootstrapOption,
main.BOOTSTRAP_DNS_SERVER_OPTIONS,
"77.88.8.8",
);
return validation.message;
};
o = section.option(
form.Value,
"dns_check_interval",
_("DNS Check Interval"),
_("How often to check the active DNS servers."),
);
configureDnsDuration(o, "10s", dnsOption, bootstrapOption);
o = section.option(
form.Value,
"dns_recovery_check_interval",
_("Higher-priority DNS Check"),
_("How often to check whether a higher-priority DNS server has recovered."),
);
configureDnsDuration(o, "60s", dnsOption, bootstrapOption);
o = section.option(
form.Value,
"dns_check_timeout",
_("DNS Unavailability Timeout"),
_(
"Maximum time to wait for example.com to resolve during a DNS health check.",
),
);
configureDnsDuration(o, "2s", dnsOption, bootstrapOption);
o = section.option(
form.Value,
@@ -179,6 +237,24 @@ function createSettingsContent(section, capabilities) {
return true;
};
o = section.option(
form.Flag,
"dns_detour_enabled",
_("DNS through a section"),
_(
"Send main DNS requests through the selected section. Bootstrap DNS remains direct.",
),
);
configureDownloadViaProxyFlag(o, "dns_detour_section");
o = section.option(
form.ListValue,
"dns_detour_section",
_("DNS requests through"),
);
o.depends("dns_detour_enabled", "1");
configureDownloadSectionOption(o, "dns_detour_section", capabilities);
o = section.option(
widgets.DeviceSelect,
"source_network_interfaces",
+52 -13
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PODKOP PLUS\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-10 15:25+0300\n"
"PO-Revision-Date: 2026-07-10 15:25+0300\n"
"POT-Creation-Date: 2026-07-10 22:46+0300\n"
"PO-Revision-Date: 2026-07-10 22:46+0300\n"
"Last-Translator: ushan0v\n"
"Language-Team: none\n"
"Language: ru\n"
@@ -50,15 +50,24 @@ msgstr "Действие"
msgid "Active"
msgstr "Активные"
msgid "Active Bootstrap DNS"
msgstr "Активный Bootstrap DNS"
msgid "Active Connections"
msgstr "Активные соединения"
msgid "Active Main DNS"
msgstr "Активный основной DNS"
msgid "Add a section"
msgstr "Добавить секцию"
msgid "Add a server inbound"
msgstr "Добавить серверный inbound"
msgid "Add at least one DNS server"
msgstr "Добавьте хотя бы один DNS-сервер"
msgid "Add prefix to nodes"
msgstr "Добавлять префикс к узлам"
@@ -149,11 +158,11 @@ msgstr "Доступные действия"
msgid "Backend validation failed: %s"
msgstr "Backend-проверка завершилась ошибкой: %s"
msgid "Bootsrap DNS"
msgid "Bootstrap DNS"
msgstr "Bootstrap DNS"
msgid "Bootstrap DNS server"
msgstr "Bootstrap DNS-сервер"
msgid "Bootstrap DNS Servers"
msgstr "Bootstrap DNS-серверы"
msgid "Browser FakeIP check could not be completed"
msgstr "Не удалось выполнить проверку FakeIP в браузере"
@@ -386,6 +395,9 @@ msgstr "Напрямую"
msgid "Direct connection"
msgstr "Прямое подключение"
msgid "Direct DNS servers used to resolve upstream DNS and proxy addresses. The first available server has priority."
msgstr "Прямые DNS-серверы для разрешения адресов вышестоящих DNS и прокси. Первый доступный сервер имеет приоритет."
msgid "Disable autostart"
msgstr "Отключить автостарт"
@@ -404,6 +416,9 @@ msgstr "Отмена"
msgid "Display name"
msgstr "Отображаемое имя"
msgid "DNS Check Interval"
msgstr "Период проверки DNS"
msgid "DNS on router"
msgstr "DNS на роутере"
@@ -416,15 +431,24 @@ msgstr "DNS через TLS (DoT)"
msgid "DNS Protocol Type"
msgstr "Тип протокола DNS"
msgid "DNS requests through"
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 Servers"
msgstr "DNS-серверы"
msgid "DNS through a section"
msgstr "DNS через секцию"
msgid "DNS Unavailability Timeout"
msgstr "Таймаут недоступности DNS"
msgid "Do not panic, everything can be fixed, just..."
msgstr "Не паникуйте, всё можно исправить, просто..."
@@ -677,6 +701,9 @@ msgstr "Скрывать значения"
msgid "Higher-level check interval"
msgstr "Интервал проверки высших уровней"
msgid "Higher-priority DNS Check"
msgstr "Проверка верхних DNS"
msgid "Host"
msgstr "Хост"
@@ -686,6 +713,12 @@ msgstr "Как часто проверять верхние уровни при
msgid "How often the currently selected server is checked"
msgstr "Как часто проверяется текущий выбранный сервер"
msgid "How often to check the active DNS servers."
msgstr "Как часто проверять активные DNS-серверы."
msgid "How often to check whether a higher-priority DNS server has recovered."
msgstr "Как часто проверять, восстановился ли DNS-сервер с более высоким приоритетом."
msgid "HTTP error"
msgstr "Ошибка HTTP"
@@ -1049,6 +1082,9 @@ msgstr "Совпадение по IP-адресам или подсетям на
msgid "Match destination ports. Use a single port or a range"
msgstr "Совпадение по портам назначения. Используйте одиночный порт или диапазон"
msgid "Maximum time to wait for example.com to resolve during a DNS health check."
msgstr "Максимальное время ожидания разрешения example.com при проверке доступности DNS."
msgid "Memory Usage"
msgstr "Использование памяти"
@@ -1526,8 +1562,8 @@ msgstr "Выберите сетевой интерфейс для VPN-подкл
msgid "Select or enter a User-Agent"
msgstr "Выберите или введите User-Agent"
msgid "Select or enter DNS server address"
msgstr "Выберите или введите адрес DNS-сервера"
msgid "Select or enter DNS server addresses. The first available server has priority."
msgstr "Выберите или введите адреса DNS-серверов. Первый доступный сервер имеет приоритет."
msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing"
msgstr "Выберите или введите путь к файлу кеша sing-box. Изменяйте это, ТОЛЬКО если вы знаете, что делаете"
@@ -1559,6 +1595,9 @@ msgstr "Выбранная секция"
msgid "Selector and URLTest outbounds must contain a non-empty outbounds array"
msgstr "Selector и URLTest outbound должны содержать непустой массив outbounds"
msgid "Send main DNS requests through the selected section. Bootstrap DNS remains direct."
msgstr "Отправлять запросы основного DNS через выбранную секцию. Bootstrap DNS остаётся прямым."
msgid "Server"
msgstr "Сервер"
@@ -1763,9 +1802,6 @@ msgstr "URL проверки"
msgid "The desync fwmark is managed by Podkop Plus for loop prevention and must not be overridden here."
msgstr "Desync fwmark управляется Podkop Plus для защиты от циклов и не должен переопределяться здесь."
msgid "The DNS server used to look up the IP address of an upstream DNS server"
msgstr "DNS-сервер, используемый для поиска IP-адреса вышестоящего DNS-сервера"
msgid "The NFQUEUE number is assigned by Podkop Plus for each rule and must not be overridden here."
msgstr "Номер NFQUEUE назначается Podkop Plus для каждого правила и не должен переопределяться здесь."
@@ -1997,6 +2033,9 @@ msgstr "Используйте CIDR-префиксы или IP-адреса, н
msgid "Use letters, digits, dots, underscores, or hyphens"
msgstr "Используйте буквы, цифры, точки, подчёркивания или дефисы"
msgid "Use sing-box duration format like 10s, 1m or 2m30s"
msgstr "Используйте формат длительности sing-box, например 10s, 1m или 2m30s"
msgid "Use sing-box duration format like 1d, 12h or 30m"
msgstr "Используйте формат длительности sing-box, например 1d, 12h или 30m"
File diff suppressed because it is too large Load Diff
+7 -2
View File
@@ -1,8 +1,13 @@
config settings 'settings'
option dns_type 'udp'
option dns_server '77.88.8.8'
option bootstrap_dns_server '77.88.8.8'
list dns_server '77.88.8.8'
list bootstrap_dns_server '77.88.8.8'
option dns_check_interval '10s'
option dns_recovery_check_interval '60s'
option dns_check_timeout '2s'
option dns_rewrite_ttl '60'
option dns_detour_enabled '0'
#option dns_detour_section 'main'
list source_network_interfaces 'br-lan'
option enable_output_network_interface '0'
#option output_network_interface 'wan'
+1
View File
@@ -130,6 +130,7 @@ function command_spec(command) {
start: [ "service/lifecycle.uc", "start", 0 ],
stop: [ "service/lifecycle.uc", "stop", 0 ],
reload: [ "service/lifecycle.uc", "reload", 1 ],
dns_failover_apply: [ "service/lifecycle.uc", "dns-failover-apply", 1 ],
restart: [ "service/lifecycle.uc", "restart", 0 ],
enable: [ "service/lifecycle.uc", "enable", 0 ],
disable: [ "service/lifecycle.uc", "disable", 0 ],
@@ -30,6 +30,7 @@ const RULE_CONDITION_CACHE_DIR = getenv("PODKOP_RULE_CONDITION_CACHE_DIR") || RU
const RELOAD_LOCK_DIR = getenv("PODKOP_RELOAD_LOCK_DIR") || "/var/run/podkop-plus.reload.lock";
const SERVICE_INIT = getenv("PODKOP_SERVICE_INIT") || "/etc/init.d/podkop-plus";
const PRIORITY_UC = getenv("PODKOP_PRIORITY_UC") || LIB_DIR + "/singbox/priority.uc";
const DNS_FAILOVER_UC = getenv("PODKOP_DNS_FAILOVER_UC") || LIB_DIR + "/singbox/dns_failover.uc";
const COMPONENT_JOB_DIR = getenv("UPDATES_JOB_DIR") || getenv("PODKOP_UI_COMPONENT_ACTION_DIR") || "/var/run/podkop-plus/component-actions";
const COMPONENT_JOB_FINISHED_TTL_MINUTES = getenv("UPDATES_JOB_FINISHED_TTL_MINUTES") || "60";
const COMPONENT_JOB_ORPHAN_OUTPUT_TTL_MINUTES = getenv("UPDATES_JOB_ORPHAN_OUTPUT_TTL_MINUTES") || "60";
@@ -2425,19 +2426,26 @@ function subscription_update_common_locked(force, target_section, target_source_
if (!singbox_runtime_success([ "configure-service" ]))
return false;
module_success([ DNS_FAILOVER_UC, "stop-runtime" ]);
if (!singbox_runtime_success([ "init-config", "0", "1", "1" ])) {
module_success([ DNS_FAILOVER_UC, "start-runtime" ]);
log_message("Failed to rebuild sing-box after subscription update", "error");
return false;
}
module_success([ PRIORITY_UC, "stop-runtime" ]);
if (!service_state_success([ "reload-sing-box-runtime" ])) {
module_success([ PRIORITY_UC, "start-runtime" ]);
module_success([ DNS_FAILOVER_UC, "start-runtime" ]);
return false;
}
if (!module_success([ PRIORITY_UC, "start-runtime" ])) {
log_message("Failed to restart Priority runtime after subscription update", "error");
return false;
}
if (!module_success([ DNS_FAILOVER_UC, "start-runtime" ])) {
log_message("Failed to restart DNS failover runtime after subscription update", "error");
return false;
}
if (!write_current_reload_state_clean())
return false;
+17
View File
@@ -1057,6 +1057,22 @@ function migrate_list_update_enabled(ctx) {
}
}
function normalize_existing_list_option(ctx, section, key) {
let current = object_or_empty(section)[key];
if (current == null || type(current) == "array")
return;
let value = trim(as_string(current));
let values = value == "" ? [] : [ value ];
section[key] = values;
record_operation(ctx, { op: "set_list", section: section_name(section), option: key, values });
}
function migrate_dns_server_lists(ctx) {
normalize_existing_list_option(ctx, ctx.model.settings, "dns_server");
normalize_existing_list_option(ctx, ctx.model.settings, "bootstrap_dns_server");
}
function migrate_download_via_proxy_flags(ctx) {
let settings = ctx.model.settings;
let legacy_section = option(settings, "download_lists_via_proxy_section", "");
@@ -1125,6 +1141,7 @@ function migrate_model(model, constants) {
constants = object_or_empty(constants);
delete_option(ctx, model.settings, "routing_excluded_ips");
migrate_dns_server_lists(ctx);
migrate_list_update_enabled(ctx);
let converted_sections = [];
+92
View File
@@ -9,6 +9,7 @@ let zapret2_validator_module = null;
let byedpi_validator_module = null;
let constants_module = null;
let core_url = require("core.url");
let core_ip = require("core.ip");
let rule_config = require("config.rule");
let connections = require("config.connections");
@@ -71,6 +72,13 @@ function whitespace_values(value) {
return result;
}
function option_list_values(section, key) {
let value = type(section) == "object" ? section[key] : null;
if (type(value) == "array")
return value;
return whitespace_values(value);
}
function list_has_value(values, needle) {
needle = as_string(needle);
if (needle == "")
@@ -881,6 +889,89 @@ function validate_required_duration_option(value, label) {
validate_duration_option(value, label);
}
function dns_server_value_valid(value) {
value = trim(as_string(value));
if (value == "" || match(value, /[ \t\r\n@]/) != null)
return false;
let host = core_url.host(value);
if (host == "")
return false;
let port = core_url.port(value);
if (port != "" && (match(port, /^[0-9]+$/) == null || int(port) < 1 || int(port) > 65535))
return false;
if (core_ip.valid_ip(host))
return true;
return length(host) <= 253 &&
index(host, ".") > 0 &&
substr(host, 0, 1) != "." &&
substr(host, length(host) - 1, 1) != "." &&
match(host, /[\/:\[\]]/) == null;
}
function dns_setting_values(settings, key) {
let values = [];
for (let value in option_list_values(settings, key)) {
value = trim(as_string(value));
if (value != "")
push(values, value);
}
return values;
}
function validate_dns_settings(settings, sections, context) {
let dns_type = option(settings, "dns_type", "udp");
if (!contains([ "udp", "dot", "doh" ], dns_type))
fail_validation("Unsupported DNS protocol type '" + dns_type + "'. Use udp, dot, or doh. Aborted.");
let main_servers = dns_setting_values(settings, "dns_server");
let bootstrap_servers = dns_setting_values(settings, "bootstrap_dns_server");
if (length(main_servers) == 0)
fail_validation("At least one main DNS server is required. Aborted.");
if (length(bootstrap_servers) == 0)
fail_validation("At least one Bootstrap DNS server is required. Aborted.");
for (let value in main_servers)
if (!dns_server_value_valid(value))
fail_validation("Invalid main DNS server '" + value + "'. Aborted.");
for (let value in bootstrap_servers)
if (!dns_server_value_valid(value))
fail_validation("Invalid Bootstrap DNS server '" + value + "'. Aborted.");
if (length(main_servers) > 1 || length(bootstrap_servers) > 1) {
validate_required_duration_option(option(settings, "dns_check_interval", "10s"), "settings.dns_check_interval");
validate_required_duration_option(option(settings, "dns_recovery_check_interval", "60s"), "settings.dns_recovery_check_interval");
validate_required_duration_option(option(settings, "dns_check_timeout", "2s"), "settings.dns_check_timeout");
}
if (!bool_option(settings, "dns_detour_enabled", false))
return;
let target_name = option(settings, "dns_detour_section", "");
if (target_name == "")
fail_validation("DNS through a section is enabled, but no section is selected. Aborted.");
let target = null;
for (let section in sections)
if (section_name(section) == target_name) {
target = {
section: target_name,
enabled: section_enabled(section),
action: rule_action(section)
};
break;
}
if (target == null)
fail_validation("DNS through a section references missing rule '" + target_name + "'. Aborted.");
if (!target.enabled)
fail_validation("DNS through a section references disabled rule '" + target_name + "'. Aborted.");
if (!download_section_action_available(target.action, context.byedpi_installed, context.zapret_installed, context.zapret2_installed))
fail_validation("DNS through a section references rule '" + target_name + "' with unsupported action '" + target.action + "'. Select an enabled Connection, VPN, Zapret, Zapret2, or ByeDPI section. Aborted.");
}
function normalize_port_number_value(value) {
value = trim(as_string(value));
if (value == "" || match(value, /^[0-9]+$/) == null)
@@ -1469,6 +1560,7 @@ function validate_runtime_config(context) {
let sections = sections_by_type("section");
validate_runtime_mark_ranges_context(context);
validate_dns_settings(settings, sections, context);
validate_list_update_settings(settings);
validate_http_url_option(option(settings, "latency_test_url", DEFAULT_LATENCY_TEST_URL) || DEFAULT_LATENCY_TEST_URL, "settings.latency_test_url");
+61 -14
View File
@@ -4,6 +4,7 @@ let fs = require("fs");
let constants = require("core.constants");
let core_ip = require("core.ip");
let uci_core = require("core.uci");
let runtime_dns = require("singbox.dns");
const CONFIG_NAME = getenv("PODKOP_CONFIG_NAME") || constants.PODKOP_CONFIG_NAME || "podkop-plus";
const LIB_DIR = getenv("PODKOP_LIB") || "/usr/lib/podkop-plus";
@@ -1269,7 +1270,7 @@ function url_path(value) {
return helper_output("url-get-path", [ value ]);
}
function dns_check_resolve_host(host, resolver) {
function dns_check_resolve_host(host, resolver, timeout_seconds) {
host = as_string(host);
resolver = as_string(resolver);
if (host == "")
@@ -1279,8 +1280,9 @@ function dns_check_resolve_host(host, resolver) {
if (resolver == "")
return "";
timeout_seconds = int(timeout_seconds || 2);
for (let line in split(command_output_from_args([
"dig", "@" + resolver, host, "A", "+short", "+timeout=2", "+tries=1"
"dig", "@" + resolver, host, "A", "+short", "+timeout=" + as_string(timeout_seconds), "+tries=1"
]), "\n")) {
line = trim(as_string(line));
if (valid_ipv4(line))
@@ -1389,22 +1391,50 @@ function dns_check_router_resolver_available(domain) {
return false;
}
function dns_check_timeout_seconds(value) {
let rest = as_string(value);
let milliseconds = 0.0;
let units = { ns: 0.000001, us: 0.001, ms: 1, s: 1000, m: 60000, h: 3600000, d: 86400000 };
while (rest != "") {
let matched = match(rest, /^([0-9]+(\.[0-9]+)?)(ns|us|ms|s|m|h|d)/);
if (!matched)
return 2;
milliseconds += (matched[1] * 1) * units[matched[3]];
rest = substr(rest, length(matched[0]));
}
return milliseconds > 0 ? int((milliseconds + 999) / 1000) : 2;
}
function check_dns_available() {
let cfg = settings();
let dns_type = option(cfg, "dns_type", "");
let dns_server = option(cfg, "dns_server", "");
let bootstrap_dns_server = option(cfg, "bootstrap_dns_server", "");
let active = runtime_dns.active_values(cfg);
let dns_server = active.main;
let bootstrap_dns_server = active.bootstrap;
let dont_touch_dhcp = bool_option(cfg, "dont_touch_dhcp", false) ? 1 : 0;
let domain = "google.com";
let domain = "example.com";
let timeout_seconds = dns_check_timeout_seconds(option(cfg, "dns_check_timeout", "2s"));
let dns_status = 0;
let dns_on_router = 0;
let bootstrap_dns_status = 0;
let dhcp_config_status = 1;
if (dns_type == "doh")
dns_status = dns_check_doh_server_available(dns_server, bootstrap_dns_server) ? 1 : 0;
else if (dns_type == "dot" || dns_type == "udp")
dns_status = dns_check_dig_server_available(dns_type, dns_server, bootstrap_dns_server, domain) ? 1 : 0;
let active_dns_args = [ "dig" ];
if (runtime_dns.failover_enabled(cfg)) {
push(active_dns_args, "-p");
push(active_dns_args, as_string(runtime_dns.health_port("active", 0)));
}
push(active_dns_args, "@" + SB_DNS_INBOUND_ADDRESS);
push(active_dns_args, domain);
push(active_dns_args, "A");
push(active_dns_args, "+short");
push(active_dns_args, "+timeout=" + as_string(timeout_seconds));
push(active_dns_args, "+tries=1");
for (let line in split(command_output_from_args(active_dns_args), "\n"))
if (valid_ipv4(trim(as_string(line)))) {
dns_status = 1;
break;
}
if (dns_check_router_resolver_available(domain))
dns_on_router = 1;
@@ -1413,11 +1443,24 @@ function check_dns_available() {
if (dns_server_host == "")
dns_server_host = dns_server;
if (bootstrap_dns_server != "") {
let bootstrap_check_domain = domain;
if (dns_server_host != "" && !valid_ipv4(dns_server_host))
bootstrap_check_domain = dns_server_host;
if (dns_check_resolve_host(bootstrap_check_domain, bootstrap_dns_server) != "")
bootstrap_dns_status = 1;
if (length(active.state.bootstrap_servers) > 1) {
for (let line in split(command_output_from_args([
"dig", "-p", as_string(runtime_dns.health_port("bootstrap", active.state.bootstrap_index)),
"@" + runtime_dns.DNS_HEALTH_ADDRESS, domain, "A", "+short",
"+timeout=" + as_string(timeout_seconds), "+tries=1"
]), "\n"))
if (valid_ipv4(trim(as_string(line)))) {
bootstrap_dns_status = 1;
break;
}
}
else {
let bootstrap_check_domain = domain;
if (dns_server_host != "" && !valid_ipv4(dns_server_host))
bootstrap_check_domain = dns_server_host;
if (dns_check_resolve_host(bootstrap_check_domain, bootstrap_dns_server, timeout_seconds) != "")
bootstrap_dns_status = 1;
}
}
if (!module_success(DNS_APPLY_UC, [ "default-config-complete" ]))
@@ -1427,9 +1470,13 @@ function check_dns_available() {
write_json({
dns_type,
dns_server: display_dns_server,
dns_server_index: active.state.main_index,
dns_server_count: length(active.state.main_servers),
dns_status,
dns_on_router,
bootstrap_dns_server,
bootstrap_dns_server_index: active.state.bootstrap_index,
bootstrap_dns_server_count: length(active.state.bootstrap_servers),
bootstrap_dns_status,
dhcp_config_status,
dont_touch_dhcp
+11 -2
View File
@@ -361,6 +361,8 @@ 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, "list dns_server");
line = mask_after_token_space(line, "list bootstrap_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");
@@ -387,6 +389,7 @@ 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, "list dns_server '");
line = mask_after_token(line, "option yacd_secret_key");
return line;
@@ -1414,13 +1417,19 @@ function render_global_dns_check(dont_touch_dhcp) {
let dns_type = object_value(value, "dns_type") || "unknown";
let dns_server = object_value(value, "dns_server") || "unknown";
let bootstrap_dns_server = object_value(value, "bootstrap_dns_server");
let dns_position = number_value(value.dns_server_count) > 1
? " (priority " + as_string(number_value(value.dns_server_index) + 1) + "/" + as_string(number_value(value.dns_server_count)) + ")"
: "";
let bootstrap_position = number_value(value.bootstrap_dns_server_count) > 1
? " (priority " + as_string(number_value(value.bootstrap_dns_server_index) + 1) + "/" + as_string(number_value(value.bootstrap_dns_server_count)) + ")"
: "";
let dump_dhcp_config = false;
if (bootstrap_dns_server != "") {
print_line((flag_is_one(value.bootstrap_dns_status) ? "\u2705 Bootstrap DNS: " : "\u274c Bootstrap DNS: ") + bootstrap_dns_server);
print_line((flag_is_one(value.bootstrap_dns_status) ? "\u2705 Bootstrap DNS: " : "\u274c Bootstrap DNS: ") + bootstrap_dns_server + bootstrap_position);
}
print_line((flag_is_one(value.dns_status) ? "\u2705 Main DNS: " : "\u274c Main DNS: ") + dns_server + " [" + dns_type + "]");
print_line((flag_is_one(value.dns_status) ? "\u2705 Main DNS: " : "\u274c Main DNS: ") + dns_server + " [" + dns_type + "]" + dns_position);
print_line(flag_is_one(value.dns_on_router) ? "\u2705 DNS on router" : "\u274c DNS on router");
if (as_string(dont_touch_dhcp) == "1") {
+92 -1
View File
@@ -40,6 +40,8 @@ const RUNTIME_CACHE_FORMAT_FILE = getenv("PODKOP_RUNTIME_CACHE_FORMAT_FILE") ||
const PERSISTENT_SUBSCRIPTION_CACHE_DIR = getenv("PODKOP_PERSISTENT_SUBSCRIPTION_CACHE_DIR") || "/etc/podkop-plus/subscription-cache";
const PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE = getenv("PODKOP_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE") || PERSISTENT_SUBSCRIPTION_CACHE_DIR + "/cache-format";
const SUBSCRIPTION_BOOTSTRAP_RETRY_PID_FILE = getenv("PODKOP_SUBSCRIPTION_BOOTSTRAP_RETRY_PID_FILE") || RUNTIME_STATE_DIR + "/subscription-bootstrap-retry.pid";
const DNS_FAILOVER_STATE_FILE = getenv("PODKOP_DNS_FAILOVER_STATE_FILE") || RUNTIME_STATE_DIR + "/dns-failover.json";
const DNS_FAILOVER_PID_FILE = getenv("PODKOP_DNS_FAILOVER_PID_FILE") || RUNTIME_STATE_DIR + "/dns-failover.pid";
const SUBSCRIPTION_UPDATE_LOCK_DIR = getenv("PODKOP_SUBSCRIPTION_UPDATE_LOCK_DIR") || RUNTIME_STATE_DIR + "/subscription-update.lock";
const RELOAD_LOCK_DIR = getenv("PODKOP_RELOAD_LOCK_DIR") || "/var/run/podkop-plus.reload.lock";
@@ -97,6 +99,7 @@ const SERVER_UC = LIB_DIR + "/server/service.uc";
const NFT_UC = LIB_DIR + "/nft/apply.uc";
const SINGBOX_UC = LIB_DIR + "/singbox/runtime.uc";
const PRIORITY_UC = LIB_DIR + "/singbox/priority.uc";
const DNS_FAILOVER_UC = LIB_DIR + "/singbox/dns_failover.uc";
const SUBSCRIPTION_CACHE_UC = LIB_DIR + "/subscription/cache.uc";
const UPDATES_UC = LIB_DIR + "/components/updates.uc";
const STATE_UC = LIB_DIR + "/service/state.uc";
@@ -272,6 +275,8 @@ function lifecycle_env() {
PODKOP_PERSISTENT_SUBSCRIPTION_CACHE_DIR: PERSISTENT_SUBSCRIPTION_CACHE_DIR,
PODKOP_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE: PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE,
PODKOP_SUBSCRIPTION_BOOTSTRAP_RETRY_PID_FILE: SUBSCRIPTION_BOOTSTRAP_RETRY_PID_FILE,
PODKOP_DNS_FAILOVER_STATE_FILE: DNS_FAILOVER_STATE_FILE,
PODKOP_DNS_FAILOVER_PID_FILE: DNS_FAILOVER_PID_FILE,
PODKOP_SUBSCRIPTION_UPDATE_LOCK_DIR: SUBSCRIPTION_UPDATE_LOCK_DIR,
PODKOP_PENDING_RELOAD_FILE: PENDING_RELOAD_FILE,
PODKOP_RELOAD_LOCK_DIR: RELOAD_LOCK_DIR,
@@ -757,6 +762,12 @@ function start_impl() {
if (status != 0)
return status;
status = module_status(DNS_FAILOVER_UC, [ "start-runtime" ]);
if (status != 0) {
log_message("Failed to start DNS failover runtime", "fatal");
return status;
}
module_background(DIAGNOSTICS_UC, [ "get-system-info" ]);
return 0;
}
@@ -765,6 +776,7 @@ function stop_main() {
let status = 0;
log_message("Stopping Podkop Plus", "info");
module_success(DNS_FAILOVER_UC, [ "stop-runtime" ]);
module_success(PRIORITY_UC, [ "stop-runtime" ]);
module_success(SUBSCRIPTION_CACHE_UC, [ "stop-deferred-bootstrap-worker" ]);
module_success(UPDATES_UC, [ "stop-list-update" ]);
@@ -926,12 +938,19 @@ function restart_runtime_for_reload() {
if (config_commit() != 0)
return 1;
return module_status(STATE_UC, [
status = module_status(STATE_UC, [
"write-current-reload-state-clean",
RELOAD_STATE_FILE,
as_string(RELOAD_STATE_FORMAT),
RULE_CONDITION_CACHE_DIR
]);
if (status != 0)
return status;
status = module_status(DNS_FAILOVER_UC, [ "start-runtime" ]);
if (status != 0)
log_message("Failed to start DNS failover runtime after runtime restart", "error");
return status;
}
function write_service_trigger_sync_state(changed) {
@@ -998,6 +1017,69 @@ function reload_actions_summary(plan) {
return actions;
}
function release_reload_lock() {
module_success(STATE_UC, [ "release-runtime-dir-lock", RELOAD_LOCK_DIR ]);
}
function wait_dns_failover_state(candidate_state_path, attempts) {
attempts = int(attempts || 1);
for (let i = 0; i < attempts; i++) {
let status = module_status(DNS_FAILOVER_UC, [ "verify-state", candidate_state_path ]);
if (status == 0)
return 0;
if (i + 1 < attempts)
command_success_from_args([ "sleep", "1" ]);
}
return 1;
}
function dns_failover_apply(candidate_state_path) {
candidate_state_path = as_string(candidate_state_path);
if (candidate_state_path == "" || fs.stat(candidate_state_path) == null)
return 1;
if (!module_success(STATE_UC, [ "acquire-runtime-dir-lock-wait", RELOAD_LOCK_DIR, owner_pid(), "2" ]))
return 2;
let patch_result = module_capture(SINGBOX_UC, [ "patch-dns-config", candidate_state_path ]);
if (patch_result.status != 0) {
release_reload_lock();
return patch_result.status;
}
let fields = split(trim(patch_result.output), "\t");
let changed = as_string(fields[0]) == "1";
let backup_path = length(fields) > 1 ? as_string(fields[1]) : "";
let status = 0;
if (changed) {
status = module_status(STATE_UC, [ "hup-sing-box-runtime" ]);
if (status == 0)
status = wait_dns_failover_state(candidate_state_path, 5);
if (status != 0) {
log_message("sing-box SIGHUP DNS reload did not become ready; trying service reload", "warn");
status = module_status(STATE_UC, [ "reload-sing-box-runtime" ]);
if (status == 0)
status = wait_dns_failover_state(candidate_state_path, 8);
}
}
if (status == 0 && !module_success(DNS_FAILOVER_UC, [ "commit-state", candidate_state_path ]))
status = 1;
if (status != 0 && backup_path != "") {
log_message("DNS failover apply failed; restoring the previous sing-box configuration", "error");
if (module_success(SINGBOX_UC, [ "restore-dns-config", backup_path ]))
module_success(STATE_UC, [ "reload-sing-box-runtime" ]);
}
if (backup_path != "")
remove_file(backup_path);
release_reload_lock();
return status;
}
function reload(reason) {
let status;
let force_runtime_reload = as_string(reason || "") == "on_config_change" ? 0 : 1;
@@ -1111,6 +1193,7 @@ function reload(reason) {
}
if (plan.needs_sing_box_reload == 1) {
module_success(DNS_FAILOVER_UC, [ "stop-runtime" ]);
module_success(PRIORITY_UC, [ "stop-runtime" ]);
status = module_status(SINGBOX_UC, [ "configure-service" ]);
if (status != 0)
@@ -1142,6 +1225,12 @@ function reload(reason) {
cleanup_failed_runtime();
return status;
}
status = module_status(DNS_FAILOVER_UC, [ "start-runtime" ]);
if (status != 0) {
log_message("Failed to restart DNS failover runtime after sing-box reload", "fatal");
cleanup_failed_runtime();
return status;
}
}
else if (plan.needs_nft_rebuild == 1 && nft_populate_enabled == 1) {
status = nft_populate_runtime_sets();
@@ -1307,6 +1396,8 @@ else if (mode == "stop")
status = stop();
else if (mode == "reload")
status = reload_tracked(ARGV[1] || "");
else if (mode == "dns-failover-apply")
status = dns_failover_apply(ARGV[1] || "");
else if (mode == "restart")
status = restart();
else if (mode == "enable")
+36 -2
View File
@@ -449,6 +449,16 @@ function pid_is_sing_box(pid) {
return path_basename(command_trimmed_output_from_args([ "readlink", "/proc/" + pid + "/exe" ])) == "sing-box";
}
function hup_sing_box_runtime() {
let pid = sing_box_service_pid_runtime();
if (pid <= 0 || !pid_is_sing_box(pid))
exit(1);
command_success_from_args([ "logger", "-t", "podkop-plus", "[info] Applying DNS failover with sing-box SIGHUP reload" ]);
if (!command_success_from_args([ "kill", "-HUP", as_string(pid) ]))
exit(1);
}
function process_age_seconds(pid) {
pid = as_string(pid);
if (match(pid, /^[0-9]+$/) == null)
@@ -549,6 +559,19 @@ function option(section, key, fallback) {
return as_string(value);
}
function list_option(section, key, fallback) {
let value = object_or_empty(section)[key];
if (type(value) == "array")
return value;
value = trim(as_string(value));
if (value != "")
return split(value, /[ \t\r\n]+/);
fallback = as_string(fallback);
return fallback == "" ? [] : [ fallback ];
}
function bool_option(section, key, fallback) {
return arg_bool(option(section, key, fallback ? "1" : "0"));
}
@@ -1221,8 +1244,17 @@ function sing_box_signature_body(settings, sections, servers, mwan3_active) {
let body = "";
body = signature_add_value(body, "settings.dns_type", option(settings, "dns_type", "doh"));
body = signature_add_value(body, "settings.dns_server", option(settings, "dns_server", "1.1.1.1"));
body = signature_add_value(body, "settings.bootstrap_dns_server", option(settings, "bootstrap_dns_server", "77.88.8.8"));
for (let value in list_option(settings, "dns_server", "77.88.8.8"))
body = signature_add_value(body, "settings.dns_server", value);
for (let value in list_option(settings, "bootstrap_dns_server", "77.88.8.8"))
body = signature_add_value(body, "settings.bootstrap_dns_server", value);
body = signature_add_value(body, "settings.dns_check_interval", option(settings, "dns_check_interval", "10s"));
body = signature_add_value(body, "settings.dns_recovery_check_interval", option(settings, "dns_recovery_check_interval", "60s"));
body = signature_add_value(body, "settings.dns_check_timeout", option(settings, "dns_check_timeout", "2s"));
let dns_detour_enabled = bool_option_value(settings, "dns_detour_enabled", false);
body = signature_add_value(body, "settings.dns_detour_enabled", dns_detour_enabled);
if (dns_detour_enabled == "1")
body = signature_add_value(body, "settings.dns_detour_section", option(settings, "dns_detour_section", ""));
body = signature_add_value(body, "settings.dns_rewrite_ttl", option(settings, "dns_rewrite_ttl", "60"));
body = signature_add_value(body, "settings.output_network_interface", option(settings, "output_network_interface", ""));
body = signature_add_value(body, "settings.disable_quic", bool_option_value(settings, "disable_quic", false));
@@ -1629,6 +1661,8 @@ else if (mode == "release-runtime-dir-lock")
release_runtime_dir_lock(ARGV[1]);
else if (mode == "reload-sing-box-runtime")
reload_sing_box_runtime();
else if (mode == "hup-sing-box-runtime")
hup_sing_box_runtime();
else if (mode == "clear-reload-state")
clear_reload_state(ARGV[1], ARGV[2]);
else if (mode == "remove-file")
+219 -5
View File
@@ -6,7 +6,95 @@ let runtime_constants = require("singbox.constants");
let runtime_url = require("core.url");
let as_string = common.as_string;
let bool_option = common.bool_option;
let list_option = common.list_option;
let object_or_empty = common.object_or_empty;
let option = common.option;
let read_json_file = common.read_json_file;
const DNS_FAILOVER_STATE_FILE = getenv("PODKOP_DNS_FAILOVER_STATE_FILE") || "/var/run/podkop-plus/dns-failover.json";
const DNS_HEALTH_ADDRESS = getenv("PODKOP_DNS_HEALTH_ADDRESS") || "127.0.0.42";
const DNS_HEALTH_PORT_BASE = int(getenv("PODKOP_DNS_HEALTH_PORT_BASE") || "10053");
function server_list(settings, key, fallback) {
let result = [];
for (let value in list_option(settings, key)) {
value = trim(as_string(value));
if (value != "")
push(result, value);
}
if (length(result) == 0)
push(result, fallback);
return result;
}
function arrays_equal(left, right) {
if (length(left || []) != length(right || []))
return false;
for (let i = 0; i < length(left); i++)
if (as_string(left[i]) != as_string(right[i]))
return false;
return true;
}
function detour_tag(settings) {
if (!bool_option(settings, "dns_detour_enabled", false))
return "";
let section_name = option(settings, "dns_detour_section", "");
return section_name == "" ? "" : runtime_constants.outbound_tag(section_name);
}
function state_template(settings) {
return {
version: 1,
dns_type: option(settings, "dns_type", "udp"),
dns_detour: detour_tag(settings),
main_servers: server_list(settings, "dns_server", "77.88.8.8"),
bootstrap_servers: server_list(settings, "bootstrap_dns_server", "77.88.8.8"),
main_index: 0,
bootstrap_index: 0
};
}
function state_matches(template, state) {
state = object_or_empty(state);
return int(state.version || 0) == 1 &&
as_string(state.dns_type) == template.dns_type &&
as_string(state.dns_detour) == template.dns_detour &&
arrays_equal(state.main_servers, template.main_servers) &&
arrays_equal(state.bootstrap_servers, template.bootstrap_servers);
}
function bounded_index(value, values) {
let index_value = int(value || 0);
return index_value >= 0 && index_value < length(values) ? index_value : 0;
}
function normalize_state(settings, state) {
let result = state_template(settings);
if (!state_matches(result, state))
return result;
result.main_index = bounded_index(object_or_empty(state).main_index, result.main_servers);
result.bootstrap_index = bounded_index(object_or_empty(state).bootstrap_index, result.bootstrap_servers);
return result;
}
function runtime_state(settings, override_state) {
let state = override_state;
if (state == null)
state = read_json_file(DNS_FAILOVER_STATE_FILE);
return normalize_state(settings, state);
}
function active_values(settings, override_state) {
let state = runtime_state(settings, override_state);
return {
state,
main: state.main_servers[state.main_index],
bootstrap: state.bootstrap_servers[state.bootstrap_index]
};
}
function server_from_options(tag_name, dns_type, dns_server, detour) {
let server = runtime_url.host(dns_server);
@@ -45,16 +133,142 @@ function server_from_options(tag_name, dns_type, dns_server, detour) {
return result;
}
function server_config(settings) {
function bootstrap_server(tag_name, value) {
let server = runtime_url.host(value);
let port = runtime_url.port(value);
return {
type: "udp",
tag: tag_name,
server: server != "" ? server : value,
server_port: port != "" ? int(port, 10) : 53
};
}
function server_config(settings, override_state) {
let active = active_values(settings, override_state);
return server_from_options(
runtime_constants.DNS_SERVER_TAG,
option(settings, "dns_type", "doh"),
option(settings, "dns_server", "1.1.1.1"),
""
active.state.dns_type,
active.main,
active.state.dns_detour
);
}
function bootstrap_config(settings, override_state) {
let active = active_values(settings, override_state);
return bootstrap_server(runtime_constants.BOOTSTRAP_DNS_SERVER_TAG, active.bootstrap);
}
function failover_enabled(settings) {
let state = state_template(settings);
return length(state.main_servers) > 1 || length(state.bootstrap_servers) > 1;
}
function health_tag(kind, index_value, suffix) {
return "dns-health-" + as_string(kind) + "-" + as_string(index_value + 1) + "-" + as_string(suffix);
}
function health_port(kind, index_value) {
if (kind == "active")
return DNS_HEALTH_PORT_BASE + 2000;
return DNS_HEALTH_PORT_BASE + int(index_value) * 2 + (kind == "bootstrap" ? 1 : 0);
}
function add_active_health_inbound(result) {
let inbound_tag = "dns-health-active-main-in";
push(result.inbounds, {
type: "direct",
tag: inbound_tag,
listen: DNS_HEALTH_ADDRESS,
listen_port: health_port("active", 0)
});
push(result.rules, {
action: "route",
inbound: inbound_tag,
server: runtime_constants.DNS_SERVER_TAG,
disable_cache: true
});
push(result.sniff_inbounds, inbound_tag);
}
function add_health_candidate(result, kind, index_value, server) {
let server_tag = health_tag(kind, index_value, "server");
let inbound_tag = health_tag(kind, index_value, "in");
let dns_server = kind == "main"
? server_from_options(server_tag, result.state.dns_type, server, result.state.dns_detour)
: bootstrap_server(server_tag, server);
if (dns_server.unsupported) {
result.unsupported = dns_server.unsupported;
return;
}
push(result.servers, dns_server);
push(result.inbounds, {
type: "direct",
tag: inbound_tag,
listen: DNS_HEALTH_ADDRESS,
listen_port: health_port(kind, index_value)
});
push(result.rules, {
action: "route",
inbound: inbound_tag,
server: server_tag,
disable_cache: true
});
push(result.sniff_inbounds, inbound_tag);
}
function config(settings, override_state) {
let state = runtime_state(settings, override_state);
let main = server_config(settings, state);
if (main.unsupported)
return { unsupported: main.unsupported };
let result = {
state,
servers: [ bootstrap_config(settings, state), main ],
inbounds: [],
rules: [],
sniff_inbounds: []
};
if (length(state.main_servers) > 1 || length(state.bootstrap_servers) > 1)
add_active_health_inbound(result);
if (length(state.main_servers) > 1)
for (let i = 0; i < length(state.main_servers); i++)
add_health_candidate(result, "main", i, state.main_servers[i]);
if (length(state.bootstrap_servers) > 1)
for (let i = 0; i < length(state.bootstrap_servers); i++)
add_health_candidate(result, "bootstrap", i, state.bootstrap_servers[i]);
return result;
}
function default_domain_resolver(settings) {
return bool_option(settings, "dns_detour_enabled", false)
? runtime_constants.BOOTSTRAP_DNS_SERVER_TAG
: runtime_constants.DNS_SERVER_TAG;
}
return {
DNS_FAILOVER_STATE_FILE,
DNS_HEALTH_ADDRESS,
active_values,
arrays_equal,
bootstrap_config,
config,
default_domain_resolver,
detour_tag,
failover_enabled,
health_port,
normalize_state,
runtime_state,
server_config,
server_from_options,
server_config
server_list,
state_matches,
state_template
};
@@ -0,0 +1,379 @@
#!/usr/bin/env ucode
let fs = require("fs");
let uci_core = require("core.uci");
let common = require("core.common");
let core_ip = require("core.ip");
let runtime_dns = require("singbox.dns");
const CONFIG_NAME = getenv("PODKOP_CONFIG_NAME") || "podkop-plus";
const LIB_DIR = getenv("PODKOP_LIB") || "/usr/lib/podkop-plus";
const RUNTIME_STATE_DIR = getenv("PODKOP_RUNTIME_STATE_DIR") || "/var/run/podkop-plus";
const STATE_FILE = getenv("PODKOP_DNS_FAILOVER_STATE_FILE") || RUNTIME_STATE_DIR + "/dns-failover.json";
const PID_FILE = getenv("PODKOP_DNS_FAILOVER_PID_FILE") || RUNTIME_STATE_DIR + "/dns-failover.pid";
const DNS_FAILOVER_UC = getenv("PODKOP_DNS_FAILOVER_UC") || LIB_DIR + "/singbox/dns_failover.uc";
const SERVICE_BIN = getenv("PODKOP_BIN") || "/usr/bin/podkop-plus";
const CHECK_DOMAIN = "example.com";
function as_string(value) {
return value == null ? "" : "" + value;
}
function shell_quote(value) {
return "'" + replace(as_string(value), /'/g, "'\\''") + "'";
}
function command_from_args(args) {
let result = [];
for (let arg in args)
push(result, shell_quote(arg));
return join(" ", result);
}
function command_status(command) {
let status = int(system(command));
return status > 255 ? int(status / 256) : status;
}
function command_success_from_args(args) {
return command_status(command_from_args(args) + " >/dev/null 2>&1") == 0;
}
function command_output_from_args(args) {
let pipe = fs.popen(command_from_args(args), "r");
if (!pipe)
return "";
let data = pipe.read("all");
let status = pipe.close();
return status == 0 && data != null ? as_string(data) : "";
}
function settings() {
return common.object_or_empty(uci_core.get_all(CONFIG_NAME, "settings"));
}
function ensure_dir(path) {
return command_success_from_args([ "mkdir", "-p", path ]);
}
function remove_file(path) {
try {
fs.unlink(as_string(path));
}
catch (e) {
}
}
function write_state(path, value) {
if (!ensure_dir(RUNTIME_STATE_DIR))
return false;
let stamp = clock();
let temporary = as_string(path) + ".tmp." + as_string(stamp[0]) + "." + as_string(stamp[1]);
if (!common.write_json_file(temporary, value))
return false;
if (!fs.rename(temporary, path)) {
remove_file(temporary);
return false;
}
return true;
}
function log_message(message, level) {
command_success_from_args([ "logger", "-t", "podkop-plus", "[" + as_string(level || "info") + "] DNS failover: " + as_string(message) ]);
}
function duration_milliseconds(value, fallback) {
let rest = as_string(value);
let total = 0.0;
let units = { ns: 0.000001, us: 0.001, ms: 1, s: 1000, m: 60000, h: 3600000, d: 86400000 };
while (rest != "") {
let matched = match(rest, /^([0-9]+(\.[0-9]+)?)(ns|us|ms|s|m|h|d)/);
if (!matched)
return fallback;
total += (matched[1] * 1) * units[matched[3]];
rest = substr(rest, length(matched[0]));
}
return total > 0 ? int(total + 0.5) : fallback;
}
function duration_seconds(value, fallback) {
let milliseconds = duration_milliseconds(value, fallback * 1000);
return int((milliseconds + 999) / 1000);
}
function probe_timeout(settings_value) {
return duration_seconds(settings_value, 2);
}
function probe_port(kind, index_value, timeout_seconds) {
let args = [
"dig", "-p", as_string(runtime_dns.health_port(kind, index_value)),
"@" + runtime_dns.DNS_HEALTH_ADDRESS,
CHECK_DOMAIN, "A", "+short",
"+timeout=" + as_string(timeout_seconds), "+tries=1"
];
for (let line in split(command_output_from_args(args), "\n"))
if (core_ip.valid_ipv4(trim(as_string(line))))
return true;
return false;
}
function probe_canonical_main(timeout_seconds) {
let args = [
"dig", "-p", as_string(runtime_dns.health_port("active", 0)),
"@" + runtime_dns.DNS_HEALTH_ADDRESS, CHECK_DOMAIN, "A", "+short",
"+timeout=" + as_string(timeout_seconds), "+tries=1"
];
for (let line in split(command_output_from_args(args), "\n"))
if (core_ip.valid_ipv4(trim(as_string(line))))
return true;
return false;
}
function verification_plan(previous, candidate) {
previous = common.object_or_empty(previous);
candidate = common.object_or_empty(candidate);
return {
main: int(candidate.main_index || 0) != int(previous.main_index || 0),
bootstrap: int(candidate.bootstrap_index || 0) != int(previous.bootstrap_index || 0) &&
length(candidate.bootstrap_servers || []) > 1
};
}
function verify_state(path) {
let cfg = settings();
let state = common.read_json_file(path);
let expected = runtime_dns.state_template(cfg);
if (!runtime_dns.state_matches(expected, state))
return false;
state = runtime_dns.normalize_state(cfg, state);
let previous = runtime_dns.normalize_state(cfg, common.read_json_file(STATE_FILE));
let plan = verification_plan(previous, state);
let timeout_seconds = probe_timeout(common.option(cfg, "dns_check_timeout", "2s"));
if (plan.main && !probe_canonical_main(timeout_seconds))
return false;
if (plan.bootstrap && !probe_port("bootstrap", state.bootstrap_index, timeout_seconds))
return false;
return true;
}
function commit_state(path) {
let cfg = settings();
let state = common.read_json_file(path);
if (!runtime_dns.state_matches(runtime_dns.state_template(cfg), state))
return false;
return fs.rename(path, STATE_FILE);
}
function now_seconds() {
return int(clock()[0]);
}
function choose_index(kind, state, current_index, timeout_seconds, recovery) {
let values = kind == "main" ? state.main_servers : state.bootstrap_servers;
if (length(values) <= 1)
return { index: 0, reason: "single", alive: true };
if (recovery && current_index > 0) {
for (let i = 0; i < current_index; i++)
if (probe_port(kind, i, timeout_seconds))
return { index: i, reason: "recovery", alive: true };
return { index: current_index, reason: "unchanged", alive: true };
}
if (probe_port(kind, current_index, timeout_seconds))
return { index: current_index, reason: "alive", alive: true };
for (let i = 0; i < length(values); i++) {
if (i == current_index)
continue;
if (probe_port(kind, i, timeout_seconds))
return { index: i, reason: i < current_index ? "recovery" : "active_dead", alive: true };
}
return { index: current_index, reason: "all_down", alive: false };
}
function clone_state(value) {
return json(sprintf("%J", value));
}
function apply_selections(state, selections) {
let candidate = clone_state(state);
let changed = [];
for (let kind in [ "bootstrap", "main" ]) {
let selected = common.object_or_empty(common.object_or_empty(selections)[kind]);
if (selected.index == null)
continue;
let key = kind == "main" ? "main_index" : "bootstrap_index";
if (int(candidate[key]) == int(selected.index))
continue;
candidate[key] = int(selected.index);
push(changed, { kind, selected });
}
if (length(changed) == 0)
return true;
let stamp = clock();
let candidate_path = STATE_FILE + ".candidate." + as_string(stamp[0]) + "." + as_string(stamp[1]);
if (!write_state(candidate_path, candidate))
return false;
let status = command_status(command_from_args([ SERVICE_BIN, "dns_failover_apply", candidate_path ]) + " >/dev/null 2>&1");
remove_file(candidate_path);
if (status != 0)
return false;
state.main_index = candidate.main_index;
state.bootstrap_index = candidate.bootstrap_index;
for (let item in changed) {
let values = item.kind == "main" ? state.main_servers : state.bootstrap_servers;
let selected = item.selected;
log_message("switched " + item.kind + " DNS to priority " + as_string(selected.index + 1) + " (" + values[selected.index] + ", " + selected.reason + ")", "info");
}
return true;
}
function worker() {
let cfg = settings();
let state = runtime_dns.normalize_state(cfg, common.read_json_file(STATE_FILE));
if (length(state.main_servers) <= 1 && length(state.bootstrap_servers) <= 1)
return 0;
if (!write_state(STATE_FILE, state))
return 1;
let active_interval = duration_seconds(common.option(cfg, "dns_check_interval", "10s"), 10);
let recovery_interval = duration_seconds(common.option(cfg, "dns_recovery_check_interval", "60s"), 60);
let timeout_seconds = probe_timeout(common.option(cfg, "dns_check_timeout", "2s"));
let next_active = now_seconds();
let next_recovery = now_seconds() + recovery_interval;
let all_down = { main: false, bootstrap: false };
while (true) {
let now = now_seconds();
let current_cfg = settings();
if (!runtime_dns.state_matches(runtime_dns.state_template(current_cfg), state))
return 0;
if (now >= next_active) {
let bootstrap = choose_index("bootstrap", state, int(state.bootstrap_index), timeout_seconds, false);
if (!bootstrap.alive && !all_down.bootstrap)
log_message("all configured bootstrap DNS servers are unavailable", "warn");
all_down.bootstrap = !bootstrap.alive;
if (bootstrap.index != int(state.bootstrap_index)) {
let main = choose_index("main", state, int(state.main_index), timeout_seconds, false);
let selections = { bootstrap };
if (main.alive)
selections.main = main;
let applied = apply_selections(state, selections);
next_active = now_seconds() + (applied && !main.alive ? 1 : active_interval);
}
else {
let main = choose_index("main", state, int(state.main_index), timeout_seconds, false);
if (!main.alive && !all_down.main)
log_message("all configured main DNS servers are unavailable", "warn");
all_down.main = !main.alive;
apply_selections(state, { main });
next_active = now_seconds() + active_interval;
}
}
if (now >= next_recovery) {
let bootstrap = choose_index("bootstrap", state, int(state.bootstrap_index), timeout_seconds, true);
let main = choose_index("main", state, int(state.main_index), timeout_seconds, true);
let has_changes = bootstrap.index != int(state.bootstrap_index) || main.index != int(state.main_index);
let applied = apply_selections(state, { bootstrap, main });
let completed = now_seconds();
if (has_changes && applied)
next_active = completed + active_interval;
next_recovery = completed + recovery_interval;
}
command_success_from_args([ "sleep", "1" ]);
}
}
function file_first_line(path) {
let data = fs.readfile(path);
if (data == null)
return "";
return trim(split(as_string(data), "\n")[0]);
}
function process_running(pid) {
return match(as_string(pid), /^[0-9]+$/) != null && command_success_from_args([ "kill", "-0", pid ]);
}
function stop_runtime() {
let pid = file_first_line(PID_FILE);
if (process_running(pid))
command_success_from_args([ "kill", pid ]);
remove_file(PID_FILE);
return 0;
}
function start_runtime() {
let cfg = settings();
stop_runtime();
if (!runtime_dns.failover_enabled(cfg)) {
remove_file(STATE_FILE);
return 0;
}
if (!ensure_dir(RUNTIME_STATE_DIR))
return 1;
let command = command_from_args([ "ucode", "-L", LIB_DIR, DNS_FAILOVER_UC, "worker" ]) +
" >/dev/null 2>&1 1000>&- & echo $! >" + shell_quote(PID_FILE);
return command_status(command);
}
function select_fixture(state_path, alive_path, kind, recovery) {
let state = common.object_or_empty(common.read_json_file(state_path));
let alive = common.object_or_empty(common.read_json_file(alive_path));
let key = kind == "main" ? "main_index" : "bootstrap_index";
let values = kind == "main" ? state.main_servers : state.bootstrap_servers;
let current = int(state[key] || 0);
let selected = { index: current, reason: "all_down", alive: false };
let probe = function(index_value) { return alive[as_string(index_value)] === true || alive[as_string(index_value)] == 1; };
if (as_string(recovery) == "1" && current > 0) {
for (let i = 0; i < current; i++)
if (probe(i)) {
selected = { index: i, reason: "recovery", alive: true };
break;
}
}
else if (probe(current)) {
selected = { index: current, reason: "alive", alive: true };
}
else {
for (let i = 0; i < length(values); i++)
if (i != current && probe(i)) {
selected = { index: i, reason: i < current ? "recovery" : "active_dead", alive: true };
break;
}
}
common.write_json(selected);
}
let mode = ARGV[0] || "";
if (mode == "start-runtime")
exit(start_runtime());
else if (mode == "stop-runtime")
exit(stop_runtime());
else if (mode == "worker")
exit(worker());
else if (mode == "verify-state")
exit(verify_state(ARGV[1]) ? 0 : 1);
else if (mode == "commit-state")
exit(commit_state(ARGV[1]) ? 0 : 1);
else if (mode == "select-fixture")
select_fixture(ARGV[1], ARGV[2], ARGV[3], ARGV[4]);
else if (mode == "verification-plan-fixture")
common.write_json(verification_plan(common.read_json_file(ARGV[1]), common.read_json_file(ARGV[2])));
else {
warn("Usage: singbox/dns_failover.uc <start-runtime|stop-runtime|worker|verify-state|commit-state|select-fixture|verification-plan-fixture> ...\n");
exit(1);
}
+43 -29
View File
@@ -412,12 +412,48 @@ function tproxy_inbound_matcher() {
function base_config(settings, service_address, runtime_context) {
let log_level = option(settings, "log_level", "warn");
let bootstrap_dns_server = option(settings, "bootstrap_dns_server", "77.88.8.8");
let rewrite_ttl = int_option(settings, "dns_rewrite_ttl", "60");
let cache_path = option(settings, "cache_path", "/tmp/sing-box/cache.db");
let dns_server = runtime_dns.server_config(settings);
if (dns_server.unsupported)
runtime_generate_unsupported(dns_server.unsupported);
let dns_config = runtime_dns.config(settings);
if (dns_config.unsupported)
runtime_generate_unsupported(dns_config.unsupported);
let dns_rules = [];
for (let rule in dns_config.rules)
push(dns_rules, rule);
for (let rule in [
{ action: "reject", query_type: "HTTPS" },
{ action: "reject", domain_suffix: "use-application-dns.net" },
{
action: "route",
server: runtime_constants.FAKEIP_DNS_SERVER_TAG,
rewrite_ttl,
domain: [ runtime_constants.FAKEIP_TEST_DOMAIN, runtime_constants.CHECK_PROXY_IP_DOMAIN ]
}
])
push(dns_rules, rule);
let dns_servers = [];
for (let server in dns_config.servers)
push(dns_servers, server);
push(dns_servers, {
type: "fakeip",
tag: runtime_constants.FAKEIP_DNS_SERVER_TAG,
inet4_range: runtime_constants.FAKEIP_INET4_RANGE,
inet6_range: runtime_constants.FAKEIP_INET6_RANGE
});
let inbounds = [
{ type: "tproxy", tag: runtime_constants.TPROXY_INBOUND_TAG, listen: runtime_constants.TPROXY_INBOUND_ADDRESS, listen_port: runtime_constants.TPROXY_INBOUND_PORT, tcp_fast_open: true, udp_fragment: true },
{ type: "tproxy", tag: runtime_constants.TPROXY_INBOUND6_TAG, listen: runtime_constants.TPROXY_INBOUND6_ADDRESS, listen_port: runtime_constants.TPROXY_INBOUND_PORT, tcp_fast_open: true, udp_fragment: true },
{ type: "direct", tag: runtime_constants.DNS_INBOUND_TAG, listen: runtime_constants.DNS_INBOUND_ADDRESS, listen_port: runtime_constants.DNS_INBOUND_PORT }
];
for (let inbound in dns_config.inbounds)
push(inbounds, inbound);
runtime_context = object_or_empty(runtime_context);
runtime_context.dns_health_inbounds = dns_config.sniff_inbounds;
runtime_context.default_domain_resolver = runtime_dns.default_domain_resolver(settings);
return {
log: {
@@ -426,26 +462,8 @@ function base_config(settings, service_address, runtime_context) {
timestamp: false
},
dns: {
servers: [
{ type: "udp", tag: runtime_constants.BOOTSTRAP_DNS_SERVER_TAG, server: bootstrap_dns_server, server_port: 53 },
dns_server,
{
type: "fakeip",
tag: runtime_constants.FAKEIP_DNS_SERVER_TAG,
inet4_range: runtime_constants.FAKEIP_INET4_RANGE,
inet6_range: runtime_constants.FAKEIP_INET6_RANGE
}
],
rules: [
{ action: "reject", query_type: "HTTPS" },
{ action: "reject", domain_suffix: "use-application-dns.net" },
{
action: "route",
server: runtime_constants.FAKEIP_DNS_SERVER_TAG,
rewrite_ttl,
domain: [ runtime_constants.FAKEIP_TEST_DOMAIN, runtime_constants.CHECK_PROXY_IP_DOMAIN ]
}
],
servers: dns_servers,
rules: dns_rules,
final: runtime_constants.DNS_SERVER_TAG,
strategy: "prefer_ipv4",
independent_cache: true
@@ -453,11 +471,7 @@ function base_config(settings, service_address, runtime_context) {
ntp: {},
certificate: {},
endpoints: [],
inbounds: [
{ type: "tproxy", tag: runtime_constants.TPROXY_INBOUND_TAG, listen: runtime_constants.TPROXY_INBOUND_ADDRESS, listen_port: runtime_constants.TPROXY_INBOUND_PORT, tcp_fast_open: true, udp_fragment: true },
{ type: "tproxy", tag: runtime_constants.TPROXY_INBOUND6_TAG, listen: runtime_constants.TPROXY_INBOUND6_ADDRESS, listen_port: runtime_constants.TPROXY_INBOUND_PORT, tcp_fast_open: true, udp_fragment: true },
{ type: "direct", tag: runtime_constants.DNS_INBOUND_TAG, listen: runtime_constants.DNS_INBOUND_ADDRESS, listen_port: runtime_constants.DNS_INBOUND_PORT }
],
inbounds,
outbounds: [
{ type: "direct", tag: runtime_constants.DIRECT_OUTBOUND_TAG },
{ type: "direct", tag: runtime_constants.BYPASS_OUTBOUND_TAG }
+12 -2
View File
@@ -7,6 +7,10 @@ let runtime_constants = require("singbox.constants");
let option = common.option;
let bool_option = common.bool_option;
function as_string(value) {
return value == null ? "" : "" + value;
}
function bool_value(value) {
return value === true || value == "1" || value == "true" || value == "yes" || value == "on";
}
@@ -14,9 +18,13 @@ function bool_value(value) {
function config(settings, runtime) {
let output_network_interface = option(settings, "output_network_interface", "");
let mwan3_active = type(runtime) == "object" && bool_value(runtime.mwan3_active);
let sniff_inbounds = [ runtime_constants.TPROXY_INBOUND_TAG, runtime_constants.DNS_INBOUND_TAG ];
if (type(runtime) == "object" && type(runtime.dns_health_inbounds) == "array")
for (let inbound in runtime.dns_health_inbounds)
push(sniff_inbounds, inbound);
let result = {
rules: [
{ action: "sniff", inbound: [ runtime_constants.TPROXY_INBOUND_TAG, runtime_constants.DNS_INBOUND_TAG ] },
{ action: "sniff", inbound: sniff_inbounds },
{ action: "hijack-dns", port: 53 },
{ action: "hijack-dns", protocol: "dns" },
{ action: "reject", ip_version: 6 }
@@ -24,7 +32,9 @@ function config(settings, runtime) {
rule_set: [],
final: runtime_constants.DIRECT_OUTBOUND_TAG,
auto_detect_interface: output_network_interface == "" && !mwan3_active,
default_domain_resolver: runtime_constants.DNS_SERVER_TAG,
default_domain_resolver: type(runtime) == "object" && as_string(runtime.default_domain_resolver) != ""
? as_string(runtime.default_domain_resolver)
: runtime_constants.DNS_SERVER_TAG,
default_mark: runtime_constants.OUTBOUND_MARK
};
+77
View File
@@ -2,6 +2,8 @@
let fs = require("fs");
let uci_core = require("core.uci");
let common = require("core.common");
let runtime_dns = require("singbox.dns");
const CONFIG_NAME = getenv("PODKOP_CONFIG_NAME") || "podkop-plus";
const LIB_DIR = getenv("PODKOP_LIB") || "/usr/lib/podkop-plus";
@@ -665,6 +667,77 @@ function save_config_file(temp_file_path, config_path) {
return true;
}
function replace_dns_server(config, replacement) {
let servers = array_or_empty(object_or_empty(config.dns).servers);
for (let i = 0; i < length(servers); i++) {
if (as_string(object_or_empty(servers[i]).tag) == as_string(replacement.tag)) {
servers[i] = replacement;
return true;
}
}
return false;
}
function patch_dns_config(state_path) {
let settings = uci_settings();
let candidate_state = common.read_json_file(state_path);
let expected = runtime_dns.state_template(settings);
if (!runtime_dns.state_matches(expected, candidate_state)) {
log_message("DNS failover state does not match the current UCI configuration", "warn");
exit(2);
}
let config_path = option(settings, "config_path", "");
let config = common.read_json_file(config_path);
if (config_path == "" || type(config) != "object") {
log_message("Cannot read the current sing-box configuration for DNS failover", "error");
exit(1);
}
let main = runtime_dns.server_config(settings, candidate_state);
let bootstrap = runtime_dns.bootstrap_config(settings, candidate_state);
if (main.unsupported || !replace_dns_server(config, main) || !replace_dns_server(config, bootstrap)) {
log_message("Cannot locate or build canonical DNS servers for failover", "error");
exit(1);
}
let backup_path = temp_path();
let temp_config = temp_path();
if (backup_path == "" || temp_config == "" ||
fs.writefile(backup_path, fs.readfile(config_path)) == null ||
!common.write_json_file(temp_config, config)) {
remove_files([ backup_path, temp_config ]);
exit(1);
}
if (!command_success_from_args([ "sing-box", "-c", temp_config, "check" ])) {
log_message("DNS failover produced an invalid sing-box configuration", "error");
remove_files([ backup_path, temp_config ]);
exit(1);
}
let changed = md5_file(config_path) != md5_file(temp_config);
if (!save_config_file(temp_config, config_path)) {
remove_file(backup_path);
exit(1);
}
if (!changed) {
remove_file(backup_path);
print("0\n");
return;
}
print("1\t", backup_path, "\n");
}
function restore_dns_config(backup_path) {
let config_path = option(uci_settings(), "config_path", "");
if (config_path == "" || !file_exists(backup_path))
return false;
return command_success_from_args([ "mv", "-f", backup_path, config_path ]);
}
function init_config(populate_nft, caches_prepared, no_refresh) {
let settings = uci_settings();
let config_path = option(settings, "config_path", "");
@@ -753,6 +826,10 @@ else if (mode == "init-config")
init_config(arg_bool(ARGV[1] || "1"), arg_bool(ARGV[2] || "0"), arg_bool(ARGV[3] || "0"));
else if (mode == "save-config-file-fixture")
exit(save_config_file(ARGV[1] || "", ARGV[2] || "") ? 0 : 1);
else if (mode == "patch-dns-config")
patch_dns_config(ARGV[1] || "");
else if (mode == "restore-dns-config")
exit(restore_dns_config(ARGV[1] || "") ? 0 : 1);
else if (mode == "managed-service-installed")
exit(managed_service_installed() ? 0 : 1);
else if (mode == "remove-managed-service-script")
@@ -33,6 +33,8 @@ const fixture = {
download_lists_via_proxy: '1',
download_subscriptions_via_proxy: '1',
download_lists_via_proxy_section: 'legacy-urltest',
dns_server: '9.9.9.9',
bootstrap_dns_server: '1.1.1.1',
routing_excluded_ips: [ '192.0.2.0/24' ]
},
rule: [
@@ -142,6 +144,9 @@ const childByType = type => config[type] || [];
const subscriptionUrls = childByType('subscription_url');
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');
function assert(condition, message) {
if (!condition) {
console.error(message);
@@ -23,7 +23,17 @@ JSON
context="$(cat "$WORK_DIR/context.json")"
validate_fixture() {
PODKOP_LIB="$PODKOP_LIB" ucode -L "$PODKOP_LIB" "$VALIDATOR" validate-runtime-fixture "$1" "$context"
local source="$1"
local normalized="$WORK_DIR/normalized-$(basename "$source")"
node - "$source" "$normalized" <<'JS'
const fs = require('fs');
const input = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
input.settings ??= { '.name': 'settings', '.type': 'settings' };
if (input.settings.dns_server === undefined) input.settings.dns_server = ['77.88.8.8'];
if (input.settings.bootstrap_dns_server === undefined) input.settings.bootstrap_dns_server = ['77.88.8.8'];
fs.writeFileSync(process.argv[3], JSON.stringify(input));
JS
PODKOP_LIB="$PODKOP_LIB" ucode -L "$PODKOP_LIB" "$VALIDATOR" validate-runtime-fixture "$normalized" "$context"
}
assert_rejects() {
@@ -48,6 +58,14 @@ cat >"$WORK_DIR/valid.json" <<'JSON'
"list_update_enabled": "1",
"update_interval": "1d",
"latency_test_url": "https://latency.example/generate_204",
"dns_type": "doh",
"dns_server": [ "dns.google/dns-query", "cloudflare-dns.com/dns-query" ],
"bootstrap_dns_server": [ "1.1.1.1", "8.8.8.8" ],
"dns_check_interval": "10s",
"dns_recovery_check_interval": "60s",
"dns_check_timeout": "2s",
"dns_detour_enabled": "1",
"dns_detour_section": "proxy",
"download_lists_via_proxy": "1",
"download_lists_via_proxy_section": "proxy"
},
@@ -116,6 +134,74 @@ JSON
validate_fixture "$WORK_DIR/valid.json"
cat >"$WORK_DIR/bad-dns-duration.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"dns_server": [ "1.1.1.1", "8.8.8.8" ],
"bootstrap_dns_server": [ "77.88.8.8" ],
"dns_check_interval": "ten seconds"
},
"section": []
}
JSON
assert_rejects "bad DNS interval" "$WORK_DIR/bad-dns-duration.json" "settings.dns_check_interval"
cat >"$WORK_DIR/bad-dns-server.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"dns_server": [ "1.1.1.1", "bad value" ],
"bootstrap_dns_server": [ "77.88.8.8" ]
},
"section": []
}
JSON
assert_rejects "bad DNS server" "$WORK_DIR/bad-dns-server.json" "Invalid main DNS server"
cat >"$WORK_DIR/empty-main-dns.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"dns_server": [],
"bootstrap_dns_server": [ "77.88.8.8" ]
},
"section": []
}
JSON
assert_rejects "empty main DNS list" "$WORK_DIR/empty-main-dns.json" "At least one main DNS server is required"
cat >"$WORK_DIR/empty-bootstrap-dns.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"dns_server": [ "77.88.8.8" ],
"bootstrap_dns_server": []
},
"section": []
}
JSON
assert_rejects "empty Bootstrap DNS list" "$WORK_DIR/empty-bootstrap-dns.json" "At least one Bootstrap DNS server is required"
cat >"$WORK_DIR/bad-dns-detour-bypass.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"dns_detour_enabled": "1",
"dns_detour_section": "bypass"
},
"section": [
{ ".name": "bypass", ".type": "section", "enabled": "1", "action": "bypass" }
]
}
JSON
assert_rejects "DNS detour bypass" "$WORK_DIR/bad-dns-detour-bypass.json" "unsupported action 'bypass'"
cat >"$WORK_DIR/bad-direct-action.json" <<'JSON'
{
"settings": { ".name": "settings", ".type": "settings" },
@@ -262,7 +348,7 @@ assert_rejects "bad country" "$WORK_DIR/bad-country.json" "Invalid country code
provider_context="$(node -e 'const fs=require("fs"); const c=JSON.parse(fs.readFileSync(process.argv[1], "utf8")); c.byedpi_installed=true; process.stdout.write(JSON.stringify(c));' "$WORK_DIR/context.json")"
cat >"$WORK_DIR/bad-byedpi.json" <<'JSON'
{
"settings": { ".name": "settings", ".type": "settings" },
"settings": { ".name": "settings", ".type": "settings", "dns_server": [ "77.88.8.8" ], "bootstrap_dns_server": [ "77.88.8.8" ] },
"section": [
{ ".name": "bye", ".type": "section", "enabled": "1", "action": "byedpi", "byedpi_cmd_opts": "--port 1080 --disorder 3" }
]
@@ -283,7 +369,7 @@ ln -s "$PODKOP_LIB/providers" "$runtime_lib/providers"
touch "$WORK_DIR/ciadpi-provider"
cat >"$WORK_DIR/bad-byedpi-runtime-state.json" <<'JSON'
{
"settings": { ".name": "settings", ".type": "settings" },
"settings": { ".name": "settings", ".type": "settings", "dns_server": [ "77.88.8.8" ], "bootstrap_dns_server": [ "77.88.8.8" ] },
"section": [
{ ".name": "bye", ".type": "section", "enabled": "1", "action": "byedpi", "byedpi_cmd_opts": "--port 1080 --disorder 3" }
]
+163
View File
@@ -0,0 +1,163 @@
#!/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"
FAILOVER="$PODKOP_LIB/singbox/dns_failover.uc"
WORK_DIR="$(mktemp -d)"
cleanup() {
rm -rf "$WORK_DIR"
}
trap cleanup EXIT
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
generate() {
local fixture="$1"
local output="$2"
local state="${3:-$WORK_DIR/missing-state.json}"
PODKOP_LIB="$PODKOP_LIB" \
PODKOP_DNS_FAILOVER_STATE_FILE="$state" \
ucode -L "$PODKOP_LIB" "$GENERATOR" generate-config-fixture "$fixture" "$output" 192.168.1.1 0
}
cat >"$WORK_DIR/single.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"dns_type": "udp",
"dns_server": "77.88.8.8",
"bootstrap_dns_server": "77.88.8.8"
},
"section": [
{
".name": "direct",
".type": "section",
"enabled": "1",
"action": "bypass",
"domain_suffix": [ "example.org" ]
}
]
}
JSON
cat >"$WORK_DIR/multi.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"dns_type": "doh",
"dns_server": [ "dns.google/dns-query", "cloudflare-dns.com/dns-query" ],
"bootstrap_dns_server": [ "1.1.1.1", "8.8.8.8" ],
"dns_check_interval": "10s",
"dns_recovery_check_interval": "60s",
"dns_check_timeout": "2s",
"dns_detour_enabled": "1",
"dns_detour_section": "proxy"
},
"section": [
{
".name": "proxy",
".type": "section",
"enabled": "1",
"action": "outbound",
"outbound_json": "{\"type\":\"direct\",\"tag\":\"fixture\"}",
"domain_suffix": [ "example.org" ]
}
]
}
JSON
cat >"$WORK_DIR/state.json" <<'JSON'
{
"version": 1,
"dns_type": "doh",
"dns_detour": "proxy-out",
"main_servers": [ "dns.google/dns-query", "cloudflare-dns.com/dns-query" ],
"bootstrap_servers": [ "1.1.1.1", "8.8.8.8" ],
"main_index": 1,
"bootstrap_index": 1
}
JSON
generate "$WORK_DIR/single.json" "$WORK_DIR/single-config.json"
generate "$WORK_DIR/multi.json" "$WORK_DIR/multi-config.json" "$WORK_DIR/state.json"
ucode -e '
let fs = require("fs");
function cfg(path) { return json(fs.readfile(path)); }
function assert(value, message) { if (!value) { warn("FAIL: ", message, "\n"); exit(1); } }
function find_tag(values, tag) { for (let value in values || []) if (value.tag == tag) return value; return null; }
function count_prefix(values, prefix) { let count = 0; for (let value in values || []) if (index(value.tag || "", prefix) == 0) count++; return count; }
let single = cfg(ARGV[0]);
assert(length(single.dns.servers) == 3, "singleton keeps the legacy three-server shape");
assert(length(single.inbounds) == 3, "singleton adds no health inbounds");
assert(count_prefix(single.dns.servers, "dns-health-") == 0, "singleton adds no health servers");
assert(find_tag(single.dns.servers, "dns-server").server == "77.88.8.8", "singleton main DNS preserved");
assert(single.route.default_domain_resolver == "dns-server", "singleton keeps main default resolver");
let multi = cfg(ARGV[1]);
assert(length(multi.dns.servers) == 7, "two main and two bootstrap candidates are generated");
assert(length(multi.inbounds) == 8, "candidate and active health inbounds are generated");
assert(count_prefix(multi.dns.servers, "dns-health-main-") == 2, "main health servers generated");
assert(count_prefix(multi.dns.servers, "dns-health-bootstrap-") == 2, "bootstrap health servers generated");
let main = find_tag(multi.dns.servers, "dns-server");
let bootstrap = find_tag(multi.dns.servers, "bootstrap-dns-server");
assert(main.server == "cloudflare-dns.com", "runtime main index selects the second server");
assert(main.detour == "proxy-out", "main DNS uses selected section detour");
assert(main.domain_resolver == "bootstrap-dns-server", "main hostname uses direct bootstrap resolver");
assert(bootstrap.server == "8.8.8.8" && bootstrap.detour == null, "bootstrap index selects direct second server");
assert(multi.route.default_domain_resolver == "bootstrap-dns-server", "detour mode breaks endpoint DNS cycles with bootstrap");
let health_rules = 0;
for (let rule in multi.dns.rules || []) {
if (index(rule.inbound || "", "dns-health-") == 0) {
health_rules++;
assert(rule.disable_cache === true, "health checks bypass DNS cache");
}
}
assert(health_rules == 5, "each candidate and the canonical active DNS have an inbound-specific rule");
' "$WORK_DIR/single-config.json" "$WORK_DIR/multi-config.json"
cat >"$WORK_DIR/select-state.json" <<'JSON'
{
"main_servers": [ "first", "second", "third" ],
"bootstrap_servers": [ "a", "b" ],
"main_index": 0,
"bootstrap_index": 1
}
JSON
cat >"$WORK_DIR/alive-failover.json" <<'JSON'
{ "0": false, "1": true, "2": true }
JSON
cat >"$WORK_DIR/alive-recovery.json" <<'JSON'
{ "0": true, "1": true }
JSON
selected="$(ucode -L "$PODKOP_LIB" "$FAILOVER" select-fixture "$WORK_DIR/select-state.json" "$WORK_DIR/alive-failover.json" main 0)"
printf '%s' "$selected" | grep -Eq '"index"[[:space:]]*:[[:space:]]*1' || fail "dead active DNS must select the first live server below"
printf '%s' "$selected" | grep -Eq '"reason"[[:space:]]*:[[:space:]]*"active_dead"' || fail "failover reason"
selected="$(ucode -L "$PODKOP_LIB" "$FAILOVER" select-fixture "$WORK_DIR/select-state.json" "$WORK_DIR/alive-recovery.json" bootstrap 1)"
printf '%s' "$selected" | grep -Eq '"index"[[:space:]]*:[[:space:]]*0' || fail "recovery must return to the highest live priority"
printf '%s' "$selected" | grep -Eq '"reason"[[:space:]]*:[[:space:]]*"recovery"' || fail "recovery reason"
cat >"$WORK_DIR/verify-previous.json" <<'JSON'
{ "main_index": 0, "bootstrap_index": 0, "bootstrap_servers": [ "a", "b" ] }
JSON
cat >"$WORK_DIR/verify-bootstrap.json" <<'JSON'
{ "main_index": 0, "bootstrap_index": 1, "bootstrap_servers": [ "a", "b" ] }
JSON
verification="$(ucode -L "$PODKOP_LIB" "$FAILOVER" verification-plan-fixture "$WORK_DIR/verify-previous.json" "$WORK_DIR/verify-bootstrap.json")"
printf '%s' "$verification" | grep -Eq '"main"[[:space:]]*:[[:space:]]*false' || fail "bootstrap-only switch must not require a dead main DNS to recover"
printf '%s' "$verification" | grep -Eq '"bootstrap"[[:space:]]*:[[:space:]]*true' || fail "bootstrap-only switch must verify the selected bootstrap DNS"
printf 'DNS failover regression checks passed\n'
+1 -1
View File
@@ -51,7 +51,7 @@ assert_mark_range_no_overlap "Zapret2" "$((ZAPRET2_ROUTE_MARK_BASE))" "$ZAPRET2_
cat >"$WORK_DIR/fixture.json" <<'JSON'
{
"settings": { ".name": "settings", ".type": "settings" }
"settings": { ".name": "settings", ".type": "settings", "dns_server": [ "77.88.8.8" ], "bootstrap_dns_server": [ "77.88.8.8" ] }
}
JSON
+9 -1
View File
@@ -217,8 +217,16 @@ validate_rejects() {
local fixture="$1"
local expected="$2"
local output
local normalized="$WORK_DIR/validator-$(basename "$fixture")"
node - "$fixture" "$normalized" <<'JS'
const fs = require('fs');
const input = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
input.settings.dns_server = ['77.88.8.8'];
input.settings.bootstrap_dns_server = ['77.88.8.8'];
fs.writeFileSync(process.argv[3], JSON.stringify(input));
JS
if output="$(PODKOP_LIB="$PODKOP_LIB" ucode -L "$PODKOP_LIB" "$VALIDATOR_UC" \
validate-runtime-fixture "$fixture" '{}' 2>/dev/null)"; then
validate-runtime-fixture "$normalized" '{}' 2>/dev/null)"; then
fail "validator accepted $fixture"
fi
printf '%s\n' "$output" | grep -Fq "$expected" ||
+11 -1
View File
@@ -28,8 +28,18 @@ generate_config() {
}
validate_fixture() {
local source="$1"
local normalized="$WORK_DIR/validator-$(basename "$source")"
node - "$source" "$normalized" <<'JS'
const fs = require('fs');
const input = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
input.settings ??= { '.name': 'settings', '.type': 'settings' };
input.settings.dns_server ??= ['77.88.8.8'];
input.settings.bootstrap_dns_server ??= ['77.88.8.8'];
fs.writeFileSync(process.argv[3], JSON.stringify(input));
JS
PODKOP_LIB="$PODKOP_LIB" ucode -L "$PODKOP_LIB" "$VALIDATOR_UC" \
validate-runtime-fixture "$1" "{}"
validate-runtime-fixture "$normalized" "{}"
}
assert_rejects() {
@@ -400,6 +400,14 @@ tcp
9.9.9.9
[settings.bootstrap_dns_server]
1.0.0.1
[settings.dns_check_interval]
10s
[settings.dns_recovery_check_interval]
60s
[settings.dns_check_timeout]
2s
[settings.dns_detour_enabled]
0
[settings.dns_rewrite_ttl]
120
[settings.output_network_interface]
@@ -103,6 +103,14 @@ if (mode == "stop-runtime" || mode == "start-runtime")
exit(64);
'
write_stub "$FAKE_LIB/singbox/dns_failover.uc" "$stub_header"'
let mode = as_string(ARGV[0]);
record("singbox/dns_failover:" + mode);
if (mode == "stop-runtime" || mode == "start-runtime")
exit(0);
exit(64);
'
run_update() {
local summary="$1"
local log="$2"
@@ -141,10 +149,12 @@ const expected = [
"server/service:prepare-all-defaults",
"config/validator:validate-runtime",
"singbox/runtime:configure-service",
"singbox/dns_failover:stop-runtime",
"singbox/runtime:init-config:0:1:1",
"singbox/priority:stop-runtime",
"service/state:reload-sing-box-runtime",
"singbox/priority:start-runtime",
"singbox/dns_failover:start-runtime",
"service/state:write-current-reload-state-clean",
"service/state:run-pending-reload-if-requested"
];
@@ -164,7 +174,7 @@ JS
unchanged_log="$WORK_DIR/unchanged.log"
run_update "0 0 1 0" "$unchanged_log"
if grep -Eq 'server/service|config/validator|singbox/runtime|singbox/priority|reload-sing-box-runtime|write-current-reload-state-clean' "$unchanged_log"; then
if grep -Eq 'server/service|config/validator|singbox/runtime|singbox/priority|singbox/dns_failover|reload-sing-box-runtime|write-current-reload-state-clean' "$unchanged_log"; then
fail "unchanged subscription update must not rebuild or reload sing-box"
fi