Add prioritized DNS failover
This commit is contained in:
+528
-437
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PODKOP PLUS\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-10 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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" }
|
||||
]
|
||||
|
||||
@@ -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'
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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" ||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user