From c05843153d9af7403926acaaab8e092aff3af4d2 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sun, 16 Aug 2026 21:31:03 +0200 Subject: [PATCH] Split setup_dns_blocker.sh, a live systemd unit target 569 -> 211 across three libs. All seven heredocs sit inside functions, so function boundaries were safe seams. The script guards its entry with SETUP_DNS_BLOCKER_SKIP_MAIN, which made a real function-set comparison possible on a script that would otherwise reconfigure the machine when sourced: an identical 74 names against a detached worktree at HEAD. The dns-blocklist-refresh unit still resolves to it and it still parses. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY --- ...x-config-dns-blocker-split-2026-08-16.json | 17 + ...x-config-dns-blocker-split-2026-08-16.json | 37 ++ .../single_use/features/lib/dns_dhcp.sh | 188 +++++++++ .../single_use/features/lib/dns_firewall.sh | 86 ++++ .../single_use/features/lib/dns_units.sh | 109 +++++ .../single_use/features/setup_dns_blocker.sh | 372 +----------------- 6 files changed, 444 insertions(+), 365 deletions(-) create mode 100644 docs/superpowers/contracts/linux-config-dns-blocker-split-2026-08-16.json create mode 100644 docs/superpowers/evidence/linux-config-dns-blocker-split-2026-08-16.json create mode 100755 linux_configuration/scripts/single_use/features/lib/dns_dhcp.sh create mode 100755 linux_configuration/scripts/single_use/features/lib/dns_firewall.sh create mode 100755 linux_configuration/scripts/single_use/features/lib/dns_units.sh diff --git a/docs/superpowers/contracts/linux-config-dns-blocker-split-2026-08-16.json b/docs/superpowers/contracts/linux-config-dns-blocker-split-2026-08-16.json new file mode 100644 index 00000000..1bad40fd --- /dev/null +++ b/docs/superpowers/contracts/linux-config-dns-blocker-split-2026-08-16.json @@ -0,0 +1,17 @@ +{ + "title": "Split setup_dns_blocker.sh under the 250-line cap", + "objective": "Bring the DNS blocker setup script under the cap. It is the ExecStart of the live dns-blocklist-refresh.service unit, so the unit path and the script's exported function set must be unchanged.", + "acceptance_criteria": [ + "The entry script and all three new libs are at most 250 lines.", + "Sourcing the script yields exactly the same 74 function names as before, diffed against a detached worktree at HEAD. The script exposes SETUP_DNS_BLOCKER_SKIP_MAIN, so this can be checked without running main().", + "systemctl cat dns-blocklist-refresh.service still resolves to the script, and the script still parses.", + "Every seam falls on a function boundary, outside the seven heredocs.", + "The pre-commit shellcheck hook passes on every file, with no new suppression." + ], + "out_of_scope": [ + "Running main(); it installs dnsmasq, writes systemd units and changes firewall rules on the live machine.", + "Changing any generated config.", + "The other over-cap linux_configuration scripts." + ], + "verifier": "SETUP_DNS_BLOCKER_SKIP_MAIN=1 source + `declare -F`, diffed against the same from a worktree at HEAD; plus systemctl cat and bash -n on the unit's ExecStart; plus pre-commit run --files and check_file_length.sh." +} diff --git a/docs/superpowers/evidence/linux-config-dns-blocker-split-2026-08-16.json b/docs/superpowers/evidence/linux-config-dns-blocker-split-2026-08-16.json new file mode 100644 index 00000000..ebc439e6 --- /dev/null +++ b/docs/superpowers/evidence/linux-config-dns-blocker-split-2026-08-16.json @@ -0,0 +1,37 @@ +{ + "intent": "Split setup_dns_blocker.sh under the cap without disturbing the live systemd unit that runs it.", + "scope": [ + "linux_configuration/scripts/single_use/features/setup_dns_blocker.sh (569 -> 211)", + "lib/dns_units.sh, dns_firewall.sh, dns_dhcp.sh (new)", + "Non-goal: main() not run" + ], + "changes": [ + "Three libs: the dnsmasq config and systemd unit writers, the validation and firewall block, and the DHCP-server and status block. All seven heredocs sit inside functions, so function boundaries were safe seams.", + "The script guards its own entry with SETUP_DNS_BLOCKER_SKIP_MAIN, which made a proper function-set comparison possible on a script that otherwise reconfigures the machine when sourced." + ], + "verification": [ + { + "command": "SETUP_DNS_BLOCKER_SKIP_MAIN=1 source the script and diff `declare -F` against a detached worktree at HEAD", + "result": "pass", + "evidence": "Identical 74-function set (its own plus common.sh's). This is the same check used for the two shared libs, and it is available here only because the script provides that escape hatch." + }, + { + "command": "systemctl cat dns-blocklist-refresh.service, test -f its ExecStart, bash -n that file", + "result": "pass", + "evidence": "Still resolves to the split script and still parses. This is one of only two units pointing into the repo." + }, + { + "command": "bash -n on the entry and each lib; pre-commit run --files", + "result": "pass", + "evidence": "All four parse and the gate is clean, with no suppression." + } + ], + "risks": [ + "main() was not run: it installs dnsmasq, writes units, and changes nftables and DHCP settings on the live LAN. The function-set diff proves the surface, not the behaviour.", + "The refresh timer invokes the script with `refresh`, which reaches cmd_refresh through the same dispatch; that path is covered by the function-set check but not executed." + ], + "rollback": [ + "git revert the commit; the script returns to 569 lines and the three libs disappear.", + "After rollback, re-run the SKIP_MAIN function-set check and expect the same 74 names." + ] +} diff --git a/linux_configuration/scripts/single_use/features/lib/dns_dhcp.sh b/linux_configuration/scripts/single_use/features/lib/dns_dhcp.sh new file mode 100755 index 00000000..e6abbe09 --- /dev/null +++ b/linux_configuration/scripts/single_use/features/lib/dns_dhcp.sh @@ -0,0 +1,188 @@ +#!/bin/bash +# Static IP, DHCP server config and the status report. +# +# Sourced by setup_dns_blocker.sh; split out to keep it under the 250-line cap. +# Sourced rather than run, so it inherits the caller's strict mode. + +configure_static_ip() { + has_cmd nmcli || die "nmcli (NetworkManager) not found; cannot pin a static IP automatically." + local con + con="$(nm_connection)" + [[ -n $con ]] || die "No active NetworkManager connection on ${LAN_IFACE}." + log_info "Pinning ${LAN_IFACE} to static ${LAN_IP}/24 (gw ${GATEWAY}) on '${con}'..." + nmcli con mod "$con" \ + ipv4.method manual \ + ipv4.addresses "${LAN_IP}/24" \ + ipv4.gateway "$GATEWAY" \ + ipv4.dns "$GATEWAY" + nmcli con up "$con" >/dev/null + # Safety self-check: if the static config broke connectivity, auto-revert to + # DHCP-client mode immediately so the machine never ends up stranded. + if ping -c1 -W3 "$GATEWAY" >/dev/null 2>&1; then + log_ok "Static IP applied; gateway reachable. ${LAN_IFACE} no longer needs external DHCP." + else + log_error "Gateway unreachable after static IP change -- auto-reverting to DHCP." + revert_nic_to_dhcp "$con" + die "Static IP self-check failed; reverted to DHCP. DHCP mode aborted (no changes kept)." + fi +} + +write_dhcp_conf() { + local mac net_prefix + mac="$(cat "/sys/class/net/${LAN_IFACE}/address")" + net_prefix="${LAN_IP%.*}" + log_info "Writing ${DHCP_CONF} (range ${net_prefix}.${DHCP_START_HOST}-${net_prefix}.${DHCP_END_HOST}, DNS ${LAN_IP})." + cat >"$DHCP_CONF" < Zapisz)," + log_warn "otherwise two DHCP servers will fight on the LAN." + if ! ask_yes_no "Have you already disabled the router's DHCP server?"; then + log_warn "Not activating -- two DHCP servers on one LAN conflict." + log_warn "Disable the router's DHCP ('wlacz serwer DHCP' -> Zapisz), then re-run: sudo ${SELF} dhcp" + return 0 + fi + configure_static_ip + write_dhcp_conf + dnsmasq --test >/dev/null 2>&1 || die "dnsmasq config invalid after adding DHCP -- not restarting." + systemctl restart dnsmasq + log_ok "This PC is now the LAN DHCP server; new leases advertise ${LAN_IP} as DNS." + log_info "Reconnect a device's WiFi to pick up the new lease, then browse to a blocked site to confirm." + cat < stops serving DHCP, KEEPS this PC online on ${LAN_IP}. + Then re-tick the router's DHCP ('wlacz serwer DHCP' -> Zapisz) so other + devices get addresses again. This PC stays reachable throughout. + -------------------------------------------------------------------------- +EOF +} + +# Roll back DHCP mode: stop serving leases. KEEPS this PC on its static IP so +# running rollback can never strand the machine (the router reserves ${LAN_IP} +# anyway). Re-enable the router's DHCP afterwards for the other devices. +cmd_dhcp_off() { + detect_lan + if [[ -f $DHCP_CONF ]]; then + rm -f "$DHCP_CONF" + log_ok "Removed ${DHCP_CONF} (PC no longer serves DHCP)." + else + log_info "No DHCP config present; nothing to remove." + fi + is_service_active dnsmasq && systemctl restart dnsmasq + log_ok "DHCP serving stopped. This PC keeps its static IP (${LAN_IP}); still online." + log_warn "Now re-enable the router's DHCP ('wlacz serwer DHCP' -> Zapisz) so other devices get addresses." + log_info "Optional: to also put THIS PC back on router DHCP (after re-enabling it above), run:" + log_info " nmcli con mod '$(nm_connection)' ipv4.method auto ipv4.addresses '' ipv4.gateway '' ipv4.dns '' && nmcli con up '$(nm_connection)'" +} + +# ---- Status ---------------------------------------------------------------- +status_line() { + # $1 ok/bad flag ("0" ok), $2 message + if [[ $1 == "0" ]]; then log_ok "$2"; else log_warn "$2"; fi +} + +cmd_status() { + detect_lan + echo "=== LAN DNS blocker status ===" + + if is_service_active dnsmasq; then + status_line 0 "dnsmasq: active" + else + status_line 1 "dnsmasq: NOT active" + fi + if is_service_enabled dnsmasq; then + status_line 0 "dnsmasq: enabled at boot" + else + status_line 1 "dnsmasq: NOT enabled" + fi + if [[ -f $DNSMASQ_DROPIN ]]; then + status_line 0 "Restart=always drop-in present" + else + status_line 1 "Restart drop-in missing" + fi + if [[ -f $FEED ]]; then + status_line 0 "blocklist feed: $(wc -l <"$FEED") lines, updated $(date -r "$FEED" '+%Y-%m-%d %H:%M')" + else + status_line 1 "blocklist feed missing: ${FEED}" + fi + + if ss -tulnp 2>/dev/null | grep -qE "${LAN_IP}:53|:53 "; then + status_line 0 "listening on ${LAN_IP}:53" + else + status_line 1 "not listening on ${LAN_IP}:53" + fi + + local fw_input + fw_input="$(nft list chain inet filter input 2>/dev/null || true)" + if [[ $fw_input == *"policy drop"* ]]; then + if [[ $fw_input == *"dport 53"* ]]; then + status_line 0 "firewall: DNS/DHCP open for LAN" + else + status_line 1 "firewall loaded but DNS/DHCP NOT open -- run: sudo ${WG_SCRIPT} allow-dns" + fi + elif [[ $EUID -ne 0 ]]; then + log_info "firewall: run 'sudo $0 status' to inspect nftables (needs root)" + else + status_line 0 "firewall: no default-drop ruleset loaded (DNS/DHCP not filtered)" + fi + + if systemctl is-active dns-blocklist-refresh.timer &>/dev/null; then + status_line 0 "refresh timer: active (next $(systemctl show -p NextElapseUSecRealtime --value dns-blocklist-refresh.timer 2>/dev/null))" + else + status_line 1 "refresh timer: NOT active" + fi + if [[ -f $DHCP_CONF ]]; then + if ss -ulnp 2>/dev/null | grep -q ':67 '; then + status_line 0 "DHCP mode: ON (serving leases; PC is the LAN DHCP server)" + else + status_line 1 "DHCP mode: config present but not serving -- restart dnsmasq" + fi + else + status_line 0 "DHCP mode: off (using router DHCP / manual per-device DNS)" + fi + + echo + echo "=== Live resolution test (via ${LAN_IP}) ===" + if has_cmd dig; then + local blocked passthru + blocked="$(dig +short +time=2 +tries=1 @"${LAN_IP}" youtube.com 2>/dev/null | head -1)" + passthru="$(dig +short +time=2 +tries=1 @"${LAN_IP}" example.com 2>/dev/null | head -1)" + if [[ $blocked == "0.0.0.0" ]]; then + status_line 0 "youtube.com -> ${blocked} (BLOCKED, correct)" + else + status_line 1 "youtube.com -> ${blocked:-} (expected 0.0.0.0)" + fi + if [[ -n $passthru && $passthru != "0.0.0.0" ]]; then + status_line 0 "example.com -> ${passthru} (passthrough, correct)" + else + status_line 1 "example.com -> ${passthru:-} (expected a real IP)" + fi + else + log_warn "install 'dig' (bind) to run the live resolution test." + fi + + if [[ -s $LOG_FILE ]]; then + echo + echo "=== Recent dnsmasq log (${LOG_FILE}) ===" + tail -5 "$LOG_FILE" + fi +} diff --git a/linux_configuration/scripts/single_use/features/lib/dns_firewall.sh b/linux_configuration/scripts/single_use/features/lib/dns_firewall.sh new file mode 100755 index 00000000..7e133cb9 --- /dev/null +++ b/linux_configuration/scripts/single_use/features/lib/dns_firewall.sh @@ -0,0 +1,86 @@ +#!/bin/bash +# Validation, firewall rules and the allow-DNS flag. +# +# Sourced by setup_dns_blocker.sh; split out to keep it under the 250-line cap. +# Sourced rather than run, so it inherits the caller's strict mode. + +validate_and_enable() { + log_info "Validating dnsmasq configuration..." + dnsmasq --test 2>&1 | tail -3 + dnsmasq --test >/dev/null 2>&1 || die "dnsmasq config failed validation -- not enabling." + log_ok "dnsmasq config valid." + systemctl daemon-reload + enable_service dnsmasq + # enable --now does not restart an already-running daemon, so restart to + # apply config changes on idempotent re-runs. + systemctl restart dnsmasq + systemctl enable --now dns-blocklist-refresh.timer + systemctl enable --now dnsmasq-watchdog.timer + log_ok "dnsmasq, the daily refresh timer, and the watchdog are enabled." +} + +# Open port 53 for LAN clients -- but only touch the firewall if it is already +# the active (wireguard-managed) default-drop ruleset. If nftables is inactive, +# port 53 is already reachable and force-loading the firewall here could +# disrupt other services, so we only persist the intent and instruct the user. +# A default-drop nftables ruleset can be loaded in the kernel even while +# nftables.service reads "inactive", so detect the actual ruleset, not the unit. +firewall_is_loaded() { + # Capture then match (no 'grep -q' pipe: it closes early -> SIGPIPE on nft -> + # pipefail makes the whole pipeline fail, a false negative under set -o pipefail). + local rules + rules="$(nft list chain inet filter input 2>/dev/null || true)" + [[ $rules == *"policy drop"* ]] +} + +configure_firewall() { + if firewall_is_loaded; then + log_info "Default-drop firewall detected; opening DNS/DHCP for the LAN via the firewall owner." + bash "$WG_SCRIPT" allow-dns + else + log_warn "No default-drop firewall loaded -- DNS/DHCP already reachable on the LAN." + persist_allow_dns_flag + log_warn "If you later enable the default-drop firewall, run: sudo ${WG_SCRIPT} allow-dns" + fi +} + +# Record ALLOW_DNS=true in the wireguard config so a future firewall apply keeps +# :53 open for the LAN, without activating the firewall now. +persist_allow_dns_flag() { + local cfg="${SCRIPT_DIR}/.wireguard_ssh.conf" + [[ -f $cfg ]] || return 0 + if grep -qE '^ALLOW_DNS=' "$cfg"; then + sed -i 's/^ALLOW_DNS=.*/ALLOW_DNS="true"/' "$cfg" + else + printf 'ALLOW_DNS="true"\n' >>"$cfg" + fi + log_ok "Persisted ALLOW_DNS=true in ${cfg} for the next firewall apply." +} + +print_manual_steps() { + cat < Static -> DNS). + + PHONE (all methods) -- turn Private DNS OFF (Settings -> Network -> Private + DNS -> Off). Android's default "Automatic" uses DoH and bypasses LAN DNS. + This is a toggle, not an app, so it keeps the "no extra app" rule. + + Known limitations (voluntary DNS): a device using DoH, a VPN, or a manually + set DNS bypasses this. It cannot be forced because the PC is not in the + traffic path. Works for the common case (devices that honor network DNS). +============================================================================ +EOF +} diff --git a/linux_configuration/scripts/single_use/features/lib/dns_units.sh b/linux_configuration/scripts/single_use/features/lib/dns_units.sh new file mode 100755 index 00000000..2a3c90c1 --- /dev/null +++ b/linux_configuration/scripts/single_use/features/lib/dns_units.sh @@ -0,0 +1,109 @@ +#!/bin/bash +# dnsmasq config, drop-in, refresh timer and watchdog units. +# +# Sourced by setup_dns_blocker.sh; split out to keep it under the 250-line cap. +# Sourced rather than run, so it inherits the caller's strict mode. + +write_dnsmasq_conf() { + ensure_dir "$(dirname "$DNSMASQ_CONF")" # /etc/dnsmasq.d may not exist yet + log_info "Writing ${DNSMASQ_CONF} (listen on ${LAN_IFACE}/${LAN_IP}, upstream ${UPSTREAM})." + cat >"$DNSMASQ_CONF" <"$DNSMASQ_DROPIN" <<'EOF' +# Managed by setup_dns_blocker.sh -- keep the resolver up and wait for the +# network so bind-interfaces can bind the LAN address at boot. +[Unit] +After=network-online.target +Wants=network-online.target + +[Service] +Restart=always +RestartSec=5 +EOF +} + +install_refresh_timer() { + cat >"$REFRESH_SERVICE" <"$REFRESH_TIMER" <<'EOF' +[Unit] +Description=Daily rebuild of the LAN DNS blocklist + +[Timer] +OnCalendar=daily +Persistent=true +RandomizedDelaySec=15m + +[Install] +WantedBy=timers.target +EOF +} + +# The whole LAN's DHCP/DNS depends on this one process. Restart=always in the +# drop-in only catches an actual crash; an incident on 2026-07-23 found +# dnsmasq dead with ZERO error logged (no crash, no "Stopping"/"Stopped" line +# either -- consistent with something issuing a plain stop, not a failure) and +# it stayed down for hours, silently, until noticed by a phone unable to join +# WiFi. Mirrors the existing shutdown-timer-monitor-watchdog pattern used +# elsewhere in this repo: same 5-minute cadence, same is-active-or-start shape, +# plus a logger call so a future incident leaves a trace instead of none. +install_watchdog() { + cat >"$WATCHDOG_SERVICE" <<'EOF' +[Unit] +Description=Watchdog for dnsmasq (LAN DHCP/DNS) +After=multi-user.target + +[Service] +Type=oneshot +ExecStart=/bin/bash -c 'systemctl is-active --quiet dnsmasq || { logger -t dnsmasq-watchdog "dnsmasq was down -- restarting"; systemctl restart dnsmasq; }' +EOF + cat >"$WATCHDOG_TIMER" <<'EOF' +[Unit] +Description=Watchdog Timer for dnsmasq + +[Timer] +OnBootSec=60 +OnUnitActiveSec=300 +Persistent=true + +[Install] +WantedBy=timers.target +EOF +} diff --git a/linux_configuration/scripts/single_use/features/setup_dns_blocker.sh b/linux_configuration/scripts/single_use/features/setup_dns_blocker.sh index 78400a45..d9fce123 100755 --- a/linux_configuration/scripts/single_use/features/setup_dns_blocker.sh +++ b/linux_configuration/scripts/single_use/features/setup_dns_blocker.sh @@ -114,190 +114,14 @@ build_feed() { log_ok "Feed built: ${count} lines at ${FEED}." } -write_dnsmasq_conf() { - ensure_dir "$(dirname "$DNSMASQ_CONF")" # /etc/dnsmasq.d may not exist yet - log_info "Writing ${DNSMASQ_CONF} (listen on ${LAN_IFACE}/${LAN_IP}, upstream ${UPSTREAM})." - cat >"$DNSMASQ_CONF" <"$DNSMASQ_DROPIN" <<'EOF' -# Managed by setup_dns_blocker.sh -- keep the resolver up and wait for the -# network so bind-interfaces can bind the LAN address at boot. -[Unit] -After=network-online.target -Wants=network-online.target - -[Service] -Restart=always -RestartSec=5 -EOF -} - -install_refresh_timer() { - cat >"$REFRESH_SERVICE" <"$REFRESH_TIMER" <<'EOF' -[Unit] -Description=Daily rebuild of the LAN DNS blocklist - -[Timer] -OnCalendar=daily -Persistent=true -RandomizedDelaySec=15m - -[Install] -WantedBy=timers.target -EOF -} - -# The whole LAN's DHCP/DNS depends on this one process. Restart=always in the -# drop-in only catches an actual crash; an incident on 2026-07-23 found -# dnsmasq dead with ZERO error logged (no crash, no "Stopping"/"Stopped" line -# either -- consistent with something issuing a plain stop, not a failure) and -# it stayed down for hours, silently, until noticed by a phone unable to join -# WiFi. Mirrors the existing shutdown-timer-monitor-watchdog pattern used -# elsewhere in this repo: same 5-minute cadence, same is-active-or-start shape, -# plus a logger call so a future incident leaves a trace instead of none. -install_watchdog() { - cat >"$WATCHDOG_SERVICE" <<'EOF' -[Unit] -Description=Watchdog for dnsmasq (LAN DHCP/DNS) -After=multi-user.target - -[Service] -Type=oneshot -ExecStart=/bin/bash -c 'systemctl is-active --quiet dnsmasq || { logger -t dnsmasq-watchdog "dnsmasq was down -- restarting"; systemctl restart dnsmasq; }' -EOF - cat >"$WATCHDOG_TIMER" <<'EOF' -[Unit] -Description=Watchdog Timer for dnsmasq - -[Timer] -OnBootSec=60 -OnUnitActiveSec=300 -Persistent=true - -[Install] -WantedBy=timers.target -EOF -} - -validate_and_enable() { - log_info "Validating dnsmasq configuration..." - dnsmasq --test 2>&1 | tail -3 - dnsmasq --test >/dev/null 2>&1 || die "dnsmasq config failed validation -- not enabling." - log_ok "dnsmasq config valid." - systemctl daemon-reload - enable_service dnsmasq - # enable --now does not restart an already-running daemon, so restart to - # apply config changes on idempotent re-runs. - systemctl restart dnsmasq - systemctl enable --now dns-blocklist-refresh.timer - systemctl enable --now dnsmasq-watchdog.timer - log_ok "dnsmasq, the daily refresh timer, and the watchdog are enabled." -} - -# Open port 53 for LAN clients -- but only touch the firewall if it is already -# the active (wireguard-managed) default-drop ruleset. If nftables is inactive, -# port 53 is already reachable and force-loading the firewall here could -# disrupt other services, so we only persist the intent and instruct the user. -# A default-drop nftables ruleset can be loaded in the kernel even while -# nftables.service reads "inactive", so detect the actual ruleset, not the unit. -firewall_is_loaded() { - # Capture then match (no 'grep -q' pipe: it closes early -> SIGPIPE on nft -> - # pipefail makes the whole pipeline fail, a false negative under set -o pipefail). - local rules - rules="$(nft list chain inet filter input 2>/dev/null || true)" - [[ $rules == *"policy drop"* ]] -} - -configure_firewall() { - if firewall_is_loaded; then - log_info "Default-drop firewall detected; opening DNS/DHCP for the LAN via the firewall owner." - bash "$WG_SCRIPT" allow-dns - else - log_warn "No default-drop firewall loaded -- DNS/DHCP already reachable on the LAN." - persist_allow_dns_flag - log_warn "If you later enable the default-drop firewall, run: sudo ${WG_SCRIPT} allow-dns" - fi -} - -# Record ALLOW_DNS=true in the wireguard config so a future firewall apply keeps -# :53 open for the LAN, without activating the firewall now. -persist_allow_dns_flag() { - local cfg="${SCRIPT_DIR}/.wireguard_ssh.conf" - [[ -f $cfg ]] || return 0 - if grep -qE '^ALLOW_DNS=' "$cfg"; then - sed -i 's/^ALLOW_DNS=.*/ALLOW_DNS="true"/' "$cfg" - else - printf 'ALLOW_DNS="true"\n' >>"$cfg" - fi - log_ok "Persisted ALLOW_DNS=true in ${cfg} for the next firewall apply." -} - -print_manual_steps() { - cat < Static -> DNS). - - PHONE (all methods) -- turn Private DNS OFF (Settings -> Network -> Private - DNS -> Off). Android's default "Automatic" uses DoH and bypasses LAN DNS. - This is a toggle, not an app, so it keeps the "no extra app" rule. - - Known limitations (voluntary DNS): a device using DoH, a VPN, or a manually - set DNS bypasses this. It cannot be forced because the PC is not in the - traffic path. Works for the common case (devices that honor network DNS). -============================================================================ -EOF -} cmd_setup() { detect_lan @@ -342,188 +166,6 @@ revert_nic_to_dhcp() { nmcli con up "$con" >/dev/null 2>&1 || true } -configure_static_ip() { - has_cmd nmcli || die "nmcli (NetworkManager) not found; cannot pin a static IP automatically." - local con - con="$(nm_connection)" - [[ -n $con ]] || die "No active NetworkManager connection on ${LAN_IFACE}." - log_info "Pinning ${LAN_IFACE} to static ${LAN_IP}/24 (gw ${GATEWAY}) on '${con}'..." - nmcli con mod "$con" \ - ipv4.method manual \ - ipv4.addresses "${LAN_IP}/24" \ - ipv4.gateway "$GATEWAY" \ - ipv4.dns "$GATEWAY" - nmcli con up "$con" >/dev/null - # Safety self-check: if the static config broke connectivity, auto-revert to - # DHCP-client mode immediately so the machine never ends up stranded. - if ping -c1 -W3 "$GATEWAY" >/dev/null 2>&1; then - log_ok "Static IP applied; gateway reachable. ${LAN_IFACE} no longer needs external DHCP." - else - log_error "Gateway unreachable after static IP change -- auto-reverting to DHCP." - revert_nic_to_dhcp "$con" - die "Static IP self-check failed; reverted to DHCP. DHCP mode aborted (no changes kept)." - fi -} - -write_dhcp_conf() { - local mac net_prefix - mac="$(cat "/sys/class/net/${LAN_IFACE}/address")" - net_prefix="${LAN_IP%.*}" - log_info "Writing ${DHCP_CONF} (range ${net_prefix}.${DHCP_START_HOST}-${net_prefix}.${DHCP_END_HOST}, DNS ${LAN_IP})." - cat >"$DHCP_CONF" < Zapisz)," - log_warn "otherwise two DHCP servers will fight on the LAN." - if ! ask_yes_no "Have you already disabled the router's DHCP server?"; then - log_warn "Not activating -- two DHCP servers on one LAN conflict." - log_warn "Disable the router's DHCP ('wlacz serwer DHCP' -> Zapisz), then re-run: sudo ${SELF} dhcp" - return 0 - fi - configure_static_ip - write_dhcp_conf - dnsmasq --test >/dev/null 2>&1 || die "dnsmasq config invalid after adding DHCP -- not restarting." - systemctl restart dnsmasq - log_ok "This PC is now the LAN DHCP server; new leases advertise ${LAN_IP} as DNS." - log_info "Reconnect a device's WiFi to pick up the new lease, then browse to a blocked site to confirm." - cat < stops serving DHCP, KEEPS this PC online on ${LAN_IP}. - Then re-tick the router's DHCP ('wlacz serwer DHCP' -> Zapisz) so other - devices get addresses again. This PC stays reachable throughout. - -------------------------------------------------------------------------- -EOF -} - -# Roll back DHCP mode: stop serving leases. KEEPS this PC on its static IP so -# running rollback can never strand the machine (the router reserves ${LAN_IP} -# anyway). Re-enable the router's DHCP afterwards for the other devices. -cmd_dhcp_off() { - detect_lan - if [[ -f $DHCP_CONF ]]; then - rm -f "$DHCP_CONF" - log_ok "Removed ${DHCP_CONF} (PC no longer serves DHCP)." - else - log_info "No DHCP config present; nothing to remove." - fi - is_service_active dnsmasq && systemctl restart dnsmasq - log_ok "DHCP serving stopped. This PC keeps its static IP (${LAN_IP}); still online." - log_warn "Now re-enable the router's DHCP ('wlacz serwer DHCP' -> Zapisz) so other devices get addresses." - log_info "Optional: to also put THIS PC back on router DHCP (after re-enabling it above), run:" - log_info " nmcli con mod '$(nm_connection)' ipv4.method auto ipv4.addresses '' ipv4.gateway '' ipv4.dns '' && nmcli con up '$(nm_connection)'" -} - -# ---- Status ---------------------------------------------------------------- -status_line() { - # $1 ok/bad flag ("0" ok), $2 message - if [[ $1 == "0" ]]; then log_ok "$2"; else log_warn "$2"; fi -} - -cmd_status() { - detect_lan - echo "=== LAN DNS blocker status ===" - - if is_service_active dnsmasq; then - status_line 0 "dnsmasq: active" - else - status_line 1 "dnsmasq: NOT active" - fi - if is_service_enabled dnsmasq; then - status_line 0 "dnsmasq: enabled at boot" - else - status_line 1 "dnsmasq: NOT enabled" - fi - if [[ -f $DNSMASQ_DROPIN ]]; then - status_line 0 "Restart=always drop-in present" - else - status_line 1 "Restart drop-in missing" - fi - if [[ -f $FEED ]]; then - status_line 0 "blocklist feed: $(wc -l <"$FEED") lines, updated $(date -r "$FEED" '+%Y-%m-%d %H:%M')" - else - status_line 1 "blocklist feed missing: ${FEED}" - fi - - if ss -tulnp 2>/dev/null | grep -qE "${LAN_IP}:53|:53 "; then - status_line 0 "listening on ${LAN_IP}:53" - else - status_line 1 "not listening on ${LAN_IP}:53" - fi - - local fw_input - fw_input="$(nft list chain inet filter input 2>/dev/null || true)" - if [[ $fw_input == *"policy drop"* ]]; then - if [[ $fw_input == *"dport 53"* ]]; then - status_line 0 "firewall: DNS/DHCP open for LAN" - else - status_line 1 "firewall loaded but DNS/DHCP NOT open -- run: sudo ${WG_SCRIPT} allow-dns" - fi - elif [[ $EUID -ne 0 ]]; then - log_info "firewall: run 'sudo $0 status' to inspect nftables (needs root)" - else - status_line 0 "firewall: no default-drop ruleset loaded (DNS/DHCP not filtered)" - fi - - if systemctl is-active dns-blocklist-refresh.timer &>/dev/null; then - status_line 0 "refresh timer: active (next $(systemctl show -p NextElapseUSecRealtime --value dns-blocklist-refresh.timer 2>/dev/null))" - else - status_line 1 "refresh timer: NOT active" - fi - if [[ -f $DHCP_CONF ]]; then - if ss -ulnp 2>/dev/null | grep -q ':67 '; then - status_line 0 "DHCP mode: ON (serving leases; PC is the LAN DHCP server)" - else - status_line 1 "DHCP mode: config present but not serving -- restart dnsmasq" - fi - else - status_line 0 "DHCP mode: off (using router DHCP / manual per-device DNS)" - fi - - echo - echo "=== Live resolution test (via ${LAN_IP}) ===" - if has_cmd dig; then - local blocked passthru - blocked="$(dig +short +time=2 +tries=1 @"${LAN_IP}" youtube.com 2>/dev/null | head -1)" - passthru="$(dig +short +time=2 +tries=1 @"${LAN_IP}" example.com 2>/dev/null | head -1)" - if [[ $blocked == "0.0.0.0" ]]; then - status_line 0 "youtube.com -> ${blocked} (BLOCKED, correct)" - else - status_line 1 "youtube.com -> ${blocked:-} (expected 0.0.0.0)" - fi - if [[ -n $passthru && $passthru != "0.0.0.0" ]]; then - status_line 0 "example.com -> ${passthru} (passthrough, correct)" - else - status_line 1 "example.com -> ${passthru:-} (expected a real IP)" - fi - else - log_warn "install 'dig' (bind) to run the live resolution test." - fi - - if [[ -s $LOG_FILE ]]; then - echo - echo "=== Recent dnsmasq log (${LOG_FILE}) ===" - tail -5 "$LOG_FILE" - fi -} usage() { cat <