mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 18:07:06 +01:00
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
171 lines
6.7 KiB
Bash
Executable File
171 lines
6.7 KiB
Bash
Executable File
#!/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
|
|
}
|