Features: - Per-Section MAC Exclusions: nft, generator_routes, section.js, i18n - Per-Section Routed DNS: connections, generator_routes, nft, validator, types.ts, section.js, state.uc, reload.uc - Boot Latency Sweep: ui.uc latency boot sweep with fixture dispatchers - WDTT provider: full backend (common, validator, runtime), sing-box outbound, diagnostics, component install, updater, i18n, section.js UI - OlcRTC provider: full backend (common, validator, runtime), sing-box outbound, diagnostics, component install, updater, i18n, section.js UI - WireGuard-over-TURN tunnel (WDTT) and TCP-over-WebRTC tunnel (OlcRTC) as sidecar processes with local SOCKS5, routed through sing-box outbound Bug fixes: - route.uc: restrict hijack-dns to local DNS inbounds only (dns-in, source-dns-in, dns-health-*) — fixes socket collision between sing-box and dnsmasq on port 53 via SO_REUSEADDR transparent write-back sockets - runtime.uc: add Port 53 socket collision diagnostic with auto-fix (sing-box restart to clear parasitic UNCONN sockets with Recv-Q) - runtime.uc: add dnsmasq dns_redirect check with auto-fix — disables firewall4 DNAT that causes TProxy to rewrite destination to router :53 - failover.uc: add wdtt/olcrtc to failover candidate actions - telegram.uc: show WDTT/OlcRTC installed status in /info Tests: 135/155 shell + 591/591 frontend
330 lines
10 KiB
Ucode
330 lines
10 KiB
Ucode
#!/usr/bin/env ucode
|
|
|
|
let fs = require("fs");
|
|
let common = require("core.common");
|
|
|
|
let as_string = common.as_string;
|
|
|
|
function arg_bool(value) {
|
|
return value === true || value == "true" || value == "1" || value == 1;
|
|
}
|
|
|
|
function whitespace_fields(value) {
|
|
value = as_string(value);
|
|
let result = [];
|
|
|
|
for (let item in split(trim(value), /[ \t\r\n]+/))
|
|
if (item != "")
|
|
push(result, item);
|
|
|
|
return result;
|
|
}
|
|
|
|
function list_has_item(items, needle) {
|
|
needle = as_string(needle);
|
|
if (needle == "")
|
|
return false;
|
|
|
|
for (let item in whitespace_fields(items))
|
|
if (item == needle)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
function new_urltest_enabled_sections(previous_sections, current_sections, previous_known) {
|
|
if (!arg_bool(previous_known))
|
|
return "";
|
|
|
|
let result = [];
|
|
for (let section in whitespace_fields(current_sections))
|
|
if (!list_has_item(previous_sections, section))
|
|
push(result, section);
|
|
|
|
return join(" ", result);
|
|
}
|
|
|
|
function emit(name, value) {
|
|
print(name, "\t", as_string(value), "\n");
|
|
}
|
|
|
|
function emit_bool(name, value) {
|
|
emit(name, value ? "1" : "0");
|
|
}
|
|
|
|
function emit_reload_plan(previous, current, context) {
|
|
let changed = {
|
|
service_triggers: current.service_trigger != previous.service_trigger,
|
|
dnsmasq: current.dnsmasq != previous.dnsmasq,
|
|
sing_box: current.sing_box != previous.sing_box,
|
|
nft: current.nft != previous.nft,
|
|
zapret_queue: current.zapret_queue != previous.zapret_queue,
|
|
zapret_runtime: current.zapret_runtime != previous.zapret_runtime,
|
|
zapret2_queue: current.zapret2_queue != previous.zapret2_queue,
|
|
zapret2_runtime: current.zapret2_runtime != previous.zapret2_runtime,
|
|
byedpi_runtime: current.byedpi_runtime != previous.byedpi_runtime,
|
|
wdtt_runtime: current.wdtt_runtime != previous.wdtt_runtime,
|
|
olcrtc_runtime: current.olcrtc_runtime != previous.olcrtc_runtime,
|
|
list: current.list != previous.list,
|
|
hosts_list: current.hosts_list != previous.hosts_list,
|
|
cron: current.cron != previous.cron
|
|
};
|
|
|
|
let needs = {
|
|
sing_box_reload: false,
|
|
nft_rebuild: false,
|
|
zapret_restart: false,
|
|
zapret2_restart: false,
|
|
byedpi_restart: false,
|
|
wdtt_restart: false,
|
|
olcrtc_restart: false,
|
|
dnsmasq_configure: false,
|
|
dnsmasq_restore: false,
|
|
cron_refresh: false,
|
|
list_update: false,
|
|
hosts_update: false
|
|
};
|
|
|
|
if (changed.sing_box)
|
|
needs.sing_box_reload = true;
|
|
|
|
if (changed.nft)
|
|
needs.nft_rebuild = true;
|
|
|
|
if (changed.zapret_queue) {
|
|
needs.zapret_restart = true;
|
|
needs.nft_rebuild = true;
|
|
needs.sing_box_reload = true;
|
|
}
|
|
|
|
if (changed.zapret_runtime) {
|
|
needs.zapret_restart = true;
|
|
needs.nft_rebuild = true;
|
|
}
|
|
|
|
if (changed.zapret2_queue) {
|
|
needs.zapret2_restart = true;
|
|
needs.nft_rebuild = true;
|
|
needs.sing_box_reload = true;
|
|
}
|
|
|
|
if (changed.zapret2_runtime) {
|
|
needs.zapret2_restart = true;
|
|
needs.nft_rebuild = true;
|
|
}
|
|
|
|
if (changed.byedpi_runtime)
|
|
needs.byedpi_restart = true;
|
|
|
|
if (changed.wdtt_runtime)
|
|
needs.wdtt_restart = true;
|
|
|
|
if (changed.olcrtc_runtime)
|
|
needs.olcrtc_restart = true;
|
|
|
|
if (changed.dnsmasq) {
|
|
if (!current.dont_touch_dhcp)
|
|
needs.dnsmasq_configure = true;
|
|
else if (context.dnsmasq_managed_state)
|
|
needs.dnsmasq_restore = true;
|
|
}
|
|
|
|
if (changed.cron)
|
|
needs.cron_refresh = true;
|
|
|
|
if (changed.list && context.has_list_update_sources)
|
|
needs.list_update = true;
|
|
|
|
if (changed.hosts_list) {
|
|
needs.hosts_update = true;
|
|
needs.sing_box_reload = true;
|
|
}
|
|
|
|
if (needs.nft_rebuild && context.has_nft_list_update_sources)
|
|
needs.list_update = true;
|
|
|
|
if (context.runtime_cache_needs_rebuild) {
|
|
changed.sing_box = true;
|
|
needs.sing_box_reload = true;
|
|
}
|
|
|
|
if (context.force_runtime_reload &&
|
|
!needs.sing_box_reload &&
|
|
!needs.nft_rebuild &&
|
|
!needs.zapret_restart &&
|
|
!needs.zapret2_restart &&
|
|
!needs.byedpi_restart &&
|
|
!needs.dnsmasq_configure &&
|
|
!needs.dnsmasq_restore)
|
|
needs.sing_box_reload = true;
|
|
|
|
let has_work =
|
|
needs.sing_box_reload ||
|
|
needs.nft_rebuild ||
|
|
needs.zapret_restart ||
|
|
needs.zapret2_restart ||
|
|
needs.byedpi_restart ||
|
|
needs.dnsmasq_configure ||
|
|
needs.dnsmasq_restore ||
|
|
needs.cron_refresh ||
|
|
needs.list_update ||
|
|
needs.hosts_update;
|
|
|
|
emit("urltest_new_enabled_sections", new_urltest_enabled_sections(
|
|
previous.urltest_sections,
|
|
current.urltest_sections,
|
|
previous.urltest_sections_known
|
|
));
|
|
|
|
emit_bool("changed_service_triggers", changed.service_triggers);
|
|
emit_bool("changed_dnsmasq", changed.dnsmasq);
|
|
emit_bool("changed_sing_box", changed.sing_box);
|
|
emit_bool("changed_nft", changed.nft);
|
|
emit_bool("changed_zapret_queue", changed.zapret_queue);
|
|
emit_bool("changed_zapret_runtime", changed.zapret_runtime);
|
|
emit_bool("changed_zapret2_queue", changed.zapret2_queue);
|
|
emit_bool("changed_zapret2_runtime", changed.zapret2_runtime);
|
|
emit_bool("changed_byedpi_runtime", changed.byedpi_runtime);
|
|
emit_bool("changed_wdtt_runtime", changed.wdtt_runtime);
|
|
emit_bool("changed_olcrtc_runtime", changed.olcrtc_runtime);
|
|
emit_bool("changed_cron", changed.cron);
|
|
emit_bool("changed_list", changed.list);
|
|
emit_bool("changed_hosts_list", changed.hosts_list);
|
|
|
|
emit_bool("needs_sing_box_reload", needs.sing_box_reload);
|
|
emit_bool("needs_nft_rebuild", needs.nft_rebuild);
|
|
emit_bool("needs_zapret_restart", needs.zapret_restart);
|
|
emit_bool("needs_zapret2_restart", needs.zapret2_restart);
|
|
emit_bool("needs_byedpi_restart", needs.byedpi_restart);
|
|
emit_bool("needs_wdtt_restart", needs.wdtt_restart);
|
|
emit_bool("needs_olcrtc_restart", needs.olcrtc_restart);
|
|
emit_bool("needs_dnsmasq_configure", needs.dnsmasq_configure);
|
|
emit_bool("needs_dnsmasq_restore", needs.dnsmasq_restore);
|
|
emit_bool("needs_cron_refresh", needs.cron_refresh);
|
|
emit_bool("needs_list_update", needs.list_update);
|
|
emit_bool("needs_hosts_update", needs.hosts_update);
|
|
emit_bool("has_work", has_work);
|
|
}
|
|
|
|
function reload_plan() {
|
|
let previous = {
|
|
service_trigger: as_string(ARGV[1]),
|
|
dnsmasq: as_string(ARGV[2]),
|
|
sing_box: as_string(ARGV[3]),
|
|
nft: as_string(ARGV[4]),
|
|
zapret_queue: as_string(ARGV[5]),
|
|
zapret_runtime: as_string(ARGV[6]),
|
|
zapret2_queue: as_string(ARGV[7]),
|
|
zapret2_runtime: as_string(ARGV[8]),
|
|
byedpi_runtime: as_string(ARGV[9]),
|
|
list: as_string(ARGV[10]),
|
|
cron: as_string(ARGV[11]),
|
|
urltest_sections_known: arg_bool(ARGV[12]),
|
|
urltest_sections: as_string(ARGV[13])
|
|
};
|
|
|
|
let current = {
|
|
service_trigger: as_string(ARGV[14]),
|
|
dnsmasq: as_string(ARGV[15]),
|
|
sing_box: as_string(ARGV[16]),
|
|
nft: as_string(ARGV[17]),
|
|
zapret_queue: as_string(ARGV[18]),
|
|
zapret_runtime: as_string(ARGV[19]),
|
|
zapret2_queue: as_string(ARGV[20]),
|
|
zapret2_runtime: as_string(ARGV[21]),
|
|
byedpi_runtime: as_string(ARGV[22]),
|
|
list: as_string(ARGV[23]),
|
|
cron: as_string(ARGV[24]),
|
|
urltest_sections: as_string(ARGV[25]),
|
|
dont_touch_dhcp: arg_bool(ARGV[26])
|
|
};
|
|
|
|
let context = {
|
|
force_runtime_reload: arg_bool(ARGV[27]),
|
|
dnsmasq_managed_state: arg_bool(ARGV[28]),
|
|
has_list_update_sources: arg_bool(ARGV[29]),
|
|
has_nft_list_update_sources: arg_bool(ARGV[30]),
|
|
runtime_cache_needs_rebuild: arg_bool(ARGV[31])
|
|
};
|
|
|
|
emit_reload_plan(previous, current, context);
|
|
}
|
|
|
|
function read_state_file(path) {
|
|
let data = fs.readfile(path);
|
|
if (data == null)
|
|
return null;
|
|
|
|
let state = {};
|
|
let has = {};
|
|
|
|
for (let line in split(data, "\n")) {
|
|
if (line == "")
|
|
continue;
|
|
|
|
let equals = index(line, "=");
|
|
let key = equals >= 0 ? substr(line, 0, equals) : line;
|
|
if (key == "")
|
|
continue;
|
|
|
|
state[key] = equals >= 0 ? substr(line, equals + 1) : "";
|
|
has[key] = true;
|
|
}
|
|
|
|
state.__has = has;
|
|
return state;
|
|
}
|
|
|
|
function plan_state_from_file(path) {
|
|
let state = read_state_file(path);
|
|
if (state == null)
|
|
return null;
|
|
|
|
return {
|
|
format: as_string(state.format),
|
|
service_trigger: as_string(state.service_trigger_signature || state.restart_signature),
|
|
dnsmasq: as_string(state.dnsmasq_signature),
|
|
sing_box: as_string(state.sing_box_signature),
|
|
nft: as_string(state.nft_signature),
|
|
zapret_queue: as_string(state.zapret_queue_signature),
|
|
zapret_runtime: as_string(state.zapret_runtime_signature),
|
|
zapret2_queue: as_string(state.zapret2_queue_signature),
|
|
zapret2_runtime: as_string(state.zapret2_runtime_signature),
|
|
byedpi_runtime: as_string(state.byedpi_runtime_signature),
|
|
list: as_string(state.list_signature),
|
|
cron: as_string(state.cron_signature),
|
|
urltest_sections_known: state.__has.urltest_enabled_sections === true,
|
|
urltest_sections: as_string(state.urltest_enabled_sections),
|
|
dont_touch_dhcp: arg_bool(state.dont_touch_dhcp)
|
|
};
|
|
}
|
|
|
|
function plan_state_files() {
|
|
let previous = plan_state_from_file(ARGV[1]);
|
|
let current = plan_state_from_file(ARGV[2]);
|
|
if (previous == null || current == null || previous.format == "" || previous.format != current.format)
|
|
exit(2);
|
|
|
|
let context = {
|
|
force_runtime_reload: arg_bool(ARGV[3]),
|
|
dnsmasq_managed_state: arg_bool(ARGV[4]),
|
|
has_list_update_sources: arg_bool(ARGV[5]),
|
|
has_nft_list_update_sources: arg_bool(ARGV[6]),
|
|
runtime_cache_needs_rebuild: arg_bool(ARGV[7])
|
|
};
|
|
|
|
emit_reload_plan(previous, current, context);
|
|
}
|
|
|
|
let mode = ARGV[0] || "";
|
|
|
|
if (mode == "plan")
|
|
reload_plan();
|
|
else if (mode == "plan-state-files")
|
|
plan_state_files();
|
|
else {
|
|
warn("Usage: service/reload.uc plan ...\n");
|
|
warn(" service/reload.uc plan-state-files <previous-state> <current-state> <force-reload> <dnsmasq-managed-state> <list-update-sources> <nft-list-update-sources> <runtime-cache-needs-rebuild>\n");
|
|
exit(1);
|
|
}
|