fix(dpi-bypass): TCP keepalive + conntrack tuning + nfqueue watchdog
- sysctl: tcp_keepalive=60/10/3, conntrack_established=600s (was 5 days) - nfqws/nfqws2: --ctrack-timeouts=30,30,600,30,30,30,30,30 in default strategy - watchdog: probe_nfqueue checks nfqws/nfqws2 process health every 300s - watchdog: heal_nfqueue_stopped restarts tachyon if DPI bypass processes die This eliminates the need for manual DNS flush + device restart when DPI starts blocking connections. Dead connections are now detected in ~90s instead of hours, and stale conntrack entries expire in 10 min instead of 5 days.
This commit is contained in:
@@ -181,7 +181,7 @@ function constants_map() {
|
||||
c.ZAPRET_DESYNC_MARK = env("ZAPRET_DESYNC_MARK", "0x40000000");
|
||||
c.ZAPRET_DESYNC_MARK_POSTNAT = env("ZAPRET_DESYNC_MARK_POSTNAT", "0x20000000");
|
||||
c.ZAPRET_LEGACY_DEFAULT_NFQWS_OPT = env("ZAPRET_LEGACY_DEFAULT_NFQWS_OPT", "--filter-tcp=80 <HOSTLIST> --dpi-desync=fake,fakedsplit --dpi-desync-autottl=2 --dpi-desync-fooling=badsum --new --filter-tcp=443 --hostlist=/opt/zapret/ipset/zapret-hosts-google.txt --dpi-desync=fake,multidisorder --dpi-desync-split-pos=1,midsld --dpi-desync-repeats=11 --dpi-desync-fooling=badsum --dpi-desync-fake-tls-mod=rnd,dupsid,sni=www.google.com --new --filter-udp=443 --hostlist=/opt/zapret/ipset/zapret-hosts-google.txt --dpi-desync=fake --dpi-desync-repeats=11 --dpi-desync-fake-quic=/opt/zapret/files/fake/quic_initial_www_google_com.bin --new --filter-udp=443 <HOSTLIST_NOAUTO> --dpi-desync=fake --dpi-desync-repeats=11 --new --filter-tcp=443 <HOSTLIST> --dpi-desync=multidisorder --dpi-desync-split-pos=1,sniext+1,host+1,midsld-2,midsld,midsld+2,endhost-1");
|
||||
c.ZAPRET_DEFAULT_NFQWS_OPT = env("ZAPRET_DEFAULT_NFQWS_OPT", "--filter-tcp=80 --dpi-desync=fake,fakedsplit --dpi-desync-autottl=2 --dpi-desync-fooling=badsum --new --filter-tcp=443 --dpi-desync=fake,multidisorder --dpi-desync-split-pos=1,midsld --dpi-desync-repeats=11 --dpi-desync-fooling=badsum --dpi-desync-fake-tls-mod=rnd,dupsid,sni=www.google.com --new --filter-udp=443 --dpi-desync=fake --dpi-desync-repeats=11 --dpi-desync-fake-quic=/opt/zapret/files/fake/quic_initial_www_google_com.bin");
|
||||
c.ZAPRET_DEFAULT_NFQWS_OPT = env("ZAPRET_DEFAULT_NFQWS_OPT", "--filter-tcp=80 --dpi-desync=fake,fakedsplit --dpi-desync-autottl=2 --dpi-desync-fooling=badsum --new --filter-tcp=443 --dpi-desync=fake,multidisorder --dpi-desync-split-pos=1,midsld --dpi-desync-repeats=11 --dpi-desync-fooling=badsum --dpi-desync-fake-tls-mod=rnd,dupsid,sni=www.google.com --new --filter-udp=443 --dpi-desync=fake --dpi-desync-repeats=11 --dpi-desync-fake-quic=/opt/zapret/files/fake/quic_initial_www_google_com.bin --ctrack-timeouts=30,30,600,30,30,30,30,30");
|
||||
|
||||
c.ZAPRET2_PROVIDER_BASE_DIR = env("ZAPRET2_PROVIDER_BASE_DIR", "/opt/zapret2");
|
||||
c.ZAPRET2_PROVIDER_NFQWS2_BIN = env("ZAPRET2_PROVIDER_NFQWS2_BIN", c.ZAPRET2_PROVIDER_BASE_DIR + "/nfq2/nfqws2");
|
||||
@@ -199,7 +199,7 @@ function constants_map() {
|
||||
c.ZAPRET2_NFQWS2_RESPAWN_DELAY = env("ZAPRET2_NFQWS2_RESPAWN_DELAY", "5");
|
||||
c.ZAPRET2_DESYNC_MARK = env("ZAPRET2_DESYNC_MARK", "0x40000000");
|
||||
c.ZAPRET2_DESYNC_MARK_POSTNAT = env("ZAPRET2_DESYNC_MARK_POSTNAT", "0x20000000");
|
||||
c.ZAPRET2_DEFAULT_NFQWS2_OPT = env("ZAPRET2_DEFAULT_NFQWS2_OPT", "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=fake:blob=fake_default_tls:tcp_md5:tcp_seq=-10000 --lua-desync=multidisorder:pos=1,midsld --new --filter-udp=443 --filter-l7=quic --payload=quic_initial --lua-desync=fake:blob=fake_default_quic:repeats=6");
|
||||
c.ZAPRET2_DEFAULT_NFQWS2_OPT = env("ZAPRET2_DEFAULT_NFQWS2_OPT", "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=fake:blob=fake_default_tls:tcp_md5:tcp_seq=-10000 --lua-desync=multidisorder:pos=1,midsld --new --filter-udp=443 --filter-l7=quic --payload=quic_initial --lua-desync=fake:blob=fake_default_quic:repeats=6 --ctrack-timeouts=30,30,600,30,30,30,30,30");
|
||||
|
||||
c.BYEDPI_BIN = env("BYEDPI_BIN", "/usr/bin/ciadpi");
|
||||
c.BYEDPI_SERVICE_INIT = env("BYEDPI_SERVICE_INIT", "/etc/init.d/byedpi");
|
||||
|
||||
@@ -1829,6 +1829,28 @@ function ensure_bridge_netfilter_disabled() {
|
||||
run_args([ "sysctl", "-w", "net.bridge.bridge-nf-call-ip6tables=0" ]);
|
||||
}
|
||||
|
||||
// TCP keepalive: detect dead connections in ~90s instead of kernel default (hours).
|
||||
// Conntrack: expire stale TCP entries in 10 min instead of 5 days.
|
||||
function apply_connection_tuning() {
|
||||
let sysctls = [
|
||||
[ "net.ipv4.tcp_keepalive_time", "60" ],
|
||||
[ "net.ipv4.tcp_keepalive_intvl", "10" ],
|
||||
[ "net.ipv4.tcp_keepalive_probes", "3" ],
|
||||
[ "net.netfilter.nf_conntrack_tcp_timeout_established", "600" ],
|
||||
[ "net.netfilter.nf_conntrack_tcp_timeout_time_wait", "30" ]
|
||||
];
|
||||
let ok = true;
|
||||
for (let pair in sysctls) {
|
||||
let current = trim(command_output_from_args([ "sysctl", "-n", pair[0] ]) || "");
|
||||
if (current == pair[1]) continue;
|
||||
if (!run_args_quiet([ "sysctl", "-w", pair[0] + "=" + pair[1] ]))
|
||||
ok = false;
|
||||
}
|
||||
if (ok)
|
||||
log_debug("Connection tuning applied: tcp_keepalive=60/10/3, conntrack_established=600");
|
||||
return ok;
|
||||
}
|
||||
|
||||
function community_service_has_subnet_list(value) {
|
||||
return rule_config.community_service_has_subnet_list(value);
|
||||
}
|
||||
@@ -1960,6 +1982,7 @@ function nft_create_full_runtime_from_uci(rt_table, table, localv4_set, common_s
|
||||
log_debug("Building nftables runtime model");
|
||||
|
||||
return ensure_bridge_netfilter_disabled() &&
|
||||
apply_connection_tuning() &&
|
||||
ensure_tproxy_route_rule(rt_table, fakeip_mark) &&
|
||||
nft_create_runtime_base_from_uci(table, localv4_set, common_set, port_set, ip_port_set, interface_set, fakeip_mark, outbound_mark, fakeip_range, tproxy_port, localv6_set, common6_set, ip_port6_set, fakeip6_range, tproxy6_address) &&
|
||||
nft_add_section_priority_rules_from_sections(uci_sections("section"), table, interface_set, localv4_set, localv6_set, fakeip_mark) &&
|
||||
|
||||
@@ -70,6 +70,8 @@ const EV = {
|
||||
SECTIONS_EMPTY: "sections.empty",
|
||||
ANOMALY_RECONNECTS: "anomaly.reconnects",
|
||||
PAUSE_EXPIRED: "pause.expired",
|
||||
NFQUEUE_DOWN: "nfqueue.down",
|
||||
NFQUEUE_UP: "nfqueue.up",
|
||||
TICK: "tick"
|
||||
};
|
||||
|
||||
@@ -957,6 +959,46 @@ function controller(bus, opts) {
|
||||
bus.emit(EV.OOM_RECOVERABLE, { last_oom: state.last_oom_time, scale: current_scale });
|
||||
}
|
||||
|
||||
// ── Probe: nfqueue/zapret runtime health ──────────────────────────────────
|
||||
// Checks if nfqws/nfqws2 supervisor processes are alive by counting PID
|
||||
// files. A mismatch means the DPI bypass engine crashed or was killed.
|
||||
function probe_nfqueue() {
|
||||
if (!bus.has(EV.NFQUEUE_DOWN) && !bus.has(EV.NFQUEUE_UP)) return;
|
||||
if (is_reload_in_progress()) return;
|
||||
|
||||
let pid_dirs = [
|
||||
"/var/run/tachyon/zapret/child-pid",
|
||||
"/var/run/tachyon/zapret2/child-pid"
|
||||
];
|
||||
|
||||
for (let i = 0; i < length(pid_dirs); i++) {
|
||||
let child_dir = pid_dirs[i];
|
||||
let child_entries = fs.readdir(child_dir);
|
||||
if (child_entries == null) continue;
|
||||
let expected = length(child_entries);
|
||||
if (expected == 0) continue;
|
||||
|
||||
let running = 0;
|
||||
for (let entry in child_entries) {
|
||||
let pid_data = fs.readfile(child_dir + "/" + entry);
|
||||
if (pid_data == null) continue;
|
||||
let pid = trim(as_string(pid_data));
|
||||
if (pid == "") continue;
|
||||
try { system("kill -0 " + pid + " 2>/dev/null"); running++; } catch (e) {}
|
||||
}
|
||||
|
||||
if (running == expected) {
|
||||
bus.emit(EV.NFQUEUE_UP, { running: running, expected: expected });
|
||||
} else {
|
||||
bus.emit(EV.NFQUEUE_DOWN, {
|
||||
running: running,
|
||||
expected: expected,
|
||||
dir: child_dir
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tier drivers ──────────────────────────────────────────────────────────
|
||||
// Each probe is wrapped so one failing observation cannot stop the tier;
|
||||
// this mirrors the safe_call() isolation the old per-check loop had.
|
||||
@@ -1004,6 +1046,7 @@ function controller(bus, opts) {
|
||||
run_probe(probe_empty_sections, "empty_sections");
|
||||
run_probe(probe_anomalies, "anomalies");
|
||||
run_probe(probe_oom_recovery, "oom_recovery");
|
||||
run_probe(probe_nfqueue, "nfqueue");
|
||||
};
|
||||
|
||||
// Health is graded by how long we go without publishing a fault: after a
|
||||
|
||||
@@ -649,6 +649,10 @@ function note_dns_recovered(ev) {
|
||||
settle_recovery("dns", "fixed");
|
||||
}
|
||||
|
||||
function note_nfqueue_recovered(ev) {
|
||||
settle_recovery("nfqueue", "fixed");
|
||||
}
|
||||
|
||||
// Guard: skip if a tachyon reload is already in progress (prevents concurrent reload_firewall races)
|
||||
function is_reload_in_progress() {
|
||||
return fs.stat("/var/run/tachyon.reload.lock") != null
|
||||
@@ -1022,6 +1026,35 @@ function heal_dns_continuous(ev) {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── AI nfqueue/zapret Runtime Recovery ──────────────────────────────────────
|
||||
// When nfqws/nfqws2 processes die (crash, OOM, kill), DPI bypass stops working.
|
||||
// The simplest fix is a full tachyon restart which respawns everything.
|
||||
function heal_nfqueue_stopped(ev) {
|
||||
if (settings().ai_nfqueue_health_enabled == "0") return;
|
||||
if (suppressed_by_root_cause("heal_nfqueue_stopped")) return;
|
||||
|
||||
let running = int(ev.payload.running) || 0;
|
||||
let expected = int(ev.payload.expected) || 0;
|
||||
|
||||
// Only act when some processes are running but not all — a full crash is
|
||||
// already caught by the sing-box stopped or proxy down healers.
|
||||
if (running > 0 && running >= expected) return;
|
||||
|
||||
let incident = {
|
||||
type: "nfqueue_health",
|
||||
description: sprintf("nfqueue/zapret runtime: %d/%d processes alive (%s)",
|
||||
running, expected, as_string(ev.payload.dir || "")),
|
||||
resolution: "Restarting tachyon to respawn nfqws/nfqws2"
|
||||
};
|
||||
|
||||
if (!safe_proxy_restart("nfqueue_health")) {
|
||||
ai_heal_report(incident.type, incident.description,
|
||||
"Restart skipped due to rate limit", "skipped");
|
||||
return;
|
||||
}
|
||||
watch_recovery("nfqueue", incident, "nfqueue_health");
|
||||
}
|
||||
|
||||
// ─── AI Empty Sections Recovery ─────────────────────────────────────────────
|
||||
function heal_empty_sections(ev) {
|
||||
let recovered = common.array_or_empty(ev.payload.sections);
|
||||
@@ -1619,6 +1652,10 @@ function register_subscribers() {
|
||||
subscribe(EV.DNS_DOWN, heal_dns_continuous,
|
||||
{ name: "heal_dns_continuous", priority: PRIORITY_DNS + 1 });
|
||||
|
||||
// nfqueue/zapret runtime: respawn crashed nfqws/nfqws2 processes.
|
||||
subscribe(EV.NFQUEUE_DOWN, heal_nfqueue_stopped,
|
||||
{ name: "heal_nfqueue_stopped", priority: 45, cooldown: 300 });
|
||||
|
||||
// Recovery facts. These were published from the start but had no subscriber,
|
||||
// which is why a repair could only ever report its own intent. No cooldown:
|
||||
// settle_recovery() is a no-op unless a watch is actually open, and skipping
|
||||
@@ -1632,6 +1669,8 @@ function register_subscribers() {
|
||||
// suppression deadline.
|
||||
subscribe(EV.WAN_UP, note_wan_recovered,
|
||||
{ name: "note_wan_recovered", priority: 5 });
|
||||
subscribe(EV.NFQUEUE_UP, note_nfqueue_recovered,
|
||||
{ name: "note_nfqueue_recovered", priority: 5 });
|
||||
|
||||
subscribe(EV.SECTIONS_EMPTY, heal_empty_sections,
|
||||
{ name: "heal_empty_sections", priority: 60, cooldown: 120 });
|
||||
|
||||
Reference in New Issue
Block a user