mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 19:07:04 +01:00
Some checks failed
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
billsplit CI / quality (push) Has been cancelled
reverse-survivors CI / check (push) Has been cancelled
The shellcheck hook ran with --severity=warning, so info- and style-level
findings never failed a commit. 222 had accumulated across 94 of 244 shell
files. They were not all style noise -- six were bugs:
- Five scripts in single_use/misc/testsAndMisc-bash/ sourced
$SCRIPT_DIR/../../lib/common.sh, one directory too shallow. Nothing exists
at that path, so all five failed at runtime.
- setup_personal_website.sh and setup_kcd2_dice_solver.sh called usage()
(which ends in `exit 0`) and then `exit 1` for an unknown command. The
exit 1 was unreachable, so a typo'd subcommand reported SUCCESS.
- Six i3blocks scripts had a `# shellcheck source=` directive missing the
scripts/periodic_background/ path segment. The stale path had silently
disabled source-following for all of them.
- diagnose_arch_performance.sh word-split a newline-joined file list; when
the find came back empty, awk got no file operands, fell through to
reading stdin, and the diagnostic hung.
- lib/common.sh's handle_arg_help_or_unknown ended in an unreachable
`return 0` while documenting "Returns: 0 if handled" -- a contract no code
path could satisfy.
- deploy.sh's SCRIPT_DIR is reassigned by config.sh, which it sources at top
level, and config.sh can repoint it at the on-device path
/data/local/tmp/focus_mode. Renamed to DEPLOY_DIR.
The SC1091 bulk (81 findings) was resolved rather than silenced: .shellcheckrc
sets external-sources + source-path=SCRIPTDIR so sourced files are actually
followed and analysed. That is what surfaced the i3blocks and deploy.sh bugs.
17 `# shellcheck disable=` directives remain, each with a written reason, and
only where the finding is provably wrong: injection-test payloads that must not
expand, literal source text being grepped for, POSIX sh scripts on the phone
where arrays do not exist, and source paths that only exist at runtime.
Everything else was fixed in code.
Verified in a CLEAN worktree, not just my own tree. config.sh sources the
gitignored config_secrets.sh, which exists locally but not in a fresh checkout;
a working-tree-only check passed while ci_mirror.sh -- which lints HEAD in a
clean worktree, exactly as CI does -- failed on it.
The gate is verified to bite: a file whose only finding is SC2016 (info) fails
the hook, while the old --severity=warning gate exits 0 on it.
third_party/ is excluded -- vendored upstream, matching the prettier hook.
Not fixed, reported: check_polling_antipatterns.sh carries a pre-existing
SC2034 disable that hides a real bug -- its `patterns` array is never used, so
the checker detects nothing. Confirmed unchanged by this commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
250 lines
8.2 KiB
Bash
Executable File
250 lines
8.2 KiB
Bash
Executable File
#!/system/bin/sh
|
|
# shellcheck shell=ash
|
|
# ============================================================
|
|
# DNS enforcer for rooted Android.
|
|
#
|
|
# Why this exists:
|
|
# /etc/hosts only works for lookups done by the *system* resolver
|
|
# using classic DNS (UDP/TCP 53). Two bypass channels defeat it:
|
|
# 1. DNS-over-TLS (DoT, port 853) - used by Android when Private
|
|
# DNS is "automatic" or set to a specific provider.
|
|
# 2. DNS-over-HTTPS (DoH, port 443) - used by Chrome/Brave's
|
|
# "Use secure DNS" feature and some apps directly.
|
|
#
|
|
# Strategy:
|
|
# 1. Force `settings global private_dns_mode off` so the OS stops
|
|
# doing DoT (there is no public DoT-by-hostname toggle).
|
|
# 2. Drop outbound traffic to a fixed list of well-known DoH/DoT
|
|
# endpoints via iptables / ip6tables so apps' fallback logic
|
|
# has to use the regular resolver, which consults /etc/hosts.
|
|
#
|
|
# Limitations:
|
|
# * A custom app that hardcodes an obscure DoH IP is not caught.
|
|
# * A root user can `iptables -F` or re-enable private DNS - but
|
|
# this loop re-asserts every $DNS_CHECK_INTERVAL seconds and
|
|
# leaves tamper logs.
|
|
# ============================================================
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
# shellcheck source=config.sh
|
|
. "$SCRIPT_DIR/config.sh"
|
|
|
|
PIDFILE="$STATE_DIR/dns_enforcer.pid"
|
|
|
|
mkdir -p "$STATE_DIR"
|
|
touch "$DNS_LOG"
|
|
chmod 666 "$DNS_LOG" 2>/dev/null || true
|
|
|
|
log() {
|
|
local ts
|
|
ts="$(date '+%Y-%m-%d %H:%M:%S')"
|
|
echo "[$ts] $1" >>"$DNS_LOG"
|
|
}
|
|
|
|
rotate_log() {
|
|
local lines
|
|
lines="$(wc -l <"$DNS_LOG" 2>/dev/null || echo 0)"
|
|
if [ "$lines" -gt 500 ]; then
|
|
local tmp="$DNS_LOG.tmp"
|
|
tail -n 500 "$DNS_LOG" >"$tmp"
|
|
mv "$tmp" "$DNS_LOG"
|
|
fi
|
|
}
|
|
|
|
acquire_lock() {
|
|
if [ -f "$PIDFILE" ]; then
|
|
local old_pid
|
|
old_pid="$(cat "$PIDFILE")"
|
|
if kill -0 "$old_pid" 2>/dev/null; then
|
|
local cmdline
|
|
cmdline="$(tr '\0' ' ' <"/proc/$old_pid/cmdline" 2>/dev/null)"
|
|
if echo "$cmdline" | grep -q "dns_enforcer"; then
|
|
echo "dns_enforcer already running (PID $old_pid)"
|
|
exit 0
|
|
fi
|
|
fi
|
|
rm -f "$PIDFILE"
|
|
fi
|
|
echo $$ >"$PIDFILE"
|
|
}
|
|
|
|
# ---- Private DNS ----
|
|
|
|
ensure_private_dns_off() {
|
|
local mode
|
|
mode="$(settings get global private_dns_mode 2>/dev/null)"
|
|
# Possible values: "off", "opportunistic", "hostname", null (default=opportunistic)
|
|
if [ "$mode" != "off" ]; then
|
|
settings put global private_dns_mode off 2>/dev/null
|
|
log "Private DNS was '$mode' - forced to 'off'"
|
|
fi
|
|
# Clear any pinned DoT hostname so the "hostname" mode cannot be
|
|
# re-enabled silently by Settings UI.
|
|
local spec
|
|
spec="$(settings get global private_dns_specifier 2>/dev/null)"
|
|
if [ -n "$spec" ] && [ "$spec" != "null" ]; then
|
|
settings delete global private_dns_specifier 2>/dev/null
|
|
log "Cleared private_dns_specifier (was '$spec')"
|
|
fi
|
|
}
|
|
|
|
# ---- 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
|
|
}
|
|
|
|
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
|
|
# Drop DoT everywhere. 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
|
|
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
|
|
# call. Running that unconditionally every $DNS_CHECK_INTERVAL was measured
|
|
# to peg netd at ~50% CPU on-device (contending the xtables lock) even
|
|
# though the rule set never changes at runtime. Only rebuild when the chain
|
|
# is actually missing, untethered from OUTPUT, or the wrong size (i.e.
|
|
# someone/something - netd, a root shell, a flush - tampered with it).
|
|
# 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.
|
|
local n=2 ip
|
|
for ip in "$@"; do
|
|
[ -z "$ip" ] && continue
|
|
[ "${ip#\#}" != "$ip" ] && continue
|
|
n=$((n + 4))
|
|
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 $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 $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
|
|
# the DoH hole. `focus_ctl.sh dns-stop` does the explicit teardown.
|
|
log "dns_enforcer shutting down"
|
|
rm -f "$PIDFILE"
|
|
exit 0
|
|
}
|
|
|
|
trap cleanup INT TERM
|
|
|
|
main() {
|
|
acquire_lock
|
|
log "dns_enforcer started (PID=$$)"
|
|
|
|
# Initial arm-up
|
|
ensure_private_dns_off
|
|
enforce_iptables
|
|
|
|
while true; do
|
|
ensure_private_dns_off
|
|
enforce_iptables
|
|
rotate_log
|
|
sleep "$DNS_CHECK_INTERVAL"
|
|
done
|
|
}
|
|
|
|
main "$@"
|