2026-06-13 16:48:38 +02:00
|
|
|
#!/system/bin/sh
|
|
|
|
|
# shellcheck shell=ash
|
|
|
|
|
# ============================================================
|
|
|
|
|
# Night-curfew enforcer for rooted Android.
|
|
|
|
|
#
|
|
|
|
|
# Companion to focus_daemon.sh. The daemon handles the APP layer (disabling
|
|
|
|
|
# everything not in $NIGHT_WHITELIST while the curfew window is open at home).
|
|
|
|
|
# This enforcer adds the three "make the phone boring + unreachable" layers and
|
|
|
|
|
# keeps them locked by re-applying every $CURFEW_ENFORCER_INTERVAL seconds:
|
|
|
|
|
#
|
|
|
|
|
# 1. Grayscale - force the display monochrome via the accessibility
|
|
|
|
|
# daltonizer. The single biggest behavioural deterrent.
|
|
|
|
|
# 2. DND - force Do-Not-Disturb to alarms-only so notifications stop
|
|
|
|
|
# pulling you back in, while the morning alarm still rings.
|
|
|
|
|
# 3. Net curfew - (default OFF) per-UID iptables allow-list: only the
|
|
|
|
|
# $NIGHT_WHITELIST app UIDs (plus root/system/shell + DNS)
|
|
|
|
|
# get network; every other app is cut off.
|
|
|
|
|
#
|
|
|
|
|
# "Locked" = snap-back: a manual toggle in Settings is reverted within one
|
|
|
|
|
# interval. True impossibility would require blocking the Settings app, which
|
|
|
|
|
# risks system instability, so we deliberately do not.
|
|
|
|
|
#
|
|
|
|
|
# Acts ONLY while curfew is active (time window or forced) AND, for the
|
|
|
|
|
# non-forced case, while focus mode is ON (i.e. you are at home). On the
|
|
|
|
|
# transition back to day it restores the snapshotted display/DND state and
|
|
|
|
|
# tears the iptables chain down, so daytime is left exactly as it was.
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
|
|
# shellcheck source=config.sh
|
|
|
|
|
. "$SCRIPT_DIR/config.sh"
|
|
|
|
|
|
|
|
|
|
PIDFILE="$STATE_DIR/curfew_enforcer.pid"
|
|
|
|
|
# Snapshot of the user's pre-curfew display/DND state, captured on entry and
|
|
|
|
|
# restored on exit so we never clobber settings we did not set.
|
|
|
|
|
GRAYSCALE_SNAP="$STATE_DIR/curfew_grayscale.snap"
|
|
|
|
|
|
|
|
|
|
mkdir -p "$STATE_DIR"
|
|
|
|
|
touch "$CURFEW_ENFORCER_LOG"
|
|
|
|
|
chmod 666 "$CURFEW_ENFORCER_LOG" 2>/dev/null || true
|
|
|
|
|
|
|
|
|
|
log() {
|
|
|
|
|
local ts
|
|
|
|
|
ts="$(date '+%Y-%m-%d %H:%M:%S')"
|
|
|
|
|
echo "[$ts] $1" >> "$CURFEW_ENFORCER_LOG"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rotate_log() {
|
|
|
|
|
local lines
|
|
|
|
|
lines="$(wc -l < "$CURFEW_ENFORCER_LOG" 2>/dev/null || echo 0)"
|
|
|
|
|
if [ "$lines" -gt 500 ]; then
|
|
|
|
|
local tmp="$CURFEW_ENFORCER_LOG.tmp"
|
|
|
|
|
tail -n 500 "$CURFEW_ENFORCER_LOG" > "$tmp"
|
|
|
|
|
mv "$tmp" "$CURFEW_ENFORCER_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 "curfew_enforcer"; then
|
|
|
|
|
echo "curfew_enforcer already running (PID $old_pid)"
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
rm -f "$PIDFILE"
|
|
|
|
|
fi
|
|
|
|
|
echo $$ > "$PIDFILE"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ---- Time / activation (mirrors focus_daemon.sh::curfew_active) ----
|
|
|
|
|
|
|
|
|
|
_dec() {
|
|
|
|
|
# Strip leading zeros so a zero-padded HHMM ("0500", "0830") is not parsed
|
|
|
|
|
# as (sometimes invalid) octal by the shell's arithmetic. Keeps one digit.
|
|
|
|
|
local n="$1"
|
|
|
|
|
while [ "${n#0}" != "$n" ] && [ "${#n}" -gt 1 ]; do n="${n#0}"; done
|
|
|
|
|
printf '%s' "$n"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
is_curfew_now() {
|
|
|
|
|
local now start end
|
|
|
|
|
now="$(date +%H%M 2>/dev/null)"
|
|
|
|
|
case "$now" in
|
|
|
|
|
''|*[!0-9]*) return 1 ;;
|
|
|
|
|
esac
|
|
|
|
|
now="$(_dec "$now")"; start="$(_dec "$NIGHT_CURFEW_START")"; end="$(_dec "$NIGHT_CURFEW_END")"
|
|
|
|
|
if [ "$start" -le "$end" ]; then
|
|
|
|
|
[ "$now" -ge "$start" ] && [ "$now" -lt "$end" ]
|
|
|
|
|
else
|
|
|
|
|
[ "$now" -ge "$start" ] || [ "$now" -lt "$end" ]
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
at_home() {
|
|
|
|
|
[ -f "$MODE_FILE" ] && [ "$(cat "$MODE_FILE" 2>/dev/null)" = "focus" ]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Whether this enforcer should be applying its restrictions right now.
|
|
|
|
|
should_act() {
|
|
|
|
|
[ "${NIGHT_CURFEW_ENABLED:-0}" = "1" ] || return 1
|
|
|
|
|
[ -e "$CURFEW_OVERRIDE_FILE" ] && return 1
|
|
|
|
|
# Forced (test hook) bypasses both the clock and the home gate so the full
|
|
|
|
|
# stack can be validated during the day from anywhere.
|
|
|
|
|
[ -e "$CURFEW_FORCE_FILE" ] && return 0
|
|
|
|
|
is_curfew_now && at_home
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ---- Layer 1: grayscale ----
|
|
|
|
|
|
|
|
|
|
apply_grayscale() {
|
|
|
|
|
[ "${CURFEW_GRAYSCALE_ENABLED:-0}" = "1" ] || return 0
|
|
|
|
|
settings put secure accessibility_display_daltonizer_enabled 1 2>/dev/null || true
|
|
|
|
|
# Daltonizer "0" = full monochrome (grayscale).
|
|
|
|
|
settings put secure accessibility_display_daltonizer 0 2>/dev/null || true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
snapshot_grayscale() {
|
|
|
|
|
local en lv
|
|
|
|
|
en="$(settings get secure accessibility_display_daltonizer_enabled 2>/dev/null)"
|
|
|
|
|
lv="$(settings get secure accessibility_display_daltonizer 2>/dev/null)"
|
|
|
|
|
printf '%s\n%s\n' "${en:-0}" "${lv:-0}" > "$GRAYSCALE_SNAP" 2>/dev/null || true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
restore_grayscale() {
|
|
|
|
|
[ "${CURFEW_GRAYSCALE_ENABLED:-0}" = "1" ] || return 0
|
|
|
|
|
local en lv
|
|
|
|
|
if [ -f "$GRAYSCALE_SNAP" ]; then
|
|
|
|
|
en="$(sed -n '1p' "$GRAYSCALE_SNAP")"
|
|
|
|
|
lv="$(sed -n '2p' "$GRAYSCALE_SNAP")"
|
|
|
|
|
fi
|
|
|
|
|
# If the snapshot is missing or "null", default to disabled (the norm).
|
|
|
|
|
case "$en" in ''|null) en=0 ;; esac
|
|
|
|
|
case "$lv" in ''|null) lv=-1 ;; esac
|
|
|
|
|
settings put secure accessibility_display_daltonizer_enabled "$en" 2>/dev/null || true
|
|
|
|
|
[ "$lv" != "-1" ] && settings put secure accessibility_display_daltonizer "$lv" 2>/dev/null || true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ---- Layer 2: Do-Not-Disturb (alarms only) ----
|
|
|
|
|
|
|
|
|
|
apply_dnd() {
|
|
|
|
|
[ "${CURFEW_DND_ENABLED:-0}" = "1" ] || return 0
|
|
|
|
|
# alarms-only lets the morning alarm ring but silences everything else.
|
|
|
|
|
cmd notification set_dnd alarms >/dev/null 2>&1 || true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
restore_dnd() {
|
|
|
|
|
[ "${CURFEW_DND_ENABLED:-0}" = "1" ] || return 0
|
|
|
|
|
cmd notification set_dnd off >/dev/null 2>&1 || true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ---- Layer 3: per-UID network allow-list (default OFF) ----
|
|
|
|
|
|
|
|
|
|
# Resolve the UIDs of the night-whitelisted packages. Apps not installed are
|
|
|
|
|
# silently skipped. Output: one numeric UID per line.
|
|
|
|
|
night_uids() {
|
|
|
|
|
local plist="$STATE_DIR/night_whitelist.txt"
|
|
|
|
|
[ -f "$plist" ] || return 0
|
|
|
|
|
# `pm list packages -U` lines look like: "package:com.foo uid:10123"
|
|
|
|
|
local map="$STATE_DIR/uid_map.txt"
|
|
|
|
|
pm list packages -U 2>/dev/null \
|
|
|
|
|
| sed 's/^package://' > "$map"
|
|
|
|
|
while IFS= read -r pkg; do
|
|
|
|
|
[ -z "$pkg" ] && continue
|
|
|
|
|
awk -v p="$pkg" '$1 == p { sub(/uid:/,"",$2); print $2 }' "$map"
|
|
|
|
|
done < "$plist"
|
|
|
|
|
rm -f "$map"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ensure_net_chain() {
|
|
|
|
|
local ipt="$1"
|
2026-06-13 22:01:32 +02:00
|
|
|
if ! iptw "$ipt" -L "$CURFEW_NET_IPT_CHAIN" >/dev/null 2>&1; then
|
|
|
|
|
iptw "$ipt" -N "$CURFEW_NET_IPT_CHAIN" 2>/dev/null || return 1
|
2026-06-13 16:48:38 +02:00
|
|
|
fi
|
|
|
|
|
# De-dupe and pin exactly one OUTPUT jump at position 1.
|
2026-06-13 22:01:32 +02:00
|
|
|
while iptw "$ipt" -D OUTPUT -j "$CURFEW_NET_IPT_CHAIN" 2>/dev/null; do :; done
|
|
|
|
|
iptw "$ipt" -I OUTPUT 1 -j "$CURFEW_NET_IPT_CHAIN" 2>/dev/null || return 1
|
2026-06-13 16:48:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fill_net_chain() {
|
|
|
|
|
local ipt="$1" reject="$2"
|
2026-06-13 22:01:32 +02:00
|
|
|
iptw "$ipt" -F "$CURFEW_NET_IPT_CHAIN" 2>/dev/null || return 1
|
2026-06-13 16:48:38 +02:00
|
|
|
# Always-allowed plumbing: loopback, established flows, the OS itself, the
|
|
|
|
|
# daemon/ADB (root + shell), and DNS (apps resolve via netd, a different
|
|
|
|
|
# uid, so allow port 53 broadly or every lookup fails under the cut-off).
|
2026-06-13 22:01:32 +02:00
|
|
|
iptw "$ipt" -A "$CURFEW_NET_IPT_CHAIN" -o lo -j ACCEPT 2>/dev/null || true
|
|
|
|
|
iptw "$ipt" -A "$CURFEW_NET_IPT_CHAIN" -m state --state ESTABLISHED,RELATED -j ACCEPT 2>/dev/null || true
|
|
|
|
|
iptw "$ipt" -A "$CURFEW_NET_IPT_CHAIN" -m owner --uid-owner 0 -j ACCEPT 2>/dev/null || true
|
|
|
|
|
iptw "$ipt" -A "$CURFEW_NET_IPT_CHAIN" -m owner --uid-owner 1000 -j ACCEPT 2>/dev/null || true
|
|
|
|
|
iptw "$ipt" -A "$CURFEW_NET_IPT_CHAIN" -m owner --uid-owner 2000 -j ACCEPT 2>/dev/null || true
|
|
|
|
|
iptw "$ipt" -A "$CURFEW_NET_IPT_CHAIN" -p udp --dport 53 -j ACCEPT 2>/dev/null || true
|
|
|
|
|
iptw "$ipt" -A "$CURFEW_NET_IPT_CHAIN" -p tcp --dport 53 -j ACCEPT 2>/dev/null || true
|
|
|
|
|
# Allow each whitelisted app UID, read from the cache (refreshed once per
|
|
|
|
|
# main tick). Reading the cache instead of calling night_uids() here keeps
|
|
|
|
|
# the fast watchdog fork-free (no `pm list packages` on every rebuild).
|
2026-06-13 16:48:38 +02:00
|
|
|
local uid
|
2026-06-13 22:01:32 +02:00
|
|
|
if [ -f "$CURFEW_NET_UID_CACHE" ]; then
|
|
|
|
|
while IFS= read -r uid; do
|
|
|
|
|
case "$uid" in ''|*[!0-9]*) continue ;; esac
|
|
|
|
|
iptw "$ipt" -A "$CURFEW_NET_IPT_CHAIN" -m owner --uid-owner "$uid" -j ACCEPT 2>/dev/null || true
|
|
|
|
|
done < "$CURFEW_NET_UID_CACHE"
|
|
|
|
|
fi
|
2026-06-13 16:48:38 +02:00
|
|
|
# Cut off every remaining ordinary APP uid (10000-19999 = user-0 app range).
|
|
|
|
|
# Scoped to the app range so kernel/system sockets (no owner / low uids) are
|
|
|
|
|
# never touched — far safer than a blanket default-DROP.
|
2026-06-13 22:01:32 +02:00
|
|
|
iptw "$ipt" -A "$CURFEW_NET_IPT_CHAIN" -m owner --uid-owner 10000-19999 -j REJECT \
|
2026-06-13 16:48:38 +02:00
|
|
|
--reject-with "$reject" 2>/dev/null || true
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 22:01:32 +02:00
|
|
|
# Run iptables/ip6tables with a 2s xtables lock-wait. Android's netd runs its
|
|
|
|
|
# own concurrent `iptables-restore`; without -w our calls silently fail the
|
|
|
|
|
# instant netd holds the lock (proven on-device: partial 19-rule chains and
|
|
|
|
|
# multi-second outages). -w queues for the lock so our calls actually land.
|
|
|
|
|
# iptables 1.8.7 legacy supports it. $1 = binary (iptables/ip6tables).
|
|
|
|
|
iptw() {
|
|
|
|
|
local bin="$1"
|
|
|
|
|
shift
|
|
|
|
|
"$bin" -w 2 "$@"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Set to 1 once the net chain has been built in this process. Used purely to
|
|
|
|
|
# tell an *anomalous* mid-curfew disappearance (something outside this script
|
|
|
|
|
# deleted the chain) apart from the legitimate first build on curfew entry.
|
|
|
|
|
# Because it is a process-local var, a fresh enforcer starts empty and never
|
|
|
|
|
# false-flags its own initial build.
|
|
|
|
|
NET_BUILT=""
|
|
|
|
|
|
|
|
|
|
# Refresh the cached UID list (one `pm list packages -U` fork). Called once per
|
|
|
|
|
# main tick so the fast watchdog can rebuild from the cache without forking.
|
|
|
|
|
refresh_uid_cache() {
|
|
|
|
|
night_uids > "$CURFEW_NET_UID_CACHE.tmp" 2>/dev/null \
|
|
|
|
|
&& mv "$CURFEW_NET_UID_CACHE.tmp" "$CURFEW_NET_UID_CACHE" 2>/dev/null || true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Rebuild the chain from cache for whichever iptables variants exist. No pm fork.
|
|
|
|
|
rebuild_net_from_cache() {
|
|
|
|
|
if command -v iptables >/dev/null 2>&1; then
|
|
|
|
|
ensure_net_chain iptables && fill_net_chain iptables icmp-port-unreachable
|
|
|
|
|
fi
|
|
|
|
|
if command -v ip6tables >/dev/null 2>&1; then
|
|
|
|
|
ensure_net_chain ip6tables && fill_net_chain ip6tables icmp6-port-unreachable
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Fast watchdog: for `total` seconds, every CURFEW_NET_REASSERT_INTERVAL check
|
|
|
|
|
# whether netd wiped our chain and, if so, re-pin it from cache. Replaces the
|
|
|
|
|
# plain inter-tick sleep while curfew is active so the leak window drops from
|
|
|
|
|
# the full 5s tick to <=1s. Echoes the number of rebuilds it performed.
|
|
|
|
|
net_hold() {
|
|
|
|
|
local total="$1" elapsed=0 rebuilds=0 step="${CURFEW_NET_REASSERT_INTERVAL:-1}"
|
|
|
|
|
while [ "$elapsed" -lt "$total" ]; do
|
|
|
|
|
sleep "$step"
|
|
|
|
|
elapsed=$((elapsed + step))
|
|
|
|
|
if command -v iptables >/dev/null 2>&1 \
|
|
|
|
|
&& ! iptw iptables -L "$CURFEW_NET_IPT_CHAIN" >/dev/null 2>&1; then
|
|
|
|
|
rebuild_net_from_cache
|
|
|
|
|
rebuilds=$((rebuilds + 1))
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
echo "$rebuilds"
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 16:48:38 +02:00
|
|
|
apply_net() {
|
|
|
|
|
[ "${CURFEW_NET_ENABLED:-0}" = "1" ] || return 0
|
2026-06-13 22:01:32 +02:00
|
|
|
refresh_uid_cache
|
|
|
|
|
# Discriminating probe: if we already built the chain on a prior tick but it
|
|
|
|
|
# is gone now, an external actor wiped it (Android netd rewriting the filter
|
|
|
|
|
# table, or a manual flush during debugging). Log each disappearance so the
|
|
|
|
|
# live test reads "flush + self-heal" vs "dead process" directly, instead of
|
|
|
|
|
# inferring it from log silence.
|
|
|
|
|
if [ -n "$NET_BUILT" ] && command -v iptables >/dev/null 2>&1 \
|
|
|
|
|
&& ! iptables -L "$CURFEW_NET_IPT_CHAIN" >/dev/null 2>&1; then
|
|
|
|
|
log "net chain $CURFEW_NET_IPT_CHAIN vanished since last tick - rebuilding (external flush?)"
|
|
|
|
|
fi
|
2026-06-13 16:48:38 +02:00
|
|
|
if command -v iptables >/dev/null 2>&1; then
|
|
|
|
|
ensure_net_chain iptables && fill_net_chain iptables icmp-port-unreachable
|
|
|
|
|
fi
|
|
|
|
|
if command -v ip6tables >/dev/null 2>&1; then
|
|
|
|
|
ensure_net_chain ip6tables && fill_net_chain ip6tables icmp6-port-unreachable
|
|
|
|
|
fi
|
2026-06-13 22:01:32 +02:00
|
|
|
NET_BUILT=1
|
2026-06-13 16:48:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
teardown_net() {
|
|
|
|
|
local ipt
|
|
|
|
|
for ipt in iptables ip6tables; do
|
|
|
|
|
command -v "$ipt" >/dev/null 2>&1 || continue
|
2026-06-13 22:01:32 +02:00
|
|
|
while iptw "$ipt" -D OUTPUT -j "$CURFEW_NET_IPT_CHAIN" 2>/dev/null; do :; done
|
|
|
|
|
iptw "$ipt" -F "$CURFEW_NET_IPT_CHAIN" 2>/dev/null || true
|
|
|
|
|
iptw "$ipt" -X "$CURFEW_NET_IPT_CHAIN" 2>/dev/null || true
|
2026-06-13 16:48:38 +02:00
|
|
|
done
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ---- Apply / revert orchestration ----
|
|
|
|
|
|
|
|
|
|
enter_curfew() {
|
|
|
|
|
if [ ! -e "$CURFEW_ENFORCER_STATE" ]; then
|
|
|
|
|
snapshot_grayscale
|
|
|
|
|
: > "$CURFEW_ENFORCER_STATE"
|
|
|
|
|
log "Curfew ON - locking grayscale${CURFEW_DND_ENABLED:+ + DND}${CURFEW_NET_ENABLED:+ + net}"
|
|
|
|
|
fi
|
|
|
|
|
# Re-apply every tick so manual toggles snap back.
|
|
|
|
|
apply_grayscale
|
|
|
|
|
apply_dnd
|
|
|
|
|
apply_net
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
exit_curfew() {
|
|
|
|
|
[ -e "$CURFEW_ENFORCER_STATE" ] || return 0
|
|
|
|
|
restore_grayscale
|
|
|
|
|
restore_dnd
|
|
|
|
|
teardown_net
|
2026-06-13 22:01:32 +02:00
|
|
|
NET_BUILT=""
|
|
|
|
|
rm -f "$CURFEW_ENFORCER_STATE" "$CURFEW_NET_UID_CACHE"
|
2026-06-13 16:48:38 +02:00
|
|
|
log "Curfew OFF - restored display/DND, tore down net chain"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cleanup() {
|
|
|
|
|
# On a clean stop, leave the user back in daytime state.
|
|
|
|
|
log "curfew_enforcer shutting down - reverting"
|
|
|
|
|
exit_curfew
|
|
|
|
|
rm -f "$PIDFILE"
|
|
|
|
|
exit 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trap cleanup INT TERM
|
|
|
|
|
|
|
|
|
|
main() {
|
|
|
|
|
acquire_lock
|
|
|
|
|
log "curfew_enforcer started (PID=$$, window=${NIGHT_CURFEW_START}-${NIGHT_CURFEW_END}, net=${CURFEW_NET_ENABLED})"
|
2026-06-13 22:01:32 +02:00
|
|
|
local tick=0 act netstate rebuilds
|
2026-06-13 16:48:38 +02:00
|
|
|
while true; do
|
2026-06-13 22:01:32 +02:00
|
|
|
if should_act; then act=1; enter_curfew; else act=0; exit_curfew; fi
|
|
|
|
|
# Heartbeat every ~6 ticks (~30s): proves the loop is alive even when it
|
|
|
|
|
# is quietly re-applying. Without this, "alive but idle" and "dead" look
|
|
|
|
|
# identical in the log, so process death can't be inferred from silence.
|
|
|
|
|
tick=$((tick + 1))
|
|
|
|
|
if [ "$((tick % 6))" -eq 0 ]; then
|
|
|
|
|
if iptw iptables -L "$CURFEW_NET_IPT_CHAIN" >/dev/null 2>&1; then
|
|
|
|
|
netstate=up
|
|
|
|
|
else
|
|
|
|
|
netstate=down
|
|
|
|
|
fi
|
|
|
|
|
log "heartbeat tick=$tick act=$act net=$netstate"
|
2026-06-13 16:48:38 +02:00
|
|
|
fi
|
|
|
|
|
rotate_log
|
2026-06-13 22:01:32 +02:00
|
|
|
# While curfew is active with the net layer on, hold the chain pinned
|
|
|
|
|
# against netd's table rewrites for the whole interval (fast watchdog),
|
|
|
|
|
# instead of sleeping blind. Otherwise a plain sleep is enough.
|
|
|
|
|
if [ "$act" = 1 ] && [ "${CURFEW_NET_ENABLED:-0}" = "1" ]; then
|
|
|
|
|
rebuilds="$(net_hold "$CURFEW_ENFORCER_INTERVAL")"
|
|
|
|
|
[ "${rebuilds:-0}" -gt 0 ] 2>/dev/null \
|
|
|
|
|
&& log "net watchdog re-pinned chain ${rebuilds}x in last ${CURFEW_ENFORCER_INTERVAL}s (netd flush)"
|
|
|
|
|
else
|
|
|
|
|
sleep "$CURFEW_ENFORCER_INTERVAL"
|
|
|
|
|
fi
|
2026-06-13 16:48:38 +02:00
|
|
|
done
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
main "$@"
|