testsAndMisc/linux_configuration/features/lib/dns_dhcp.sh
Krzysztof kuhy Rudnicki 4f2b0ccdf7 Dissolve linux_configuration/scripts/ and single_use/
Two levels of pure taxonomy carrying no information, and a 2-directory
cap leaves no room for them. Categories now sit directly under
linux_configuration/: features, fixes, utils, misc, fresh-install,
installers, diagnostics, gaming, boot_recovery, periodic_background, lib,
meta, tests. A script's own lib/ and tests/ are exempt from the cap, so
those stay put.

Depth violations 248 -> 139; roughly 40 of the remainder are inside
digital_wellbeing and hosts, deferred until the guard-lib repoint.

The moves are the easy half. What breaks is every path that counted
levels, and three classes of it had to be found by resolving paths rather
than reading diffs:

  - 52 relative `source` lines for lib/common.sh and lib/android.sh. The
    correct prefix differs per file: 40 were ../../ and 12 were ../../../,
    so a blanket replace would have fixed one set and broken the other.
  - REPO_ROOT in 7 fixes/lib/tests harnesses and setup_syncyomi_guard.sh,
    each walking up four levels from a directory now two levels shallower.
    This caused a real regression -- the fixes suite went red with
    "log_warn: command not found" -- caught by running it and comparing
    against the pre-move baseline worktree.
  - conftest.py's sys.path entries, which took the pytest gate down with
    14 collection errors, and the `# shellcheck source=` directives, which
    took shellcheck down with SC1091.

16 symlinks in $HOME broke, including the entire i3blocks bar. All
repointed, and cpu_monitor/memory/disk/time/volume were run to confirm
they emit real output rather than merely resolving.

Verified: 1798 tests pass at 100% coverage, 8 of 9 lib/tests suites pass
(the ninth needs the coverage jail, identically at baseline), pre-commit
is clean, and the two untracked secret configs are still ignored after
their gitignore paths changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 18:53:30 +02:00

189 lines
7.2 KiB
Bash
Executable File

#!/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" <<EOF
# Managed by setup_dns_blocker.sh -- this PC is the LAN DHCP server.
# Only serve leases once the router's own DHCP is disabled (two servers clash).
dhcp-authoritative
dhcp-range=${net_prefix}.${DHCP_START_HOST},${net_prefix}.${DHCP_END_HOST},${DHCP_LEASE}
dhcp-option=option:router,${GATEWAY}
dhcp-option=option:dns-server,${LAN_IP}
# Reserve this PC's static address against its own MAC.
dhcp-host=${mac},${LAN_IP}
# Log every DHCP transaction (low volume; satisfies "log failures").
log-dhcp
EOF
}
cmd_dhcp() {
detect_lan
is_service_active dnsmasq ||
die "Run 'setup' first -- dnsmasq must be configured before enabling DHCP mode."
echo
log_warn "DHCP takeover: this PC (${LAN_IP}) will become the LAN's DHCP server."
log_warn "Disable the router's DHCP FIRST (untick 'wlacz serwer DHCP' -> 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 <<EOF
---- IF ANYTHING GOES WRONG (one command, cannot lock you out) ------------
sudo ${SELF} dhcp-off
-> 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:-<no answer>} (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:-<no answer>} (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
}