testsAndMisc/linux_configuration/features/lib/wg_gateway.sh
Krzysztof kuhy Rudnicki 8ec00f2b89
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (mypy, mypy) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / lib-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
warp-window: open Mon/Thu/Fri 08:00-16:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWYHStUbKpAMzi4hdEV
2026-11-05 09:15:17 +01:00

112 lines
4.8 KiB
Bash
Executable File

#!/bin/bash
# LAN gateway rules (setup_dns_blocker.sh gateway): forward other devices'
# traffic, force their DNS to the blocker, drop DoH, DoT and the WARP tunnel.
#
# Sourced by wg_firewall.sh, which renders the result into /etc/nftables.conf,
# so a reboot or any other allow-* apply keeps it. Only FORWARDED traffic and
# the nat hooks are touched: the PC's own traffic never passes through these.
# DoH/DoT provider addresses. Everything else to these from the LAN is dropped,
# so a browser set to "secure DNS" fails over to plain DNS, which is DNATed to
# the blocker. The PC's own upstream queries are OUTPUT, never FORWARD.
readonly DOH_RESOLVERS_V4="1.1.1.1, 1.0.0.1, 8.8.8.8, 8.8.4.4, 9.9.9.9, 149.112.112.112, \
208.67.222.222, 208.67.220.220, 94.140.14.14, 94.140.15.15, 45.90.28.0/24, \
45.90.30.0/24, 76.76.2.0/24, 76.76.10.0/24, 194.242.2.0/24, 185.228.168.9, \
185.228.169.9, 104.16.248.249, 104.16.249.249, 162.159.36.1, 162.159.46.1"
# Cloudflare WARP tunnel ingress. A WARP client tunnels everything past the
# DNS redirect, so the tunnel itself is the bypass. Dedicated ranges are dropped
# outright (MASQUE falls back to TCP 443, so a port match is not enough):
# 162.159.193.0/24 WireGuard, 162.159.197.0/24 MASQUE, 162.159.192.0/24 the
# consumer endpoint. Source: developers.cloudflare.com/cloudflare-one/
# team-and-resources/devices/warp/deployment/firewall/ (2026-10-25).
readonly WARP_V4="162.159.192.0/24, 162.159.193.0/24, 162.159.197.0/24"
# Consumer WARP endpoints that share anycast space with proxied websites: only
# UDP is dropped there, so those sites still load (HTTP/3 falls back to TCP).
# No LAN-wide drop of UDP 500/1701/4500: that would break IPsec for everyone.
readonly WARP_UDP_V4="162.159.192.0/22, 162.159.204.0/22, 188.114.96.0/22"
# Sources in warp_open skip both WARP drops. Empty by default; warp_window.sh
# fills it Mon/Thu/Fri 08:00-16:00 with an element that times out by itself. Any
# re-apply of this ruleset empties it again.
# Fill in LAN_IFACE/LAN_IP from the default route unless the caller set them.
lan_gateway_detect() {
local route
[[ -n ${LAN_IFACE:-} && -n ${LAN_IP:-} ]] && return 0
route="$(ip -4 route get 1.1.1.1 2>/dev/null || true)"
LAN_IFACE="$(awk '{for(i=1;i<=NF;i++) if($i=="dev"){print $(i+1); exit}}' <<<"$route")"
LAN_IP="$(awk '{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1); exit}}' <<<"$route")"
[[ -n $LAN_IFACE && -n $LAN_IP ]] || die "Could not detect the LAN interface/IP."
}
# Fragments for render_nftables_ruleset. Each prints nothing while gateway
# mode is off -- except the nat table, whose declare+delete is always rendered
# so that turning gateway mode off removes a stale table too.
lan_gateway_on() { [[ ${ALLOW_LAN_GATEWAY:-false} == "true" ]]; }
# One interval set of IPv4 ranges, inside table inet filter.
_lan_gateway_v4_set() {
printf '\n\tset %s {\n\t\ttype ipv4_addr\n\t\tflags interval\n' "$1"
printf '\t\telements = { %s }\n\t}\n' "$2"
}
# The DoH and WARP address sets.
lan_gateway_set() {
lan_gateway_on || return 0
_lan_gateway_v4_set doh_v4 "$DOH_RESOLVERS_V4"
_lan_gateway_v4_set warp_v4 "$WARP_V4"
_lan_gateway_v4_set warp_udp_v4 "$WARP_UDP_V4"
printf '\n\tset warp_open {\n\t\ttype ipv4_addr\n\t\tflags interval,timeout\n\t}\n'
}
# Forward-chain rules, placed first in the chain: the drops before the accept.
lan_gateway_forward() {
lan_gateway_on || return 0
lan_gateway_detect
local from="iifname \"${LAN_IFACE}\" ip saddr ${LAN_SUBNET}"
printf '\n\t\t%s ip daddr @doh_v4 drop' "$from"
printf '\n\t\t%s tcp dport 853 drop' "$from"
printf '\n\t\t%s udp dport 853 drop' "$from"
printf '\n\t\t%s ip saddr != @warp_open ip daddr @warp_v4 drop' "$from"
printf '\n\t\t%s ip saddr != @warp_open ip daddr @warp_udp_v4 meta l4proto udp drop' "$from"
printf '\n\t\tiifname "%s" oifname "%s" ip saddr %s accept' \
"$LAN_IFACE" "$LAN_IFACE" "$LAN_SUBNET"
}
lan_gateway_table() {
printf 'table ip lan_gateway\ndelete table ip lan_gateway\n'
lan_gateway_on || return 0
lan_gateway_detect
local dns="iifname \"${LAN_IFACE}\" ip saddr ${LAN_SUBNET} ip saddr != ${LAN_IP} ip daddr != ${LAN_IP}"
cat <<EOF
table ip lan_gateway {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
${dns} udp dport 53 dnat to ${LAN_IP}
${dns} tcp dport 53 dnat to ${LAN_IP}
}
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
oifname "${LAN_IFACE}" ip saddr ${LAN_SUBNET} ip saddr != ${LAN_IP} ip daddr != ${LAN_SUBNET} masquerade
}
}
EOF
}
allow_lan_gateway() {
export ALLOW_LAN_GATEWAY=true
save_config
write_nftables_ruleset
verify_nftables_then_apply
log_ok "LAN gateway rules applied: forwarding, DNS redirect, DoH/DoT and WARP drop (persisted)."
}
deny_lan_gateway() {
export ALLOW_LAN_GATEWAY=false
save_config
write_nftables_ruleset
verify_nftables_then_apply
log_ok "LAN gateway rules removed (persisted)."
}