mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:27:02 +01:00
dns_enforcer: split 325 -> 171 + 170, and give it a working test
The iptables chain machinery (ensure_chain, trusted_dot_ips, fill_chain_v4/v6, expected_rule_count, chain_intact, enforce_iptables) moves into dns_iptables.sh — a sibling, not a lib/ member, because lib/ does not exist on the phone and deploy.sh copies these files flat into $REMOTE_DIR. Both deploy lists grew the entry, matching how tether_iptables.sh is carried; it is correctly absent from the chmod 755 line, since a sourced library needs read, not execute. extract_shell_functions.py refuses to anchor a source line in a #!/system/bin/sh script, as documented — but when it refuses it still writes the library and does NOT remove the functions from the source, so a naive run leaves every function defined twice and the entry script longer than it started. The excision was done separately; the hash tool confirms all 14 functions moved verbatim. test_dns_enforcer.sh is deleted rather than repaired. Its six assertions targeted extract_ping_ip, extract_package_uid and append_unique_line — none of which dns_enforcer.sh has ever defined. It died at its source line and had never run a single assertion since it was added. Its replacement, test_dns_iptables.sh, covers all seven moved functions with 20 assertions, staged the way test_tether_enforcer.sh stages its subject. Three independent mutations were each caught: undercounting the trusted-resolver ACCEPT, miscounting rules per DoH IP, and moving the ACCEPT after the blanket REJECT. Shared staging lives in dns_iptables_harness.sh so both test files stay under the cap. Not verified on the phone — no deploy has run, by instruction. Over-cap count 23 -> 22. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
This commit is contained in:
parent
728301c9a4
commit
4000c7a2ca
@ -0,0 +1,18 @@
|
||||
{
|
||||
"title": "split dns_enforcer.sh and give its chain code a real test",
|
||||
"objective": "Move the iptables chain machinery out of dns_enforcer.sh so both files are under 250 lines, carry the new sibling in both deploy.sh lists, and replace the dead test_dns_enforcer.sh with a suite that actually exercises the moved code.",
|
||||
"acceptance_criteria": [
|
||||
"verify_shell_split.sh reports all 14 top-level functions moved verbatim",
|
||||
"test_dns_iptables.sh reports 20 passed, 0 failed, and fails under each of three independent mutations",
|
||||
"dns_iptables.sh appears in BOTH the adb_cmd push list and the adb_root cp list in deploy.sh",
|
||||
"Sourcing dns_enforcer.sh with main() disabled resolves all seven moved functions at runtime",
|
||||
"shellcheck is clean on every changed shell file, standing alone, with zero suppressions",
|
||||
"check_file_length.sh --all drops from 23 over-cap files to 22, with no new file over the cap"
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Deploying to the phone or verifying on-device behaviour — explicitly excluded by the user this session",
|
||||
"Repairing test_dns_enforcer.sh, which is deleted rather than fixed",
|
||||
"Any behavioural change to the chain functions themselves"
|
||||
],
|
||||
"verifier": "bash phone_focus_mode/lib/tests/test_dns_iptables.sh + meta/scripts/verify_shell_split.sh + shellcheck + check_file_length.sh"
|
||||
}
|
||||
68
docs/superpowers/evidence/dns-enforcer-split-2026-08-17.json
Normal file
68
docs/superpowers/evidence/dns-enforcer-split-2026-08-17.json
Normal file
@ -0,0 +1,68 @@
|
||||
{
|
||||
"intent": "Bring phone_focus_mode/dns_enforcer.sh under the 250-line cap by moving its iptables chain machinery into a sibling script, and give that code its first working test.",
|
||||
"scope": [
|
||||
"phone_focus_mode/dns_enforcer.sh — 325 lines, now 171",
|
||||
"phone_focus_mode/dns_iptables.sh — new, 170 lines",
|
||||
"phone_focus_mode/deploy.sh — both per-file lists grown by one entry",
|
||||
"phone_focus_mode/lib/tests/test_dns_iptables.sh + dns_iptables_harness.sh — new tests",
|
||||
"phone_focus_mode/lib/tests/test_dns_enforcer.sh — deleted"
|
||||
],
|
||||
"changes": [
|
||||
"Moved ensure_chain, trusted_dot_ips, fill_chain_v4, fill_chain_v6, expected_rule_count, chain_intact and enforce_iptables into dns_iptables.sh, a sibling of the entry script rather than a lib/ member: lib/ does not exist on the phone and deploy.sh copies these files flat into $REMOTE_DIR.",
|
||||
"Placed the source line by hand after the existing config.sh line. extract_shell_functions.py refuses to anchor in a #!/system/bin/sh script, and when it refuses it still writes the library but does NOT remove the functions from the source, so a naive run leaves them defined twice. The excision was done separately and confirmed by hash.",
|
||||
"Added dns_iptables.sh to BOTH deploy.sh lists — the adb_cmd push staging list and the adb_root cp install list — matching how tether_iptables.sh is handled. It is correctly absent from the chmod 755 line, since a sourced library needs read, not execute.",
|
||||
"Deleted test_dns_enforcer.sh. Its six assertions targeted extract_ping_ip, extract_package_uid and append_unique_line, none of which dns_enforcer.sh has ever defined; it died at its source line and had never run a single assertion since the commit that added it.",
|
||||
"Wrote test_dns_iptables.sh (20 assertions over all seven moved functions) in the staging style test_tether_enforcer.sh proves works, with the shared staging and stubs in dns_iptables_harness.sh so both files stay under the cap."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "bash phone_focus_mode/lib/tests/test_dns_iptables.sh",
|
||||
"result": "pass",
|
||||
"evidence": "20 passed, 0 failed, covering expected_rule_count (5 cases), trusted_dot_ips (4), ensure_chain (2), fill_chain_v4/v6 (3), chain_intact (3) and enforce_iptables (3)."
|
||||
},
|
||||
{
|
||||
"command": "mutation: expected_rule_count drops the trusted-resolver tally (n+1 -> n+0)",
|
||||
"result": "fail-as-expected",
|
||||
"evidence": "'FAIL: adds one ACCEPT per trusted v4 resolver address (want 7, got 6)' — the test catches the undercount that the function's own comment warns causes a rebuild every DNS_CHECK_INTERVAL seconds."
|
||||
},
|
||||
{
|
||||
"command": "mutation: fill_chain_v4 moves the trusted ACCEPT loop after the blanket REJECT",
|
||||
"result": "fail-as-expected",
|
||||
"evidence": "'FAIL: puts the trusted-resolver ACCEPT before the blanket REJECT (want ACCEPT, got REJECT)' — first-match ordering is enforced, not assumed."
|
||||
},
|
||||
{
|
||||
"command": "mutation: expected_rule_count counts 3 rules per DoH IP instead of 4",
|
||||
"result": "fail-as-expected",
|
||||
"evidence": "6 of the 20 assertions fail. Restoring the file returns 20 passed, 0 failed, so the suite is non-vacuous."
|
||||
},
|
||||
{
|
||||
"command": "bash meta/scripts/verify_shell_split.sh HEAD phone_focus_mode/dns_enforcer.sh phone_focus_mode/dns_enforcer.sh phone_focus_mode/dns_iptables.sh",
|
||||
"result": "pass",
|
||||
"evidence": "'IDENTICAL: all 14 top-level functions moved verbatim'."
|
||||
},
|
||||
{
|
||||
"command": "sh dns_enforcer.sh with main() disabled, then command -v each function",
|
||||
"result": "pass",
|
||||
"evidence": "All 11 probed functions resolve (the 7 moved plus log, rotate_log, acquire_lock, ensure_private_dns_off), proving the new source line loads the sibling at runtime and not merely at lint time."
|
||||
},
|
||||
{
|
||||
"command": "shellcheck on all four changed shell files",
|
||||
"result": "pass",
|
||||
"evidence": "Clean, each standing alone, with zero suppressions. Two SC2016 findings were resolved structurally — a [$] character class in the sed pattern and a quoted heredoc prelude — rather than disabled."
|
||||
},
|
||||
{
|
||||
"command": "bash meta/scripts/check_file_length.sh --all",
|
||||
"result": "fail-as-expected",
|
||||
"evidence": "23 files over the cap before, 22 after. dns_enforcer.sh 171, dns_iptables.sh 170, test 197, harness 133 — all under."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"UNVERIFIED ON THE PHONE. No deploy has run, by explicit instruction. If DNS enforcement misbehaves after the next deploy, dns_iptables.sh reaching the device is the first thing to check — the two deploy.sh list entries are what carry it.",
|
||||
"The iptables stub models only the subset of the interface these functions touch (-L/-N/-F/-A/-I/-D/-C/-S). A real iptables failure mode outside that subset, such as a locking error, is not represented.",
|
||||
"tether_enforcer.sh remains unverified on the device from the previous session, so two enforcer splits are now in flight at once."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit; dns_enforcer.sh returns to 325 lines, dns_iptables.sh and both new test files disappear, and the two deploy.sh entries are removed together with the code they carry.",
|
||||
"After rollback, run sh -n phone_focus_mode/dns_enforcer.sh and confirm the file no longer sources dns_iptables.sh, then grep deploy.sh for dns_iptables to confirm zero hits — a stale cp line pointing at a file that is no longer staged is the failure this ordering prevents."
|
||||
]
|
||||
}
|
||||
@ -376,6 +376,7 @@ do_deploy() {
|
||||
adb_cmd push "$DEPLOY_DIR/focus_ctl.sh" "/data/local/tmp/focus_stage/focus_ctl.sh"
|
||||
adb_cmd push "$DEPLOY_DIR/hosts_enforcer.sh" "/data/local/tmp/focus_stage/hosts_enforcer.sh"
|
||||
adb_cmd push "$DEPLOY_DIR/dns_enforcer.sh" "/data/local/tmp/focus_stage/dns_enforcer.sh"
|
||||
adb_cmd push "$DEPLOY_DIR/dns_iptables.sh" "/data/local/tmp/focus_stage/dns_iptables.sh"
|
||||
adb_cmd push "$DEPLOY_DIR/launcher_enforcer.sh" "/data/local/tmp/focus_stage/launcher_enforcer.sh"
|
||||
adb_cmd push "$DEPLOY_DIR/curfew_enforcer.sh" "/data/local/tmp/focus_stage/curfew_enforcer.sh"
|
||||
adb_cmd push "$DEPLOY_DIR/tether_enforcer.sh" "/data/local/tmp/focus_stage/tether_enforcer.sh"
|
||||
@ -493,6 +494,7 @@ do_deploy() {
|
||||
adb_root "cp /data/local/tmp/focus_stage/focus_ctl.sh $REMOTE_DIR/focus_ctl.sh"
|
||||
adb_root "cp /data/local/tmp/focus_stage/hosts_enforcer.sh $REMOTE_DIR/hosts_enforcer.sh"
|
||||
adb_root "cp /data/local/tmp/focus_stage/dns_enforcer.sh $REMOTE_DIR/dns_enforcer.sh"
|
||||
adb_root "cp /data/local/tmp/focus_stage/dns_iptables.sh $REMOTE_DIR/dns_iptables.sh"
|
||||
adb_root "cp /data/local/tmp/focus_stage/launcher_enforcer.sh $REMOTE_DIR/launcher_enforcer.sh"
|
||||
adb_root "cp /data/local/tmp/focus_stage/curfew_enforcer.sh $REMOTE_DIR/curfew_enforcer.sh"
|
||||
adb_root "cp /data/local/tmp/focus_stage/tether_enforcer.sh $REMOTE_DIR/tether_enforcer.sh"
|
||||
|
||||
@ -28,6 +28,12 @@
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
# shellcheck source=config.sh
|
||||
. "$SCRIPT_DIR/config.sh"
|
||||
# Sourced after config.sh because the chain functions read its DNS_* values.
|
||||
# They also call log(), defined below; that is fine because nothing in either
|
||||
# file runs until main() does. A sibling, not a lib/ member: lib/ does not
|
||||
# exist on the phone, and deploy.sh copies these scripts flat into $REMOTE_DIR.
|
||||
# shellcheck source=dns_iptables.sh
|
||||
. "$SCRIPT_DIR/dns_iptables.sh"
|
||||
|
||||
PIDFILE="$STATE_DIR/dns_enforcer.pid"
|
||||
|
||||
@ -124,113 +130,6 @@ ensure_private_dns_pinned() {
|
||||
|
||||
# ---- iptables chain management ----
|
||||
|
||||
ensure_chain() {
|
||||
local ipt="$1"
|
||||
# Create the chain if missing.
|
||||
if ! "$ipt" -L "$DNS_IPT_CHAIN" >/dev/null 2>&1; then
|
||||
"$ipt" -N "$DNS_IPT_CHAIN" 2>/dev/null || {
|
||||
log "ERROR: could not create $ipt chain $DNS_IPT_CHAIN"
|
||||
return 1
|
||||
}
|
||||
log "Created $ipt chain $DNS_IPT_CHAIN"
|
||||
fi
|
||||
# Remove ALL existing OUTPUT -> chain jumps (handles duplicates from
|
||||
# previous iptables lock races where -C returned error but -I succeeded).
|
||||
local removed=0
|
||||
while "$ipt" -D OUTPUT -j "$DNS_IPT_CHAIN" 2>/dev/null; do
|
||||
removed=$((removed + 1))
|
||||
done
|
||||
# Insert exactly one jump at position 1 of OUTPUT.
|
||||
if "$ipt" -I OUTPUT 1 -j "$DNS_IPT_CHAIN" 2>/dev/null; then
|
||||
if [ "$removed" -gt 1 ]; then
|
||||
log "De-duped $removed -> 1 OUTPUT jump for $ipt chain $DNS_IPT_CHAIN"
|
||||
fi
|
||||
else
|
||||
log "ERROR: could not insert OUTPUT -> $DNS_IPT_CHAIN for $ipt"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
trusted_dot_ips() {
|
||||
# Echo the trusted resolver's addresses of the given family, one per line.
|
||||
# $1 is "4" or "6". Literals come from DNS_TRUSTED_DOT_IPS; anything else
|
||||
# is ignored, because resolving the resolver's own name here would be
|
||||
# circular once Private DNS is pinned to it.
|
||||
local want="$1" ip
|
||||
[ -z "${DNS_TRUSTED_DOT_HOST:-}" ] && return 0
|
||||
for ip in ${DNS_TRUSTED_DOT_IPS:-}; do
|
||||
[ -z "$ip" ] && continue
|
||||
[ "${ip#\#}" != "$ip" ] && continue
|
||||
case "$ip" in
|
||||
*:*) [ "$want" = "6" ] && printf '%s\n' "$ip" ;;
|
||||
*) [ "$want" = "4" ] && printf '%s\n' "$ip" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
fill_chain_v4() {
|
||||
# Flush and refill so we always converge to the intended rule set.
|
||||
iptables -F "$DNS_IPT_CHAIN" 2>/dev/null || return 1
|
||||
# Allow the one DoT resolver we trust, BEFORE the blanket reject below.
|
||||
# Order matters: iptables takes the first match, so an ACCEPT appended
|
||||
# after the REJECT would never be reached.
|
||||
local trusted
|
||||
for trusted in $(trusted_dot_ips 4); do
|
||||
iptables -A "$DNS_IPT_CHAIN" -d "$trusted" -p tcp --dport 853 \
|
||||
-j ACCEPT 2>/dev/null || true
|
||||
done
|
||||
# Drop DoT everywhere else. This is a narrow port rule - there's no legit
|
||||
# reason for arbitrary apps to talk 853/tcp on Android.
|
||||
iptables -A "$DNS_IPT_CHAIN" -p tcp --dport 853 -j REJECT \
|
||||
--reject-with tcp-reset 2>/dev/null || true
|
||||
iptables -A "$DNS_IPT_CHAIN" -p udp --dport 853 -j REJECT \
|
||||
--reject-with icmp-port-unreachable 2>/dev/null || true
|
||||
|
||||
local ip
|
||||
for ip in $DNS_DOH_IPV4; do
|
||||
[ -z "$ip" ] && continue
|
||||
[ "${ip#\#}" != "$ip" ] && continue
|
||||
# Reject 443/tcp (DoH) and 53 (classic DNS) to well-known resolvers.
|
||||
# We also block 53 so apps that try to talk to 1.1.1.1:53 directly
|
||||
# (ignoring /etc/resolv.conf) still fall back to the system resolver.
|
||||
iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 443 -j REJECT \
|
||||
--reject-with tcp-reset 2>/dev/null || true
|
||||
iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 443 -j REJECT \
|
||||
--reject-with icmp-port-unreachable 2>/dev/null || true
|
||||
iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 53 -j REJECT \
|
||||
--reject-with icmp-port-unreachable 2>/dev/null || true
|
||||
iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 53 -j REJECT \
|
||||
--reject-with tcp-reset 2>/dev/null || true
|
||||
done
|
||||
}
|
||||
|
||||
fill_chain_v6() {
|
||||
ip6tables -F "$DNS_IPT_CHAIN" 2>/dev/null || return 1
|
||||
local trusted
|
||||
for trusted in $(trusted_dot_ips 6); do
|
||||
ip6tables -A "$DNS_IPT_CHAIN" -d "$trusted" -p tcp --dport 853 \
|
||||
-j ACCEPT 2>/dev/null || true
|
||||
done
|
||||
ip6tables -A "$DNS_IPT_CHAIN" -p tcp --dport 853 -j REJECT \
|
||||
--reject-with tcp-reset 2>/dev/null || true
|
||||
ip6tables -A "$DNS_IPT_CHAIN" -p udp --dport 853 -j REJECT \
|
||||
--reject-with icmp6-port-unreachable 2>/dev/null || true
|
||||
|
||||
local ip
|
||||
for ip in $DNS_DOH_IPV6; do
|
||||
[ -z "$ip" ] && continue
|
||||
[ "${ip#\#}" != "$ip" ] && continue
|
||||
ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 443 -j REJECT \
|
||||
--reject-with tcp-reset 2>/dev/null || true
|
||||
ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 443 -j REJECT \
|
||||
--reject-with icmp6-port-unreachable 2>/dev/null || true
|
||||
ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 53 -j REJECT \
|
||||
--reject-with icmp6-port-unreachable 2>/dev/null || true
|
||||
ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 53 -j REJECT \
|
||||
--reject-with tcp-reset 2>/dev/null || true
|
||||
done
|
||||
}
|
||||
|
||||
# ---- State-change gate ----
|
||||
#
|
||||
# fill_chain_v4/v6 flush and reinsert ~50-100 iptables/ip6tables rules each
|
||||
@ -242,59 +141,6 @@ fill_chain_v6() {
|
||||
# Self-healing behavior described in the header comment is preserved; only
|
||||
# redundant identical rebuilds are skipped.
|
||||
|
||||
expected_rule_count() {
|
||||
# 2 fixed DoT rules (tcp+udp/853) + 4 rules per configured DoH/DNS IP,
|
||||
# plus 1 ACCEPT per trusted-resolver address of this family. $1 is the
|
||||
# family ("4"/"6"); the remaining arguments are the DoH/DNS IPs.
|
||||
#
|
||||
# This has to match fill_chain_* exactly. chain_intact() compares the
|
||||
# live rule count against this number and rebuilds on any mismatch, so
|
||||
# an undercount here means a rebuild every DNS_CHECK_INTERVAL seconds --
|
||||
# a silent fork storm that also drops the connection each time.
|
||||
local family="$1"
|
||||
shift
|
||||
local n=2 ip
|
||||
for ip in "$@"; do
|
||||
[ -z "$ip" ] && continue
|
||||
[ "${ip#\#}" != "$ip" ] && continue
|
||||
n=$((n + 4))
|
||||
done
|
||||
for ip in $(trusted_dot_ips "$family"); do
|
||||
n=$((n + 1))
|
||||
done
|
||||
echo "$n"
|
||||
}
|
||||
|
||||
chain_intact() {
|
||||
local ipt="$1" expected="$2" actual
|
||||
"$ipt" -C OUTPUT -j "$DNS_IPT_CHAIN" >/dev/null 2>&1 || return 1
|
||||
actual="$("$ipt" -S "$DNS_IPT_CHAIN" 2>/dev/null | grep -c '^-A')"
|
||||
[ "$actual" = "$expected" ]
|
||||
}
|
||||
|
||||
enforce_iptables() {
|
||||
if command -v iptables >/dev/null 2>&1; then
|
||||
# Intentional word split: expected_rule_count() iterates "$@" over the
|
||||
# configured IP list. This runs under /system/bin/sh on the phone, where
|
||||
# arrays do not exist, so an unquoted expansion is the POSIX way to do it.
|
||||
# shellcheck disable=SC2086
|
||||
if chain_intact iptables "$(expected_rule_count 4 $DNS_DOH_IPV4)"; then
|
||||
:
|
||||
elif ensure_chain iptables && fill_chain_v4; then
|
||||
log "iptables (v4) DNS chain rebuilt (was missing/tampered)"
|
||||
fi
|
||||
fi
|
||||
if command -v ip6tables >/dev/null 2>&1; then
|
||||
# Intentional word split, as above.
|
||||
# shellcheck disable=SC2086
|
||||
if chain_intact ip6tables "$(expected_rule_count 6 $DNS_DOH_IPV6)"; then
|
||||
:
|
||||
elif ensure_chain ip6tables && fill_chain_v6; then
|
||||
log "ip6tables (v6) DNS chain rebuilt (was missing/tampered)"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
# We intentionally leave the iptables chain in place on SIGTERM so
|
||||
# stopping the enforcer for maintenance does not immediately re-open
|
||||
|
||||
170
phone_focus_mode/dns_iptables.sh
Executable file
170
phone_focus_mode/dns_iptables.sh
Executable file
@ -0,0 +1,170 @@
|
||||
#!/system/bin/sh
|
||||
# shellcheck shell=ash
|
||||
# ============================================================
|
||||
# dns_iptables.sh — the iptables chain half of dns_enforcer.sh.
|
||||
#
|
||||
# Builds and maintains the DNS_IPT_CHAIN block chain: creating it and
|
||||
# pinning a single OUTPUT jump, filling it for IPv4 and IPv6, computing
|
||||
# the rule count that proves it is complete, and re-asserting it when it
|
||||
# has drifted.
|
||||
#
|
||||
# Sourced by dns_enforcer.sh after config.sh, whose DNS_* values these
|
||||
# functions read. They also call log(), which dns_enforcer.sh defines;
|
||||
# nothing here runs until main() calls it, so definition order is fine.
|
||||
# A sibling rather than a lib/ member: lib/ does not exist on the phone,
|
||||
# and deploy.sh copies these scripts flat into $REMOTE_DIR.
|
||||
# ============================================================
|
||||
|
||||
ensure_chain() {
|
||||
local ipt="$1"
|
||||
# Create the chain if missing.
|
||||
if ! "$ipt" -L "$DNS_IPT_CHAIN" >/dev/null 2>&1; then
|
||||
"$ipt" -N "$DNS_IPT_CHAIN" 2>/dev/null || {
|
||||
log "ERROR: could not create $ipt chain $DNS_IPT_CHAIN"
|
||||
return 1
|
||||
}
|
||||
log "Created $ipt chain $DNS_IPT_CHAIN"
|
||||
fi
|
||||
# Remove ALL existing OUTPUT -> chain jumps (handles duplicates from
|
||||
# previous iptables lock races where -C returned error but -I succeeded).
|
||||
local removed=0
|
||||
while "$ipt" -D OUTPUT -j "$DNS_IPT_CHAIN" 2>/dev/null; do
|
||||
removed=$((removed + 1))
|
||||
done
|
||||
# Insert exactly one jump at position 1 of OUTPUT.
|
||||
if "$ipt" -I OUTPUT 1 -j "$DNS_IPT_CHAIN" 2>/dev/null; then
|
||||
if [ "$removed" -gt 1 ]; then
|
||||
log "De-duped $removed -> 1 OUTPUT jump for $ipt chain $DNS_IPT_CHAIN"
|
||||
fi
|
||||
else
|
||||
log "ERROR: could not insert OUTPUT -> $DNS_IPT_CHAIN for $ipt"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
trusted_dot_ips() {
|
||||
# Echo the trusted resolver's addresses of the given family, one per line.
|
||||
# $1 is "4" or "6". Literals come from DNS_TRUSTED_DOT_IPS; anything else
|
||||
# is ignored, because resolving the resolver's own name here would be
|
||||
# circular once Private DNS is pinned to it.
|
||||
local want="$1" ip
|
||||
[ -z "${DNS_TRUSTED_DOT_HOST:-}" ] && return 0
|
||||
for ip in ${DNS_TRUSTED_DOT_IPS:-}; do
|
||||
[ -z "$ip" ] && continue
|
||||
[ "${ip#\#}" != "$ip" ] && continue
|
||||
case "$ip" in
|
||||
*:*) [ "$want" = "6" ] && printf '%s\n' "$ip" ;;
|
||||
*) [ "$want" = "4" ] && printf '%s\n' "$ip" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
fill_chain_v4() {
|
||||
# Flush and refill so we always converge to the intended rule set.
|
||||
iptables -F "$DNS_IPT_CHAIN" 2>/dev/null || return 1
|
||||
# Allow the one DoT resolver we trust, BEFORE the blanket reject below.
|
||||
# Order matters: iptables takes the first match, so an ACCEPT appended
|
||||
# after the REJECT would never be reached.
|
||||
local trusted
|
||||
for trusted in $(trusted_dot_ips 4); do
|
||||
iptables -A "$DNS_IPT_CHAIN" -d "$trusted" -p tcp --dport 853 \
|
||||
-j ACCEPT 2>/dev/null || true
|
||||
done
|
||||
# Drop DoT everywhere else. This is a narrow port rule - there's no legit
|
||||
# reason for arbitrary apps to talk 853/tcp on Android.
|
||||
iptables -A "$DNS_IPT_CHAIN" -p tcp --dport 853 -j REJECT \
|
||||
--reject-with tcp-reset 2>/dev/null || true
|
||||
iptables -A "$DNS_IPT_CHAIN" -p udp --dport 853 -j REJECT \
|
||||
--reject-with icmp-port-unreachable 2>/dev/null || true
|
||||
|
||||
local ip
|
||||
for ip in $DNS_DOH_IPV4; do
|
||||
[ -z "$ip" ] && continue
|
||||
[ "${ip#\#}" != "$ip" ] && continue
|
||||
# Reject 443/tcp (DoH) and 53 (classic DNS) to well-known resolvers.
|
||||
# We also block 53 so apps that try to talk to 1.1.1.1:53 directly
|
||||
# (ignoring /etc/resolv.conf) still fall back to the system resolver.
|
||||
iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 443 -j REJECT \
|
||||
--reject-with tcp-reset 2>/dev/null || true
|
||||
iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 443 -j REJECT \
|
||||
--reject-with icmp-port-unreachable 2>/dev/null || true
|
||||
iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 53 -j REJECT \
|
||||
--reject-with icmp-port-unreachable 2>/dev/null || true
|
||||
iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 53 -j REJECT \
|
||||
--reject-with tcp-reset 2>/dev/null || true
|
||||
done
|
||||
}
|
||||
fill_chain_v6() {
|
||||
ip6tables -F "$DNS_IPT_CHAIN" 2>/dev/null || return 1
|
||||
local trusted
|
||||
for trusted in $(trusted_dot_ips 6); do
|
||||
ip6tables -A "$DNS_IPT_CHAIN" -d "$trusted" -p tcp --dport 853 \
|
||||
-j ACCEPT 2>/dev/null || true
|
||||
done
|
||||
ip6tables -A "$DNS_IPT_CHAIN" -p tcp --dport 853 -j REJECT \
|
||||
--reject-with tcp-reset 2>/dev/null || true
|
||||
ip6tables -A "$DNS_IPT_CHAIN" -p udp --dport 853 -j REJECT \
|
||||
--reject-with icmp6-port-unreachable 2>/dev/null || true
|
||||
|
||||
local ip
|
||||
for ip in $DNS_DOH_IPV6; do
|
||||
[ -z "$ip" ] && continue
|
||||
[ "${ip#\#}" != "$ip" ] && continue
|
||||
ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 443 -j REJECT \
|
||||
--reject-with tcp-reset 2>/dev/null || true
|
||||
ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 443 -j REJECT \
|
||||
--reject-with icmp6-port-unreachable 2>/dev/null || true
|
||||
ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 53 -j REJECT \
|
||||
--reject-with icmp6-port-unreachable 2>/dev/null || true
|
||||
ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 53 -j REJECT \
|
||||
--reject-with tcp-reset 2>/dev/null || true
|
||||
done
|
||||
}
|
||||
expected_rule_count() {
|
||||
# 2 fixed DoT rules (tcp+udp/853) + 4 rules per configured DoH/DNS IP,
|
||||
# plus 1 ACCEPT per trusted-resolver address of this family. $1 is the
|
||||
# family ("4"/"6"); the remaining arguments are the DoH/DNS IPs.
|
||||
#
|
||||
# This has to match fill_chain_* exactly. chain_intact() compares the
|
||||
# live rule count against this number and rebuilds on any mismatch, so
|
||||
# an undercount here means a rebuild every DNS_CHECK_INTERVAL seconds --
|
||||
# a silent fork storm that also drops the connection each time.
|
||||
local family="$1"
|
||||
shift
|
||||
local n=2 ip
|
||||
for ip in "$@"; do
|
||||
[ -z "$ip" ] && continue
|
||||
[ "${ip#\#}" != "$ip" ] && continue
|
||||
n=$((n + 4))
|
||||
done
|
||||
for ip in $(trusted_dot_ips "$family"); do
|
||||
n=$((n + 1))
|
||||
done
|
||||
echo "$n"
|
||||
}
|
||||
chain_intact() {
|
||||
local ipt="$1" expected="$2" actual
|
||||
"$ipt" -C OUTPUT -j "$DNS_IPT_CHAIN" >/dev/null 2>&1 || return 1
|
||||
actual="$("$ipt" -S "$DNS_IPT_CHAIN" 2>/dev/null | grep -c '^-A')"
|
||||
[ "$actual" = "$expected" ]
|
||||
}
|
||||
enforce_iptables() {
|
||||
if command -v iptables >/dev/null 2>&1; then
|
||||
# Intentional word split: expected_rule_count() iterates "$@" over the
|
||||
# configured IP list. This runs under /system/bin/sh on the phone, where
|
||||
# arrays do not exist, so an unquoted expansion is the POSIX way to do it.
|
||||
# shellcheck disable=SC2086
|
||||
if chain_intact iptables "$(expected_rule_count 4 $DNS_DOH_IPV4)"; then
|
||||
:
|
||||
elif ensure_chain iptables && fill_chain_v4; then
|
||||
log "iptables (v4) DNS chain rebuilt (was missing/tampered)"
|
||||
fi
|
||||
fi
|
||||
if command -v ip6tables >/dev/null 2>&1; then
|
||||
# Intentional word split, as above.
|
||||
# shellcheck disable=SC2086
|
||||
if chain_intact ip6tables "$(expected_rule_count 6 $DNS_DOH_IPV6)"; then
|
||||
:
|
||||
elif ensure_chain ip6tables && fill_chain_v6; then
|
||||
log "ip6tables (v6) DNS chain rebuilt (was missing/tampered)"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
132
phone_focus_mode/lib/tests/dns_iptables_harness.sh
Executable file
132
phone_focus_mode/lib/tests/dns_iptables_harness.sh
Executable file
@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env bash
|
||||
# dns_iptables_harness.sh — staging and assertions shared by the
|
||||
# dns_iptables.sh test runners.
|
||||
#
|
||||
# Sourced, not executed. Stages dns_enforcer.sh + dns_iptables.sh into a temp
|
||||
# dir beside a fake config.sh, stubs iptables/ip6tables/settings, and exposes
|
||||
# _with_subject to run a snippet against the staged copy. The caller owns PASS
|
||||
# and FAIL and prints the tally.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly SCRIPT_DIR
|
||||
readonly PHONE_DIR="${SCRIPT_DIR}/../.."
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
_t_pass() {
|
||||
PASS=$((PASS + 1))
|
||||
printf ' OK: %s\n' "$1"
|
||||
}
|
||||
|
||||
_t_fail() {
|
||||
FAIL=$((FAIL + 1))
|
||||
printf ' FAIL: %s\n' "$1"
|
||||
}
|
||||
|
||||
# Assert two strings match, naming the expectation either way.
|
||||
_t_eq() {
|
||||
local want="$1" got="$2" what="$3"
|
||||
if [[ "$got" == "$want" ]]; then
|
||||
_t_pass "$what"
|
||||
else
|
||||
_t_fail "$what (want '${want}', got '${got}')"
|
||||
fi
|
||||
}
|
||||
|
||||
RUN="$(mktemp -d)"
|
||||
trap 'rm -rf "${RUN}"' EXIT
|
||||
mkdir -p "${RUN}/app" "${RUN}/bin" "${RUN}/state" "${RUN}/ipt"
|
||||
|
||||
cp "${PHONE_DIR}/dns_enforcer.sh" "${PHONE_DIR}/dns_iptables.sh" "${RUN}/app/"
|
||||
|
||||
# --- fake config.sh in the script's own dir (dns_enforcer sources a sibling) ---
|
||||
cat >"${RUN}/app/config.sh" <<EOF
|
||||
export STATE_DIR="${RUN}/state"
|
||||
export DNS_LOG="${RUN}/state/dns.log"
|
||||
export DNS_IPT_CHAIN="FOCUS_DNS_BLOCK"
|
||||
export DNS_CHECK_INTERVAL=1
|
||||
export DNS_DOH_IPV4="1.1.1.1 8.8.8.8"
|
||||
export DNS_DOH_IPV6="2606:4700:4700::1111"
|
||||
export DNS_TRUSTED_DOT_HOST="\${TEST_DOT_HOST:-}"
|
||||
export DNS_TRUSTED_DOT_IPS="\${TEST_DOT_IPS:-}"
|
||||
EOF
|
||||
|
||||
# --- iptables/ip6tables stub: a rule list per binary, plus the OUTPUT jump ---
|
||||
cat >"${RUN}/bin/iptables" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
# Models only what the functions under test inspect: a per-binary rule file
|
||||
# and a flag for the OUTPUT -> chain jump. Every call is logged so tests can
|
||||
# assert on ordering, which is what makes the ACCEPT-before-REJECT rule
|
||||
# checkable without a real firewall.
|
||||
bin="$(basename "$0")"
|
||||
store="${IPT_STATE}/${bin}"
|
||||
mkdir -p "$store"
|
||||
rules="${store}/rules"
|
||||
jump="${store}/jump"
|
||||
touch "$rules"
|
||||
printf '%s %s\n' "$bin" "$*" >>"${IPT_STATE}/calls.log"
|
||||
|
||||
case "$1" in
|
||||
-L) [[ -f "${store}/chain" ]] && exit 0 || exit 1 ;;
|
||||
-N) touch "${store}/chain"; exit 0 ;;
|
||||
-F) : >"$rules"; exit 0 ;;
|
||||
-A)
|
||||
shift
|
||||
printf -- '-A %s\n' "$*" >>"$rules"
|
||||
exit 0
|
||||
;;
|
||||
-I) touch "$jump"; exit 0 ;;
|
||||
-D)
|
||||
# Succeeds once per existing jump, so the de-dupe loop terminates.
|
||||
if [[ -f "$jump" ]]; then rm -f "$jump"; exit 0; fi
|
||||
exit 1
|
||||
;;
|
||||
-C) [[ -f "$jump" ]] && exit 0 || exit 1 ;;
|
||||
-S) cat "$rules"; exit 0 ;;
|
||||
esac
|
||||
exit 0
|
||||
STUB
|
||||
chmod +x "${RUN}/bin/iptables"
|
||||
cp "${RUN}/bin/iptables" "${RUN}/bin/ip6tables"
|
||||
|
||||
# `settings` is only reached via ensure_private_dns_*, which these tests do
|
||||
# not exercise; stubbed so an accidental call cannot hit a real binary.
|
||||
cat >"${RUN}/bin/settings" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
echo "null"
|
||||
STUB
|
||||
chmod +x "${RUN}/bin/settings"
|
||||
|
||||
# Build a sourceable copy: main() would otherwise loop forever. The pattern
|
||||
# uses a character class for the dollar so that no literal expansion appears
|
||||
# in this file, keeping the intent readable without a lint suppression.
|
||||
sed 's/^main "[$]@"$/: # main disabled under test/' \
|
||||
"${RUN}/app/dns_enforcer.sh" >"${RUN}/app/under_test.sh"
|
||||
|
||||
# Run a snippet with the subject sourced, under the stub PATH. Executed as a
|
||||
# subprocess rather than sourced here so that $0 — and therefore SCRIPT_DIR —
|
||||
# resolves to the app dir, which is how the script finds its siblings.
|
||||
#
|
||||
# The snippet is read from stdin rather than taken as an argument: it is code
|
||||
# for the *subject* to expand, so any $VAR in it must survive this file
|
||||
# untouched. A heredoc says that plainly, where a quoted argument would look
|
||||
# like an expansion bug and draw SC2016.
|
||||
_with_subject() {
|
||||
{
|
||||
cat <<'PRELUDE'
|
||||
#!/usr/bin/env bash
|
||||
. "$(cd "$(dirname "$0")" && pwd)/under_test.sh"
|
||||
PRELUDE
|
||||
cat
|
||||
} >"${RUN}/app/case.sh"
|
||||
PATH="${RUN}/bin:${PATH}" IPT_STATE="${RUN}/ipt" \
|
||||
TEST_DOT_HOST="${TEST_DOT_HOST:-}" TEST_DOT_IPS="${TEST_DOT_IPS:-}" \
|
||||
bash "${RUN}/app/case.sh"
|
||||
}
|
||||
|
||||
_reset_ipt() {
|
||||
rm -rf "${RUN}/ipt"
|
||||
mkdir -p "${RUN}/ipt"
|
||||
}
|
||||
@ -1,83 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Unit tests for dns_enforcer.sh helper functions.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
_t_pass() {
|
||||
PASS=$((PASS + 1))
|
||||
printf ' OK: %s\n' "$1"
|
||||
}
|
||||
|
||||
_t_fail() {
|
||||
FAIL=$((FAIL + 1))
|
||||
printf ' FAIL: %s\n' "$1"
|
||||
}
|
||||
|
||||
TMPDIR_TEST="$(mktemp -d)"
|
||||
trap 'rm -rf "${TMPDIR_TEST}"' EXIT
|
||||
|
||||
cat >"${TMPDIR_TEST}/config.sh" <<EOF
|
||||
#!/system/bin/sh
|
||||
export STATE_DIR="${TMPDIR_TEST}"
|
||||
export MODE_FILE="${TMPDIR_TEST}/current_mode.txt"
|
||||
export DNS_LOG="${TMPDIR_TEST}/dns.log"
|
||||
export DNS_IPT_CHAIN="FOCUS_DNS_BLOCK"
|
||||
export DNS_CHECK_INTERVAL=20
|
||||
export DNS_DOH_IPV4=""
|
||||
export DNS_DOH_IPV6=""
|
||||
export DNS_BLOCK_HOSTS=""
|
||||
export DNS_BLOCK_PACKAGES_ALWAYS=""
|
||||
export DNS_BLOCK_PACKAGES_FOCUS_ONLY=""
|
||||
EOF
|
||||
|
||||
export FOCUS_MODE_DNS_ENFORCER_TESTING=1
|
||||
export FOCUS_MODE_SCRIPT_DIR="${TMPDIR_TEST}"
|
||||
# shellcheck source=/dev/null
|
||||
source "${SCRIPT_DIR}/../../dns_enforcer.sh"
|
||||
|
||||
line_ipv4='Ping youtube.com (142.251.98.190): 56(84) bytes.'
|
||||
line_ipv6='Ping youtube.com (2a00:1450:4025:800::5b): 56(84) bytes.'
|
||||
|
||||
if [[ "$(extract_ping_ip "$line_ipv4")" == "142.251.98.190" ]]; then
|
||||
_t_pass "extract_ping_ip parses IPv4"
|
||||
else
|
||||
_t_fail "extract_ping_ip failed for IPv4"
|
||||
fi
|
||||
|
||||
if [[ "$(extract_ping_ip "$line_ipv6")" == "2a00:1450:4025:800::5b" ]]; then
|
||||
_t_pass "extract_ping_ip parses IPv6"
|
||||
else
|
||||
_t_fail "extract_ping_ip failed for IPv6"
|
||||
fi
|
||||
|
||||
pkg_line='package:com.android.chrome uid:10153'
|
||||
if [[ "$(extract_package_uid "$pkg_line")" == "10153" ]]; then
|
||||
_t_pass "extract_package_uid parses uid from package line"
|
||||
else
|
||||
_t_fail "extract_package_uid failed for package line"
|
||||
fi
|
||||
|
||||
bad_pkg_line='package:com.android.chrome'
|
||||
if [[ -z "$(extract_package_uid "$bad_pkg_line")" ]]; then
|
||||
_t_pass "extract_package_uid returns empty when uid is missing"
|
||||
else
|
||||
_t_fail "extract_package_uid should return empty when uid missing"
|
||||
fi
|
||||
|
||||
block_file="${TMPDIR_TEST}/block.txt"
|
||||
: >"$block_file"
|
||||
append_unique_line "$block_file" "1.2.3.4"
|
||||
append_unique_line "$block_file" "1.2.3.4"
|
||||
append_unique_line "$block_file" "5.6.7.8"
|
||||
|
||||
if [[ "$(wc -l < "$block_file" | tr -d ' ')" == "2" ]]; then
|
||||
_t_pass "append_unique_line de-duplicates values"
|
||||
else
|
||||
_t_fail "append_unique_line should avoid duplicates"
|
||||
fi
|
||||
|
||||
printf '\nResults: %d passed, %d failed\n' "$PASS" "$FAIL"
|
||||
[[ "$FAIL" -eq 0 ]]
|
||||
197
phone_focus_mode/lib/tests/test_dns_iptables.sh
Executable file
197
phone_focus_mode/lib/tests/test_dns_iptables.sh
Executable file
@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env bash
|
||||
# Unit tests for dns_iptables.sh — the chain half of dns_enforcer.sh.
|
||||
#
|
||||
# The predecessor of this file, test_dns_enforcer.sh, asserted against three
|
||||
# functions that dns_enforcer.sh has never defined, so it never ran a single
|
||||
# assertion. It was deleted rather than repaired.
|
||||
#
|
||||
# Staging, stubs and assertion helpers live in dns_iptables_harness.sh.
|
||||
set -euo pipefail
|
||||
|
||||
_TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=dns_iptables_harness.sh
|
||||
. "${_TEST_DIR}/dns_iptables_harness.sh"
|
||||
|
||||
printf '\nexpected_rule_count\n'
|
||||
|
||||
_reset_ipt
|
||||
# 2 fixed DoT rules + 4 per DoH IP; no trusted resolver configured.
|
||||
_t_eq "10" "$(_with_subject <<'CASE'
|
||||
expected_rule_count 4 1.1.1.1 8.8.8.8
|
||||
CASE
|
||||
)" \
|
||||
"counts 2 fixed + 4 per DoH IP"
|
||||
|
||||
_reset_ipt
|
||||
_t_eq "2" "$(_with_subject <<'CASE'
|
||||
expected_rule_count 4
|
||||
CASE
|
||||
)" \
|
||||
"counts only the 2 fixed DoT rules when no IPs are given"
|
||||
|
||||
_reset_ipt
|
||||
# A commented-out entry must not be counted, or chain_intact never converges.
|
||||
_t_eq "6" "$(_with_subject <<'CASE'
|
||||
expected_rule_count 4 1.1.1.1 "#8.8.8.8"
|
||||
CASE
|
||||
)" \
|
||||
"skips a commented-out DoH IP"
|
||||
|
||||
_reset_ipt
|
||||
_t_eq "6" "$(_with_subject <<'CASE'
|
||||
expected_rule_count 4 1.1.1.1 ""
|
||||
CASE
|
||||
)" \
|
||||
"skips an empty DoH IP"
|
||||
|
||||
_reset_ipt
|
||||
_t_eq "7" "$(TEST_DOT_HOST=dns.example.net TEST_DOT_IPS=9.9.9.9 \
|
||||
_with_subject <<'CASE'
|
||||
expected_rule_count 4 1.1.1.1
|
||||
CASE
|
||||
)" \
|
||||
"adds one ACCEPT per trusted v4 resolver address"
|
||||
|
||||
printf '\ntrusted_dot_ips\n'
|
||||
|
||||
_reset_ipt
|
||||
_t_eq "" "$(_with_subject <<'CASE'
|
||||
trusted_dot_ips 4
|
||||
CASE
|
||||
)" \
|
||||
"yields nothing when no trusted host is set"
|
||||
|
||||
_reset_ipt
|
||||
_t_eq "9.9.9.9" "$(TEST_DOT_HOST=dns.example.net TEST_DOT_IPS='9.9.9.9 2620:fe::9' \
|
||||
_with_subject <<'CASE'
|
||||
trusted_dot_ips 4
|
||||
CASE
|
||||
)" \
|
||||
"selects only the v4 address for family 4"
|
||||
|
||||
_reset_ipt
|
||||
_t_eq "2620:fe::9" "$(TEST_DOT_HOST=dns.example.net TEST_DOT_IPS='9.9.9.9 2620:fe::9' \
|
||||
_with_subject <<'CASE'
|
||||
trusted_dot_ips 6
|
||||
CASE
|
||||
)" \
|
||||
"selects only the v6 address for family 6"
|
||||
|
||||
_reset_ipt
|
||||
_t_eq "" "$(TEST_DOT_HOST=dns.example.net TEST_DOT_IPS='#9.9.9.9' \
|
||||
_with_subject <<'CASE'
|
||||
trusted_dot_ips 4
|
||||
CASE
|
||||
)" \
|
||||
"skips a commented-out trusted address"
|
||||
|
||||
printf '\nensure_chain\n'
|
||||
|
||||
_reset_ipt
|
||||
_t_eq "0" "$(_with_subject <<'CASE'
|
||||
ensure_chain iptables >/dev/null 2>&1; echo $?
|
||||
CASE
|
||||
)" \
|
||||
"creates the chain and inserts the OUTPUT jump"
|
||||
|
||||
_reset_ipt
|
||||
_t_eq "0" "$(_with_subject <<'CASE'
|
||||
ensure_chain iptables >/dev/null 2>&1
|
||||
ensure_chain iptables >/dev/null 2>&1; echo $?
|
||||
CASE
|
||||
)" \
|
||||
"is idempotent across repeated calls"
|
||||
|
||||
printf '\nfill_chain_v4 / fill_chain_v6\n'
|
||||
|
||||
_reset_ipt
|
||||
_t_eq "10" "$(_with_subject <<'CASE'
|
||||
ensure_chain iptables >/dev/null 2>&1
|
||||
fill_chain_v4 >/dev/null 2>&1
|
||||
iptables -S "$DNS_IPT_CHAIN" | grep -c "^-A"
|
||||
CASE
|
||||
)" \
|
||||
"fills v4 to exactly expected_rule_count rules"
|
||||
|
||||
_reset_ipt
|
||||
# The ACCEPT for the trusted resolver must precede the blanket REJECT, or
|
||||
# iptables' first-match wins and the allow rule is dead.
|
||||
_t_eq "ACCEPT" "$(TEST_DOT_HOST=dns.example.net TEST_DOT_IPS=9.9.9.9 \
|
||||
_with_subject <<'CASE'
|
||||
ensure_chain iptables >/dev/null 2>&1
|
||||
fill_chain_v4 >/dev/null 2>&1
|
||||
iptables -S "$DNS_IPT_CHAIN" | head -1 | grep -o "ACCEPT\|REJECT"
|
||||
CASE
|
||||
)" \
|
||||
"puts the trusted-resolver ACCEPT before the blanket REJECT"
|
||||
|
||||
_reset_ipt
|
||||
_t_eq "6" "$(_with_subject <<'CASE'
|
||||
ensure_chain ip6tables >/dev/null 2>&1
|
||||
fill_chain_v6 >/dev/null 2>&1
|
||||
ip6tables -S "$DNS_IPT_CHAIN" | grep -c "^-A"
|
||||
CASE
|
||||
)" \
|
||||
"fills v6 to 2 fixed + 4 for the single DoH v6 IP"
|
||||
|
||||
printf '\nchain_intact\n'
|
||||
|
||||
_reset_ipt
|
||||
_t_eq "1" "$(_with_subject <<'CASE'
|
||||
chain_intact iptables 10 >/dev/null 2>&1; echo $?
|
||||
CASE
|
||||
)" \
|
||||
"reports drift when the OUTPUT jump is missing"
|
||||
|
||||
_reset_ipt
|
||||
_t_eq "0" "$(_with_subject <<'CASE'
|
||||
ensure_chain iptables >/dev/null 2>&1
|
||||
fill_chain_v4 >/dev/null 2>&1
|
||||
chain_intact iptables 10 >/dev/null 2>&1; echo $?
|
||||
CASE
|
||||
)" \
|
||||
"reports intact when jump and rule count both match"
|
||||
|
||||
_reset_ipt
|
||||
_t_eq "1" "$(_with_subject <<'CASE'
|
||||
ensure_chain iptables >/dev/null 2>&1
|
||||
fill_chain_v4 >/dev/null 2>&1
|
||||
chain_intact iptables 99 >/dev/null 2>&1; echo $?
|
||||
CASE
|
||||
)" \
|
||||
"reports drift when the rule count disagrees"
|
||||
|
||||
printf '\nenforce_iptables\n'
|
||||
|
||||
_reset_ipt
|
||||
# The whole point of the module: one call brings both families to a state
|
||||
# that chain_intact then accepts, so the daemon settles instead of rebuilding.
|
||||
_t_eq "0" "$(_with_subject <<'CASE'
|
||||
enforce_iptables >/dev/null 2>&1
|
||||
chain_intact iptables "$(expected_rule_count 4 $DNS_DOH_IPV4)" \
|
||||
>/dev/null 2>&1; echo $?
|
||||
CASE
|
||||
)" \
|
||||
"converges v4 to a state chain_intact accepts"
|
||||
|
||||
_reset_ipt
|
||||
_t_eq "0" "$(_with_subject <<'CASE'
|
||||
enforce_iptables >/dev/null 2>&1
|
||||
chain_intact ip6tables "$(expected_rule_count 6 $DNS_DOH_IPV6)" \
|
||||
>/dev/null 2>&1; echo $?
|
||||
CASE
|
||||
)" \
|
||||
"converges v6 to a state chain_intact accepts"
|
||||
|
||||
_reset_ipt
|
||||
# Second call must be a no-op at the rule level, not a rebuild.
|
||||
_t_eq "10" "$(_with_subject <<'CASE'
|
||||
enforce_iptables >/dev/null 2>&1
|
||||
enforce_iptables >/dev/null 2>&1
|
||||
iptables -S "$DNS_IPT_CHAIN" | grep -c "^-A"
|
||||
CASE
|
||||
)" \
|
||||
"stays at the same rule count when run twice"
|
||||
|
||||
printf '\nResults: %d passed, %d failed\n' "$PASS" "$FAIL"
|
||||
[[ "$FAIL" -eq 0 ]]
|
||||
Loading…
Reference in New Issue
Block a user