From 57615a736ceba86d4ed22cd8fc5f6ceee1955749 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 29 May 2026 10:04:36 +0000 Subject: [PATCH 01/19] Add CLAUDE.md with project context and conventions --- CLAUDE.md | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7d1bf70 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,71 @@ +# CLAUDE.md + +Dieses Dokument gibt Claude (und anderen Mitwirkenden) den Kontext für dieses +Repository. + +## Zweck des Projekts + +Dieses Repo enthält Skripte und Konfigurationsvorlagen, um eine **Alpine-Linux-VM +in VirtualBox als abgeschotteten NAT-Router** einzurichten. Die Router-VM gewährt +weiteren VMs in einem internen VirtualBox-Netzwerk (`Internal Network`) +ausschließlich **Internetzugriff** — alle anderen privaten Adressbereiche +(RFC 1918, CGNAT, Link-Local) werden geblockt. + +### Kernanforderungen + +1. Nur Internetzugriff für Client-VMs, **keine** Erreichbarkeit anderer privater + Netze. +2. Die Router-VM selbst ist abgeschottet: **kein SSH, kein HTTP**, keine + eingehenden Dienste aus dem internen oder externen Netz erreichbar. +3. Konfiguration erfolgt **ausschließlich über die Konsole** (VirtualBox-Fenster). +4. Minimaler Footprint und minimale Angriffsfläche → **Alpine Linux**. + +## Netzwerk-Topologie + +``` ++----------------+ +-------------------+ +------------------+ +| Client-VMs | | Alpine Router | | VirtualBox Host | +| eth0 | | eth0 (intern) | | (Internet) | +| Internal Net +--------+ 10.0.99.1/24 | | | +| 10.0.99.x | | eth1 (extern) +--------+ NAT-Adapter | ++----------------+ +-------------------+ +------------------+ +``` + +- **Adapter 1** der Router-VM: `Internal Network` (Name z.B. `labnet`) → `eth0` +- **Adapter 2** der Router-VM: `NAT` (VirtualBox-NAT) → `eth1` +- **Client-VMs**: nur `Internal Network` `labnet`, kein eigener NAT-Adapter + +> Die Adapter-Reihenfolge in VirtualBox bestimmt die Interface-Namen +> (`eth0`, `eth1`). Adapter 1 muss das interne Netz sein. + +## Dateien + +| Datei | Zweck | +|----------------------------|--------------------------------------------------------------| +| `setup-nat-router.sh` | Orchestrator: installiert Pakete, Configs, Firewall, Lockdown, Persistenz | +| `firewall.sh` | Eigenständiges, idempotentes iptables/ip6tables-Ruleset | +| `config/interfaces` | Vorlage für `/etc/network/interfaces` | +| `config/dnsmasq.conf` | Vorlage für `/etc/dnsmasq.conf` (DHCP + DNS, nur intern) | +| `config/sysctl-router.conf`| IP-Forwarding aktivieren (IPv4), IPv6-Forwarding aus | +| `README.md` | Ausführliche Schritt-für-Schritt-Anleitung | + +## Konventionen + +- **Shell**: Alpine nutzt BusyBox `ash`, **nicht** Bash. Alle Skripte mit + `#!/bin/sh` und POSIX-kompatibel halten (kein `[[ ]]`, keine Bash-Arrays). +- **Idempotenz**: Skripte müssen mehrfach ausführbar sein, ohne Schaden + anzurichten (Configs sichern, Firewall flushen vor dem Anwenden). +- **Konfigurierbarkeit**: Interface-Namen und Netzbereiche stehen als Variablen + am Kopf der Skripte und sind per Environment überschreibbar. +- **Persistenz**: Alpine im Standard-Modus (diskless/RAM) braucht `lbu commit`, + damit Änderungen einen Reboot überleben. Bei einer "sys"-Installation auf + Platte ist `lbu` nicht nötig. + +## Sicherheitsprinzipien + +- Default-Policy `DROP` für `INPUT` und `FORWARD`. +- Auf dem Router selbst lauscht **kein** administrativer Dienst von außen. +- `dnsmasq` bindet ausschließlich an das interne Interface (`bind-interfaces`, + `except-interface`), niemals an `eth1`. +- IPv6-Forwarding ist deaktiviert und IPv6 wird per `ip6tables` geblockt, damit + die IPv4-Filterregeln nicht über IPv6 umgangen werden können. From 30047be9fddde1e5740b18c8294b1149db6afdef Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 29 May 2026 10:05:02 +0000 Subject: [PATCH 02/19] Add network, sysctl and dnsmasq config templates --- config/dnsmasq.conf | 35 +++++++++++++++++++++++++++++++++++ config/interfaces | 20 ++++++++++++++++++++ config/sysctl-router.conf | 21 +++++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 config/dnsmasq.conf create mode 100644 config/interfaces create mode 100644 config/sysctl-router.conf diff --git a/config/dnsmasq.conf b/config/dnsmasq.conf new file mode 100644 index 0000000..f0180d2 --- /dev/null +++ b/config/dnsmasq.conf @@ -0,0 +1,35 @@ +# /etc/dnsmasq.conf -- Alpine NAT-Router +# +# Stellt DHCP und DNS-Forwarding NUR fuer das interne Netz bereit. + +# --- Bindung: ausschliesslich das interne Interface bedienen --- +# dnsmasq darf niemals auf dem externen Interface (eth1) lauschen. +interface=eth0 +except-interface=eth1 +bind-interfaces + +# Nur an die eigene LAN-Adresse binden (kein 0.0.0.0). +listen-address=10.0.99.1 +# Loopback nicht fuer DNS oeffnen ist nicht noetig; localhost bleibt nutzbar. + +# --- DNS --- +# Keine A-Records fuer reine Hostnamen ohne Domain weiterleiten. +domain-needed +# Reverse-Lookups fuer private Adressen nicht nach oben weiterleiten. +bogus-priv +# Anfragen nur an Upstream-Server schicken, nicht /etc/hosts-Streuung. +# Upstream-Resolver (vom NAT bezogenes resolv.conf wird zusaetzlich genutzt). +server=9.9.9.9 +server=1.1.1.1 + +# Cachegroesse erhoehen. +cache-size=1000 + +# --- DHCP --- +dhcp-range=10.0.99.100,10.0.99.200,12h +# Option 3 = Default-Gateway -> Router +dhcp-option=3,10.0.99.1 +# Option 6 = DNS-Server -> Router (dnsmasq) +dhcp-option=6,10.0.99.1 +# Dieser dnsmasq ist die einzige DHCP-Autoritaet im internen Netz. +dhcp-authoritative diff --git a/config/interfaces b/config/interfaces new file mode 100644 index 0000000..89c8e42 --- /dev/null +++ b/config/interfaces @@ -0,0 +1,20 @@ +# /etc/network/interfaces -- Alpine NAT-Router +# +# eth0 = internes Netz (VirtualBox Internal Network "labnet") +# eth1 = externes Netz (VirtualBox NAT-Adapter -> Internet) +# +# Hinweis: Die Adapter-Reihenfolge in den VM-Einstellungen bestimmt die +# Interface-Namen. Adapter 1 muss das interne Netz sein, Adapter 2 das NAT. + +auto lo +iface lo inet loopback + +# Internes Interface: statische Gateway-Adresse fuer die Client-VMs +auto eth0 +iface eth0 inet static + address 10.0.99.1 + netmask 255.255.255.0 + +# Externes Interface: bezieht eine Adresse vom VirtualBox-NAT per DHCP +auto eth1 +iface eth1 inet dhcp diff --git a/config/sysctl-router.conf b/config/sysctl-router.conf new file mode 100644 index 0000000..9193eb2 --- /dev/null +++ b/config/sysctl-router.conf @@ -0,0 +1,21 @@ +# /etc/sysctl.d/99-nat-router.conf -- Alpine NAT-Router +# +# IPv4-Forwarding aktivieren, damit der Router Pakete zwischen den +# Interfaces weiterleiten kann. +net.ipv4.ip_forward = 1 + +# IPv6-Forwarding bewusst deaktivieren. Der Router filtert nur IPv4; +# ohne diese Zeile koennte IPv6-Verkehr die IPv4-Regeln umgehen. +net.ipv6.conf.all.forwarding = 0 + +# Reverse-Path-Filterung (Anti-Spoofing) im Strict-Modus. +net.ipv4.conf.all.rp_filter = 1 +net.ipv4.conf.default.rp_filter = 1 + +# Keine ICMP-Redirects annehmen oder senden. +net.ipv4.conf.all.accept_redirects = 0 +net.ipv4.conf.all.send_redirects = 0 +net.ipv4.conf.all.secure_redirects = 0 + +# Source-Routing ablehnen. +net.ipv4.conf.all.accept_source_route = 0 From c6eb52f12901b8b1b45d15b06674d4489b18e87a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 29 May 2026 10:05:35 +0000 Subject: [PATCH 03/19] Add idempotent iptables/ip6tables firewall ruleset --- firewall.sh | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100755 firewall.sh diff --git a/firewall.sh b/firewall.sh new file mode 100755 index 0000000..8b831f5 --- /dev/null +++ b/firewall.sh @@ -0,0 +1,124 @@ +#!/bin/sh +# +# firewall.sh -- iptables/ip6tables-Ruleset fuer den Alpine NAT-Router +# +# Ziel: +# * NAT (MASQUERADE) fuer ausgehenden Internetverkehr ueber das externe IF +# * Forwarding NUR ins Internet -- alle privaten Adressbereiche werden +# als Ziel geblockt (RFC 1918, CGNAT, Link-Local) +# * Der Router selbst nimmt KEINE administrativen Verbindungen an +# (kein SSH, kein HTTP) -- weder von intern noch von extern +# * IPv6 wird komplett geblockt, damit es die IPv4-Filter nicht umgeht +# +# Das Skript ist idempotent: es flusht die relevanten Tabellen und baut die +# Regeln neu auf. Es kann jederzeit erneut ausgefuehrt werden. +# +# Konfiguration ueber Environment moeglich, z.B.: +# INT_IF=eth0 EXT_IF=eth1 LAN_NET=10.0.99.0/24 sh firewall.sh +# +set -eu + +# --- Konfiguration ----------------------------------------------------------- +INT_IF="${INT_IF:-eth0}" # internes Interface (Client-Netz) +EXT_IF="${EXT_IF:-eth1}" # externes Interface (Internet via NAT) +LAN_NET="${LAN_NET:-10.0.99.0/24}" # internes Subnetz +LAN_IP="${LAN_IP:-10.0.99.1}" # eigene Adresse auf dem internen Interface + +# Private/nicht-internetfaehige Zielbereiche, die geblockt werden sollen. +PRIVATE_NETS="10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 100.64.0.0/10 169.254.0.0/16" + +# --- Hilfsfunktionen --------------------------------------------------------- +need() { + command -v "$1" >/dev/null 2>&1 || { + echo "FEHLER: '$1' nicht gefunden. Bitte 'apk add iptables' ausfuehren." >&2 + exit 1 + } +} + +need iptables +need ip6tables + +echo "Wende Firewall-Regeln an (intern=$INT_IF, extern=$EXT_IF, lan=$LAN_NET)..." + +# ============================================================================= +# IPv4 +# ============================================================================= + +# --- Tabellen leeren --- +iptables -F +iptables -X +iptables -t nat -F +iptables -t nat -X +iptables -t mangle -F 2>/dev/null || true +iptables -t mangle -X 2>/dev/null || true + +# --- Default-Policies: alles verbieten, was nicht ausdruecklich erlaubt ist --- +iptables -P INPUT DROP +iptables -P FORWARD DROP +iptables -P OUTPUT ACCEPT + +# ----------------------------------------------------------------------------- +# INPUT -- Verkehr AN den Router selbst +# ----------------------------------------------------------------------------- +# Loopback erlauben. +iptables -A INPUT -i lo -j ACCEPT + +# Bereits etablierte/zugehoerige Verbindungen erlauben (Antworten auf vom +# Router selbst initiierte Verbindungen, z.B. DNS-Upstream, apk-Updates). +iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT +iptables -A INPUT -m conntrack --ctstate INVALID -j DROP + +# Aus dem internen Netz duerfen Clients nur die Dienste erreichen, die der +# Router fuer sie bereitstellt: DHCP (67/udp) und DNS (53 udp/tcp). +iptables -A INPUT -i "$INT_IF" -p udp --dport 67 -j ACCEPT +iptables -A INPUT -i "$INT_IF" -p udp --dport 53 -j ACCEPT +iptables -A INPUT -i "$INT_IF" -p tcp --dport 53 -j ACCEPT + +# ICMP-Echo aus dem internen Netz fuer Diagnose (Router anpingbar). Optional. +iptables -A INPUT -i "$INT_IF" -p icmp --icmp-type echo-request -j ACCEPT + +# WICHTIG: Es gibt bewusst KEINE Regel, die SSH (22) oder HTTP (80/443) an den +# Router erlaubt -- weder von intern noch von extern. Vom externen Interface +# (eth1) wird ohnehin nichts angenommen (Default-Policy DROP). Damit ist der +# Router nur ueber die VirtualBox-Konsole administrierbar. + +# ----------------------------------------------------------------------------- +# FORWARD -- durchgeleiteter Verkehr (Clients <-> Internet) +# ----------------------------------------------------------------------------- +# Rueckverkehr fuer bestehende Verbindungen erlauben. +iptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT +iptables -A FORWARD -m conntrack --ctstate INVALID -j DROP + +# Private/nicht-oeffentliche Zielbereiche aus dem internen Netz heraus blocken. +for net in $PRIVATE_NETS; do + iptables -A FORWARD -i "$INT_IF" -o "$EXT_IF" -d "$net" -j DROP +done + +# Alles uebrige aus dem internen Netz ins externe Netz (= oeffentliches +# Internet) erlauben. +iptables -A FORWARD -i "$INT_IF" -o "$EXT_IF" -j ACCEPT + +# ----------------------------------------------------------------------------- +# NAT -- Quelladressen der Clients auf die externe Router-Adresse maskieren +# ----------------------------------------------------------------------------- +iptables -t nat -A POSTROUTING -o "$EXT_IF" -j MASQUERADE + +# ============================================================================= +# IPv6 -- komplett dichtmachen +# ============================================================================= +# Der Router filtert nur IPv4. Damit IPv6 die Regeln nicht umgeht, wird +# saemtlicher IPv6-Verkehr unterbunden (ausser dem fuer den lokalen Link +# zwingend noetigen ICMPv6 auf dem Router selbst). +ip6tables -F +ip6tables -X +ip6tables -P INPUT DROP +ip6tables -P FORWARD DROP +ip6tables -P OUTPUT ACCEPT + +ip6tables -A INPUT -i lo -j ACCEPT +ip6tables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT +# ICMPv6 ist fuer ein funktionierendes lokales IPv6 (NDP) noetig; FORWARD +# bleibt aber komplett gesperrt, daher kein IPv6-Internet fuer Clients. +ip6tables -A INPUT -p ipv6-icmp -j ACCEPT + +echo "Firewall-Regeln angewendet." From 9d593a99ff8d10cb83a03a7557fcbde1eb3e1ea6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 29 May 2026 10:06:39 +0000 Subject: [PATCH 04/19] Add main setup-nat-router.sh orchestrator script --- setup-nat-router.sh | 157 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100755 setup-nat-router.sh diff --git a/setup-nat-router.sh b/setup-nat-router.sh new file mode 100755 index 0000000..7b53f8c --- /dev/null +++ b/setup-nat-router.sh @@ -0,0 +1,157 @@ +#!/bin/sh +# +# setup-nat-router.sh -- Richtet eine Alpine-Linux-VM als abgeschotteten +# NAT-Router fuer ein internes VirtualBox-Netzwerk ein. +# +# Was das Skript tut: +# 1. Prueft Voraussetzungen (root, Alpine/apk) +# 2. Installiert iptables, ip6tables und dnsmasq +# 3. Schreibt /etc/network/interfaces (eth0 statisch, eth1 DHCP) +# 4. Aktiviert IPv4-Forwarding (sysctl) und deaktiviert IPv6-Forwarding +# 5. Konfiguriert dnsmasq (DHCP + DNS nur intern) +# 6. Installiert und aktiviert die Firewall (firewall.sh) und speichert die +# Regeln persistent ueber den iptables-/ip6tables-Dienst +# 7. Schaltet sshd ab (Router nur ueber Konsole administrierbar) +# 8. Aktiviert die Dienste beim Boot (rc-update) +# 9. Persistiert die Konfiguration mit 'lbu commit' (Alpine diskless) +# +# Aufruf (als root, ueber die VirtualBox-Konsole): +# sh setup-nat-router.sh +# +# Interface-/Netz-Variablen lassen sich per Environment ueberschreiben: +# INT_IF=eth0 EXT_IF=eth1 LAN_IP=10.0.99.1 sh setup-nat-router.sh +# +set -eu + +# --- Konfiguration ----------------------------------------------------------- +INT_IF="${INT_IF:-eth0}" +EXT_IF="${EXT_IF:-eth1}" +LAN_IP="${LAN_IP:-10.0.99.1}" +LAN_MASK="${LAN_MASK:-255.255.255.0}" +LAN_NET="${LAN_NET:-10.0.99.0/24}" +DHCP_START="${DHCP_START:-10.0.99.100}" +DHCP_END="${DHCP_END:-10.0.99.200}" +DHCP_LEASE="${DHCP_LEASE:-12h}" + +# Verzeichnis dieses Skripts (fuer Zugriff auf config/ und firewall.sh). +# shellcheck disable=SC1007 # 'CDPATH= cd' ist beabsichtigt (CDPATH leeren) +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +INSTALL_DIR="/etc/nat-router" + +# --- Hilfsfunktionen --------------------------------------------------------- +log() { echo ">>> $*"; } +warn() { echo "!!! $*" >&2; } +die() { echo "FEHLER: $*" >&2; exit 1; } + +backup() { + # Sichert eine existierende Datei einmalig mit .orig-Endung. + if [ -f "$1" ] && [ ! -f "$1.orig" ]; then + cp -a "$1" "$1.orig" + log "Backup angelegt: $1.orig" + fi +} + +# --- 1. Voraussetzungen pruefen --------------------------------------------- +[ "$(id -u)" -eq 0 ] || die "Bitte als root ausfuehren." +command -v apk >/dev/null 2>&1 || die "apk nicht gefunden -- dies ist kein Alpine Linux." + +log "Setze sed-Platzhalter aus folgenden Werten:" +log " internes IF : $INT_IF ($LAN_IP / $LAN_MASK, Netz $LAN_NET)" +log " externes IF : $EXT_IF (DHCP via VirtualBox-NAT)" +log " DHCP-Bereich: $DHCP_START - $DHCP_END (Lease $DHCP_LEASE)" + +# --- 2. Pakete installieren -------------------------------------------------- +log "Installiere Pakete (iptables, ip6tables, dnsmasq)..." +apk update +apk add --no-cache iptables ip6tables dnsmasq + +# --- 3. /etc/network/interfaces --------------------------------------------- +log "Schreibe /etc/network/interfaces..." +backup /etc/network/interfaces +sed -e "s/^auto eth0/auto $INT_IF/" \ + -e "s/^iface eth0/iface $INT_IF/" \ + -e "s/^auto eth1/auto $EXT_IF/" \ + -e "s/^iface eth1/iface $EXT_IF/" \ + -e "s/10\.0\.99\.1/$LAN_IP/" \ + -e "s/255\.255\.255\.0/$LAN_MASK/" \ + "$SCRIPT_DIR/config/interfaces" > /etc/network/interfaces + +# --- 4. sysctl: Forwarding --------------------------------------------------- +log "Aktiviere IPv4-Forwarding, deaktiviere IPv6-Forwarding..." +install -d /etc/sysctl.d +cp "$SCRIPT_DIR/config/sysctl-router.conf" /etc/sysctl.d/99-nat-router.conf +# Sofort anwenden (ohne Reboot abzuwarten). +sysctl -p /etc/sysctl.d/99-nat-router.conf >/dev/null + +# --- 5. dnsmasq -------------------------------------------------------------- +log "Schreibe /etc/dnsmasq.conf..." +backup /etc/dnsmasq.conf +sed -e "s/^interface=eth0/interface=$INT_IF/" \ + -e "s/^except-interface=eth1/except-interface=$EXT_IF/" \ + -e "s/^listen-address=10\.0\.99\.1/listen-address=$LAN_IP/" \ + -e "s/^dhcp-range=10\.0\.99\.100,10\.0\.99\.200,12h/dhcp-range=$DHCP_START,$DHCP_END,$DHCP_LEASE/" \ + -e "s/^dhcp-option=3,10\.0\.99\.1/dhcp-option=3,$LAN_IP/" \ + -e "s/^dhcp-option=6,10\.0\.99\.1/dhcp-option=6,$LAN_IP/" \ + "$SCRIPT_DIR/config/dnsmasq.conf" > /etc/dnsmasq.conf + +# --- 6. Firewall installieren und anwenden ----------------------------------- +log "Installiere Firewall-Skript nach $INSTALL_DIR/firewall.sh..." +install -d "$INSTALL_DIR" +install -m 0755 "$SCRIPT_DIR/firewall.sh" "$INSTALL_DIR/firewall.sh" + +log "Wende Firewall-Regeln jetzt an..." +INT_IF="$INT_IF" EXT_IF="$EXT_IF" LAN_NET="$LAN_NET" LAN_IP="$LAN_IP" \ + sh "$INSTALL_DIR/firewall.sh" + +log "Speichere Regeln persistent (iptables/ip6tables save)..." +# Der OpenRC-Dienst legt die Regeln in /etc/iptables/ ab und stellt sie beim +# Boot wieder her -- so ist firewall.sh beim Start nicht zwingend noetig. +/etc/init.d/iptables save || rc-service iptables save || true +/etc/init.d/ip6tables save || rc-service ip6tables save || true + +# --- 7. SSH abschalten ------------------------------------------------------- +log "Stelle sicher, dass sshd nicht laeuft und nicht startet..." +rc-update del sshd 2>/dev/null || true +rc-service sshd stop 2>/dev/null || true + +# --- 8. Dienste beim Boot aktivieren ---------------------------------------- +log "Aktiviere Dienste beim Boot (rc-update)..." +rc-update add iptables default +rc-update add ip6tables default +rc-update add dnsmasq default +rc-update add networking boot 2>/dev/null || true + +log "Starte dnsmasq neu..." +rc-service dnsmasq restart || rc-service dnsmasq start + +# --- 9. Persistenz fuer Alpine diskless (lbu) ------------------------------- +if command -v lbu >/dev/null 2>&1; then + log "Persistiere Konfiguration mit 'lbu commit' (Alpine diskless)..." + # /etc ist bei diskless standardmaessig in der lbu-Sicherung enthalten. + # firewall.sh liegt unter /etc/nat-router und wird damit ebenfalls gesichert. + lbu commit -d || warn "lbu commit fehlgeschlagen -- bei sys-Installation ignorierbar." +else + log "lbu nicht vorhanden -- vermutlich sys-Installation auf Platte, keine Persistenz noetig." +fi + +cat < $LAN_IP/$LAN_MASK ($LAN_NET) + Externes Interface : $EXT_IF -> DHCP (VirtualBox-NAT) + DHCP fuer Clients : $DHCP_START - $DHCP_END, DNS/Gateway = $LAN_IP + + Pruefen: + iptables -L -n -v + iptables -t nat -L -n -v + rc-status + cat /var/lib/misc/dnsmasq.leases + + Hinweise: + * SSH/HTTP sind bewusst NICHT erreichbar -- Administration nur via Konsole. + * Firewall neu anwenden: sh $INSTALL_DIR/firewall.sh && /etc/init.d/iptables save + * Bei Aenderungen ggf. erneut 'lbu commit -d' ausfuehren. +============================================================================ +EOF From 02788dce93803a643dd49256ca513fd5d6c378a6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 29 May 2026 10:07:24 +0000 Subject: [PATCH 05/19] Add README with VirtualBox setup and usage guide --- README.md | 176 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a9e36c --- /dev/null +++ b/README.md @@ -0,0 +1,176 @@ +# Alpine NAT-Router für VirtualBox + +Skripte und Konfigurationsvorlagen, um eine **Alpine-Linux-VM** in VirtualBox als +**abgeschotteten NAT-Router** einzurichten. Client-VMs in einem internen +VirtualBox-Netzwerk erhalten darüber **ausschließlich Internetzugriff** — alle +anderen privaten Adressbereiche werden blockiert. Die Router-VM ist so +abgeschottet, dass **kein SSH und kein HTTP** darauf erreichbar sind; die +Administration erfolgt nur über die VirtualBox-Konsole. + +## Warum Alpine Linux? + +- **Extrem schlank** (~130 MB auf Disk, ab 128 MB RAM) — ideal für eine + dedizierte Router-VM ohne Desktop. +- **Security-first**: musl libc, BusyBox, minimale Angriffsfläche by default. +- **Kein SSH/HTTP by default** — es läuft nur, was man explizit startet. +- **Vollständig über die Konsole** bedienbar (`setup-alpine`). +- Bringt `iptables`/`nftables` und `dnsmasq` direkt aus den Repos mit. + +## Netzwerk-Topologie + +``` ++----------------+ +-------------------+ +------------------+ +| Client-VMs | | Alpine Router | | VirtualBox Host | +| eth0 | | eth0 (intern) | | (Internet) | +| Internal Net +--------+ 10.0.99.1/24 | | | +| 10.0.99.x | | eth1 (extern) +--------+ NAT-Adapter | ++----------------+ +-------------------+ +------------------+ +``` + +| Komponente | Adapter 1 | Adapter 2 | +|--------------|---------------------------------|--------------------| +| Router-VM | `Internal Network` `labnet` → `eth0` | `NAT` → `eth1` | +| Client-VMs | `Internal Network` `labnet` | — (keiner) | + +> **Wichtig:** Die Reihenfolge der Adapter in den VM-Einstellungen bestimmt die +> Interface-Namen (`eth0`, `eth1`). Adapter 1 muss das interne Netz sein. + +## Inhalt des Repos + +| Datei | Zweck | +|-----------------------------|-------------------------------------------------------------| +| `setup-nat-router.sh` | Hauptskript: installiert & konfiguriert alles | +| `firewall.sh` | Eigenständiges, idempotentes iptables/ip6tables-Ruleset | +| `config/interfaces` | Vorlage für `/etc/network/interfaces` | +| `config/dnsmasq.conf` | Vorlage für `/etc/dnsmasq.conf` (DHCP + DNS, nur intern) | +| `config/sysctl-router.conf` | IP-Forwarding (IPv4 an, IPv6 aus) + Hardening | +| `CLAUDE.md` | Projektkontext und Konventionen | + +## Schritt 1 — VirtualBox vorbereiten + +### Router-VM anlegen + +1. Neue VM erstellen: Typ *Linux*, Version *Other Linux (64-bit)*, 512 MB RAM, + ~2 GB Disk reichen. +2. **Netzwerk → Adapter 1**: „Angeschlossen an" = **Internal Network**, + Name = `labnet`. +3. **Netzwerk → Adapter 2**: „Angeschlossen an" = **NAT**. +4. Alpine-ISO (Standard oder Virtual) als optisches Laufwerk einhängen. + +### Client-VMs anlegen + +- **Netzwerk → Adapter 1**: „Angeschlossen an" = **Internal Network**, + Name = `labnet`. Kein weiterer Adapter. + +## Schritt 2 — Alpine installieren + +1. Router-VM starten, als `root` einloggen (kein Passwort beim Live-Boot). +2. `setup-alpine` ausführen: + - Tastaturlayout/Hostname nach Wunsch. + - Beim Netzwerk: `eth1` per `dhcp` konfigurieren (für die Installation und + spätere Updates wird Internet benötigt); `eth0` kann zunächst übersprungen + werden — das übernimmt später dieses Setup. + - **SSH-Server:** `none` wählen (kein OpenSSH installieren). + - Bei „Which disk(s) to use" für eine dauerhafte Installation eine Platte + wählen und `sys` als Modus — danach von Platte booten. +3. Reboot, ISO aushängen, als `root` einloggen. + +## Schritt 3 — Skripte auf die Router-VM bringen + +Da die VM nur über die Konsole bedient wird, eine dieser Optionen nutzen: + +**Variante A — direkt herunterladen (während eth1 Internet hat):** + +```sh +apk add git +git clone nat-router +cd nat-router +``` + +**Variante B — VirtualBox „Shared Folder"** mit dem Repo-Ordner einbinden und +von dort kopieren. + +**Variante C — Dateien einzeln per `vi` aus dem Repo abtippen/einfügen** +(zur Not, aber A/B sind komfortabler). + +## Schritt 4 — Setup ausführen + +```sh +sh setup-nat-router.sh +``` + +Das Skript: + +1. installiert `iptables`, `ip6tables`, `dnsmasq`, +2. schreibt `/etc/network/interfaces` (eth0 statisch, eth1 DHCP), +3. aktiviert IPv4-Forwarding, deaktiviert IPv6-Forwarding (+ Hardening-sysctls), +4. konfiguriert `dnsmasq` (DHCP + DNS, **nur** intern), +5. wendet die Firewall an und speichert sie persistent, +6. schaltet `sshd` ab, +7. aktiviert die Dienste beim Boot, +8. persistiert alles mit `lbu commit` (bei Alpine diskless). + +### Abweichende Netzwerte + +Variablen lassen sich per Environment überschreiben, z.B.: + +```sh +INT_IF=eth0 EXT_IF=eth1 LAN_IP=10.0.50.1 LAN_NET=10.0.50.0/24 \ + DHCP_START=10.0.50.100 DHCP_END=10.0.50.200 sh setup-nat-router.sh +``` + +## Schritt 5 — Reboot & Test + +```sh +reboot +``` + +Nach dem Neustart auf dem **Router** prüfen: + +```sh +ip a # eth0 = 10.0.99.1, eth1 = DHCP-Adresse +iptables -L -n -v # Policies DROP, FORWARD-Regeln vorhanden +iptables -t nat -L -n -v # MASQUERADE auf eth1 +rc-status # iptables, ip6tables, dnsmasq laufen +cat /var/lib/misc/dnsmasq.leases # vergebene Leases +``` + +Auf einem **Client** (nur `labnet`): + +```sh +ip a # Adresse aus 10.0.99.100-200 +ip route # default via 10.0.99.1 +ping -c1 1.1.1.1 # Internet erreichbar +nslookup example.com # DNS funktioniert +ping -c1 192.168.1.1 # MUSS scheitern (privates Netz geblockt) +``` + +## Sicherheitsmodell + +- **Default-Policy `DROP`** für `INPUT` und `FORWARD`. +- Am Router lauscht **kein** administrativer Dienst von außen — kein SSH, kein + HTTP. Erlaubt sind aus dem internen Netz nur DHCP (67/udp) und DNS (53), + plus optional ICMP-Echo zur Diagnose. +- **`dnsmasq`** bindet ausschließlich an das interne Interface + (`bind-interfaces` + `except-interface`), niemals an `eth1`. +- **Private Zielbereiche** werden im Forwarding verworfen: + `10/8`, `172.16/12`, `192.168/16`, `100.64/10` (CGNAT), `169.254/16` + (Link-Local). Alles andere (öffentliches Internet) ist erlaubt. +- **IPv6** ist deaktiviert/geblockt, damit es die IPv4-Filter nicht umgeht. + +> Hinweis: Client-VMs im selben `Internal Network` können einander auf +> Layer 2 weiterhin direkt erreichen — das findet ohne den Router statt und +> lässt sich am Router nicht unterbinden. Für vollständige Isolation jeder +> Client-VM separate Internal Networks verwenden. + +## Wartung + +```sh +# Firewall-Regeln neu anwenden (z.B. nach Anpassung): +sh /etc/nat-router/firewall.sh +/etc/init.d/iptables save +/etc/init.d/ip6tables save + +# Bei Alpine diskless Änderungen dauerhaft sichern: +lbu commit -d +``` From 1f718ff0390b77f3243bd78f3fd3f816af583f4d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 22:33:26 +0000 Subject: [PATCH 06/19] Make rc-update service enabling idempotent 'rc-update add' exits non-zero when the service is already installed in the runlevel, which killed the script (set -e) on a second run. Check the OpenRC runlevel symlink first via a small enable_service helper. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015eHMkP8gw2XMC4ikGMNAw8 --- setup-nat-router.sh | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/setup-nat-router.sh b/setup-nat-router.sh index 7b53f8c..a6e052d 100755 --- a/setup-nat-router.sh +++ b/setup-nat-router.sh @@ -51,6 +51,17 @@ backup() { fi } +enable_service() { + # 'rc-update add' schlaegt fehl, wenn der Dienst bereits im Runlevel + # eingetragen ist -- fuer Idempotenz daher vorher pruefen. + # OpenRC legt pro Runlevel einen Symlink unter /etc/runlevels/ ab. + if [ -e "/etc/runlevels/$2/$1" ]; then + log "Dienst $1 ist bereits im Runlevel $2." + else + rc-update add "$1" "$2" + fi +} + # --- 1. Voraussetzungen pruefen --------------------------------------------- [ "$(id -u)" -eq 0 ] || die "Bitte als root ausfuehren." command -v apk >/dev/null 2>&1 || die "apk nicht gefunden -- dies ist kein Alpine Linux." @@ -116,10 +127,10 @@ rc-service sshd stop 2>/dev/null || true # --- 8. Dienste beim Boot aktivieren ---------------------------------------- log "Aktiviere Dienste beim Boot (rc-update)..." -rc-update add iptables default -rc-update add ip6tables default -rc-update add dnsmasq default -rc-update add networking boot 2>/dev/null || true +enable_service iptables default +enable_service ip6tables default +enable_service dnsmasq default +enable_service networking boot log "Starte dnsmasq neu..." rc-service dnsmasq restart || rc-service dnsmasq start From 056b8efb187fddaad339fbaa96b6e099eb17612c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 22:33:45 +0000 Subject: [PATCH 07/19] Bring up internal interface before starting dnsmasq The README recommends skipping eth0 during setup-alpine, so the interface had no address until reboot. dnsmasq (bind-interfaces + listen-address) then failed to bind and the script aborted before 'lbu commit', losing the whole configuration on diskless installs. Cycle the interface with ifdown/ifup right after writing /etc/network/interfaces so the new config is applied immediately. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015eHMkP8gw2XMC4ikGMNAw8 --- setup-nat-router.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/setup-nat-router.sh b/setup-nat-router.sh index a6e052d..4f4d839 100755 --- a/setup-nat-router.sh +++ b/setup-nat-router.sh @@ -87,6 +87,14 @@ sed -e "s/^auto eth0/auto $INT_IF/" \ -e "s/255\.255\.255\.0/$LAN_MASK/" \ "$SCRIPT_DIR/config/interfaces" > /etc/network/interfaces +# Internes Interface sofort aktivieren: bei der Alpine-Installation wird eth0 +# laut README uebersprungen und haette sonst bis zum Reboot keine Adresse -- +# dnsmasq (bind-interfaces, listen-address=$LAN_IP) koennte dann nicht binden +# und das Setup wuerde vor dem 'lbu commit' abbrechen. +log "Aktiviere internes Interface $INT_IF ($LAN_IP)..." +ifdown "$INT_IF" 2>/dev/null || true +ifup "$INT_IF" || die "Konnte $INT_IF nicht aktivieren -- Adapter 1 pruefen." + # --- 4. sysctl: Forwarding --------------------------------------------------- log "Aktiviere IPv4-Forwarding, deaktiviere IPv6-Forwarding..." install -d /etc/sysctl.d From 1e3dbaf7899cfa3700abaec06f88ef476b358d8c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 22:34:05 +0000 Subject: [PATCH 08/19] Do not abort setup when sysctl rejects individual keys sysctl -p exits non-zero if a key is unavailable (e.g. net.ipv6.* with the IPv6 module not loaded), which killed the script under set -e. Use sysctl -e to ignore unknown keys and downgrade remaining errors to a warning. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015eHMkP8gw2XMC4ikGMNAw8 --- setup-nat-router.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/setup-nat-router.sh b/setup-nat-router.sh index 4f4d839..609e946 100755 --- a/setup-nat-router.sh +++ b/setup-nat-router.sh @@ -99,8 +99,11 @@ ifup "$INT_IF" || die "Konnte $INT_IF nicht aktivieren -- Adapter 1 pruefen." log "Aktiviere IPv4-Forwarding, deaktiviere IPv6-Forwarding..." install -d /etc/sysctl.d cp "$SCRIPT_DIR/config/sysctl-router.conf" /etc/sysctl.d/99-nat-router.conf -# Sofort anwenden (ohne Reboot abzuwarten). -sysctl -p /etc/sysctl.d/99-nat-router.conf >/dev/null +# Sofort anwenden (ohne Reboot abzuwarten). '-e' ignoriert unbekannte Keys, +# z.B. net.ipv6.* wenn das IPv6-Modul nicht geladen ist -- sonst wuerde der +# Exit-Code von sysctl das Skript abbrechen (set -e). +sysctl -e -p /etc/sysctl.d/99-nat-router.conf >/dev/null \ + || warn "sysctl meldete Fehler -- Werte pruefen mit: sysctl -a | grep forward" # --- 5. dnsmasq -------------------------------------------------------------- log "Schreibe /etc/dnsmasq.conf..." From 9fceca8b96506d882fc039f6af2defc2421c6cc7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 22:34:22 +0000 Subject: [PATCH 09/19] Restrict FORWARD accept and MASQUERADE to the LAN source network The forward accept and masquerade rules matched only on interfaces, so packets with spoofed source addresses were forwarded and NATed too. Require -s $LAN_NET on both rules (the variable was previously unused). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015eHMkP8gw2XMC4ikGMNAw8 --- firewall.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/firewall.sh b/firewall.sh index 8b831f5..ca2b811 100755 --- a/firewall.sh +++ b/firewall.sh @@ -95,13 +95,15 @@ for net in $PRIVATE_NETS; do done # Alles uebrige aus dem internen Netz ins externe Netz (= oeffentliches -# Internet) erlauben. -iptables -A FORWARD -i "$INT_IF" -o "$EXT_IF" -j ACCEPT +# Internet) erlauben. Die Quelladresse muss aus dem LAN stammen -- gespoofte +# Absender werden so zusaetzlich zum rp_filter verworfen. +iptables -A FORWARD -i "$INT_IF" -o "$EXT_IF" -s "$LAN_NET" -j ACCEPT # ----------------------------------------------------------------------------- # NAT -- Quelladressen der Clients auf die externe Router-Adresse maskieren # ----------------------------------------------------------------------------- -iptables -t nat -A POSTROUTING -o "$EXT_IF" -j MASQUERADE +# Nur Verkehr aus dem internen Netz maskieren, nicht beliebige Quellen. +iptables -t nat -A POSTROUTING -s "$LAN_NET" -o "$EXT_IF" -j MASQUERADE # ============================================================================= # IPv6 -- komplett dichtmachen From 2f5d5a4795c1347448c2007bd954c4012852bebc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 22:34:51 +0000 Subject: [PATCH 10/19] Block all reserved/bogon destination ranges, not just RFC 1918 Extend PRIVATE_NETS with 0/8, loopback, 192.0.0/24, the TEST-NETs, 198.18/15 (benchmarking), multicast and 240/4 so clients really only reach the public internet, and document the full list in the README. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015eHMkP8gw2XMC4ikGMNAw8 --- README.md | 6 ++++-- firewall.sh | 10 ++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0a9e36c..cbfe2ff 100644 --- a/README.md +++ b/README.md @@ -153,9 +153,11 @@ ping -c1 192.168.1.1 # MUSS scheitern (privates Netz geblockt) plus optional ICMP-Echo zur Diagnose. - **`dnsmasq`** bindet ausschließlich an das interne Interface (`bind-interfaces` + `except-interface`), niemals an `eth1`. -- **Private Zielbereiche** werden im Forwarding verworfen: +- **Private/reservierte Zielbereiche** werden im Forwarding verworfen: `10/8`, `172.16/12`, `192.168/16`, `100.64/10` (CGNAT), `169.254/16` - (Link-Local). Alles andere (öffentliches Internet) ist erlaubt. + (Link-Local) sowie die übrigen Bogon-Bereiche `0/8`, `127/8`, `192.0.0/24`, + die TEST-NETs, `198.18/15` (Benchmarking), `224/4` (Multicast) und `240/4`. + Alles andere (öffentliches Internet) ist erlaubt. - **IPv6** ist deaktiviert/geblockt, damit es die IPv4-Filter nicht umgeht. > Hinweis: Client-VMs im selben `Internal Network` können einander auf diff --git a/firewall.sh b/firewall.sh index ca2b811..18d4068 100755 --- a/firewall.sh +++ b/firewall.sh @@ -24,8 +24,14 @@ EXT_IF="${EXT_IF:-eth1}" # externes Interface (Internet via NAT) LAN_NET="${LAN_NET:-10.0.99.0/24}" # internes Subnetz LAN_IP="${LAN_IP:-10.0.99.1}" # eigene Adresse auf dem internen Interface -# Private/nicht-internetfaehige Zielbereiche, die geblockt werden sollen. -PRIVATE_NETS="10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 100.64.0.0/10 169.254.0.0/16" +# Private/reservierte (Bogon-)Zielbereiche, die geblockt werden sollen: +# RFC 1918 (10/8, 172.16/12, 192.168/16), CGNAT (100.64/10), +# Link-Local (169.254/16), "this network" (0/8), Loopback (127/8), +# IETF-Protokollzuweisungen (192.0.0/24), TEST-NET-1/2/3, +# Benchmarking (198.18/15), Multicast (224/4), Reserved/Broadcast (240/4). +PRIVATE_NETS="10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 100.64.0.0/10 \ +169.254.0.0/16 0.0.0.0/8 127.0.0.0/8 192.0.0.0/24 192.0.2.0/24 \ +198.51.100.0/24 203.0.113.0/24 198.18.0.0/15 224.0.0.0/4 240.0.0.0/4" # --- Hilfsfunktionen --------------------------------------------------------- need() { From bc214fb88b85194f806303d42a24ee251d643939 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 22:35:12 +0000 Subject: [PATCH 11/19] Enable DNS rebind protection in dnsmasq Add stop-dns-rebind/rebind-localhost-ok as defense-in-depth against upstream answers pointing at private IPs, and clarify the upstream resolver comment (DHCP-provided resolv.conf is used in addition; offer a commented-out no-resolv for fixed-servers-only setups). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015eHMkP8gw2XMC4ikGMNAw8 --- config/dnsmasq.conf | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/config/dnsmasq.conf b/config/dnsmasq.conf index f0180d2..6b56b53 100644 --- a/config/dnsmasq.conf +++ b/config/dnsmasq.conf @@ -17,8 +17,14 @@ listen-address=10.0.99.1 domain-needed # Reverse-Lookups fuer private Adressen nicht nach oben weiterleiten. bogus-priv -# Anfragen nur an Upstream-Server schicken, nicht /etc/hosts-Streuung. -# Upstream-Resolver (vom NAT bezogenes resolv.conf wird zusaetzlich genutzt). +# DNS-Rebind-Schutz: Upstream-Antworten mit privaten IPs verwerfen +# (Defense-in-Depth; die Firewall blockt solche Ziele ohnehin). +stop-dns-rebind +rebind-localhost-ok +# Feste Upstream-Resolver. Zusaetzlich nutzt dnsmasq die per DHCP auf eth1 +# bezogene /etc/resolv.conf (VirtualBox-DNS). Wer NUR die festen Server +# unten verwenden will, aktiviert zusaetzlich: +#no-resolv server=9.9.9.9 server=1.1.1.1 From 9a0c917cbfd7d2aeebc0c1ed8b834a2996024c68 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 22:35:33 +0000 Subject: [PATCH 12/19] Add conf.default.* counterparts to hardening sysctls Only conf.all.* was set for redirects, source routing and IPv6 forwarding, so interfaces appearing later would inherit kernel defaults. Set the default.* keys as well and offer commented-out disable_ipv6 switches for fully IPv6-free setups. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015eHMkP8gw2XMC4ikGMNAw8 --- config/sysctl-router.conf | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/config/sysctl-router.conf b/config/sysctl-router.conf index 9193eb2..c3a89b8 100644 --- a/config/sysctl-router.conf +++ b/config/sysctl-router.conf @@ -6,7 +6,14 @@ net.ipv4.ip_forward = 1 # IPv6-Forwarding bewusst deaktivieren. Der Router filtert nur IPv4; # ohne diese Zeile koennte IPv6-Verkehr die IPv4-Regeln umgehen. +# 'default' gilt fuer kuenftig auftauchende Interfaces. net.ipv6.conf.all.forwarding = 0 +net.ipv6.conf.default.forwarding = 0 + +# Optional: IPv6 komplett deaktivieren (konsequenter als nur zu blocken; +# ip6tables-Regeln sind dann Redundanz). Bei Bedarf einkommentieren: +#net.ipv6.conf.all.disable_ipv6 = 1 +#net.ipv6.conf.default.disable_ipv6 = 1 # Reverse-Path-Filterung (Anti-Spoofing) im Strict-Modus. net.ipv4.conf.all.rp_filter = 1 @@ -14,8 +21,12 @@ net.ipv4.conf.default.rp_filter = 1 # Keine ICMP-Redirects annehmen oder senden. net.ipv4.conf.all.accept_redirects = 0 +net.ipv4.conf.default.accept_redirects = 0 net.ipv4.conf.all.send_redirects = 0 +net.ipv4.conf.default.send_redirects = 0 net.ipv4.conf.all.secure_redirects = 0 +net.ipv4.conf.default.secure_redirects = 0 # Source-Routing ablehnen. net.ipv4.conf.all.accept_source_route = 0 +net.ipv4.conf.default.accept_source_route = 0 From 9a7df0c3af8a9dcc957c1dba88af1b13670a23a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 22:35:49 +0000 Subject: [PATCH 13/19] Flush ip6tables nat and mangle tables like their IPv4 counterparts The IPv4 section flushed nat/mangle but the IPv6 section only flushed filter, leaving stale rules behind on re-runs. Guard with || true since the IPv6 nat table does not exist on all kernels. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015eHMkP8gw2XMC4ikGMNAw8 --- firewall.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/firewall.sh b/firewall.sh index 18d4068..5e8153b 100755 --- a/firewall.sh +++ b/firewall.sh @@ -119,6 +119,11 @@ iptables -t nat -A POSTROUTING -s "$LAN_NET" -o "$EXT_IF" -j MASQUERADE # zwingend noetigen ICMPv6 auf dem Router selbst). ip6tables -F ip6tables -X +# nat/mangle wie bei IPv4 mitleeren (nat existiert nicht auf allen Kerneln). +ip6tables -t nat -F 2>/dev/null || true +ip6tables -t nat -X 2>/dev/null || true +ip6tables -t mangle -F 2>/dev/null || true +ip6tables -t mangle -X 2>/dev/null || true ip6tables -P INPUT DROP ip6tables -P FORWARD DROP ip6tables -P OUTPUT ACCEPT From 4a6f2c44b34cac1488e0835e21f63e36882c3e1c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 22:36:18 +0000 Subject: [PATCH 14/19] Reject blocked private destinations instead of silently dropping Clients now get an immediate icmp-net-prohibited error instead of connection timeouts when hitting a blocked range. Stealth towards the own LAN is not a goal; external traffic still hits the DROP policy. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015eHMkP8gw2XMC4ikGMNAw8 --- README.md | 5 +++-- firewall.sh | 6 +++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cbfe2ff..ce3e0ed 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ ip a # Adresse aus 10.0.99.100-200 ip route # default via 10.0.99.1 ping -c1 1.1.1.1 # Internet erreichbar nslookup example.com # DNS funktioniert -ping -c1 192.168.1.1 # MUSS scheitern (privates Netz geblockt) +ping -c1 192.168.1.1 # MUSS scheitern: sofort "Network unreachable/prohibited" ``` ## Sicherheitsmodell @@ -153,7 +153,8 @@ ping -c1 192.168.1.1 # MUSS scheitern (privates Netz geblockt) plus optional ICMP-Echo zur Diagnose. - **`dnsmasq`** bindet ausschließlich an das interne Interface (`bind-interfaces` + `except-interface`), niemals an `eth1`. -- **Private/reservierte Zielbereiche** werden im Forwarding verworfen: +- **Private/reservierte Zielbereiche** werden im Forwarding abgewiesen + (`REJECT`, Clients erhalten sofort eine Fehlermeldung statt Timeouts): `10/8`, `172.16/12`, `192.168/16`, `100.64/10` (CGNAT), `169.254/16` (Link-Local) sowie die übrigen Bogon-Bereiche `0/8`, `127/8`, `192.0.0/24`, die TEST-NETs, `198.18/15` (Benchmarking), `224/4` (Multicast) und `240/4`. diff --git a/firewall.sh b/firewall.sh index 5e8153b..4b73d84 100755 --- a/firewall.sh +++ b/firewall.sh @@ -96,8 +96,12 @@ iptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT iptables -A FORWARD -m conntrack --ctstate INVALID -j DROP # Private/nicht-oeffentliche Zielbereiche aus dem internen Netz heraus blocken. +# REJECT statt DROP: Clients erhalten sofort "net prohibited" statt in +# Timeouts zu laufen. Stealth gegenueber dem eigenen LAN ist kein Ziel; +# von extern gilt weiterhin die Default-Policy DROP. for net in $PRIVATE_NETS; do - iptables -A FORWARD -i "$INT_IF" -o "$EXT_IF" -d "$net" -j DROP + iptables -A FORWARD -i "$INT_IF" -o "$EXT_IF" -d "$net" \ + -j REJECT --reject-with icmp-net-prohibited done # Alles uebrige aus dem internen Netz ins externe Netz (= oeffentliches From 2064cfaf71de9f376077b463e2f211ad0a510447 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 22:36:35 +0000 Subject: [PATCH 15/19] Use iptables -w to wait for the xtables lock Avoids spurious failures when another process holds the xtables lock while the ruleset is being applied. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015eHMkP8gw2XMC4ikGMNAw8 --- firewall.sh | 68 ++++++++++++++++++++++++++--------------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/firewall.sh b/firewall.sh index 4b73d84..34b4b0a 100755 --- a/firewall.sh +++ b/firewall.sh @@ -51,37 +51,37 @@ echo "Wende Firewall-Regeln an (intern=$INT_IF, extern=$EXT_IF, lan=$LAN_NET)... # ============================================================================= # --- Tabellen leeren --- -iptables -F -iptables -X -iptables -t nat -F -iptables -t nat -X -iptables -t mangle -F 2>/dev/null || true -iptables -t mangle -X 2>/dev/null || true +iptables -w -F +iptables -w -X +iptables -w -t nat -F +iptables -w -t nat -X +iptables -w -t mangle -F 2>/dev/null || true +iptables -w -t mangle -X 2>/dev/null || true # --- Default-Policies: alles verbieten, was nicht ausdruecklich erlaubt ist --- -iptables -P INPUT DROP -iptables -P FORWARD DROP -iptables -P OUTPUT ACCEPT +iptables -w -P INPUT DROP +iptables -w -P FORWARD DROP +iptables -w -P OUTPUT ACCEPT # ----------------------------------------------------------------------------- # INPUT -- Verkehr AN den Router selbst # ----------------------------------------------------------------------------- # Loopback erlauben. -iptables -A INPUT -i lo -j ACCEPT +iptables -w -A INPUT -i lo -j ACCEPT # Bereits etablierte/zugehoerige Verbindungen erlauben (Antworten auf vom # Router selbst initiierte Verbindungen, z.B. DNS-Upstream, apk-Updates). -iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT -iptables -A INPUT -m conntrack --ctstate INVALID -j DROP +iptables -w -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT +iptables -w -A INPUT -m conntrack --ctstate INVALID -j DROP # Aus dem internen Netz duerfen Clients nur die Dienste erreichen, die der # Router fuer sie bereitstellt: DHCP (67/udp) und DNS (53 udp/tcp). -iptables -A INPUT -i "$INT_IF" -p udp --dport 67 -j ACCEPT -iptables -A INPUT -i "$INT_IF" -p udp --dport 53 -j ACCEPT -iptables -A INPUT -i "$INT_IF" -p tcp --dport 53 -j ACCEPT +iptables -w -A INPUT -i "$INT_IF" -p udp --dport 67 -j ACCEPT +iptables -w -A INPUT -i "$INT_IF" -p udp --dport 53 -j ACCEPT +iptables -w -A INPUT -i "$INT_IF" -p tcp --dport 53 -j ACCEPT # ICMP-Echo aus dem internen Netz fuer Diagnose (Router anpingbar). Optional. -iptables -A INPUT -i "$INT_IF" -p icmp --icmp-type echo-request -j ACCEPT +iptables -w -A INPUT -i "$INT_IF" -p icmp --icmp-type echo-request -j ACCEPT # WICHTIG: Es gibt bewusst KEINE Regel, die SSH (22) oder HTTP (80/443) an den # Router erlaubt -- weder von intern noch von extern. Vom externen Interface @@ -92,28 +92,28 @@ iptables -A INPUT -i "$INT_IF" -p icmp --icmp-type echo-request -j ACCEPT # FORWARD -- durchgeleiteter Verkehr (Clients <-> Internet) # ----------------------------------------------------------------------------- # Rueckverkehr fuer bestehende Verbindungen erlauben. -iptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT -iptables -A FORWARD -m conntrack --ctstate INVALID -j DROP +iptables -w -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT +iptables -w -A FORWARD -m conntrack --ctstate INVALID -j DROP # Private/nicht-oeffentliche Zielbereiche aus dem internen Netz heraus blocken. # REJECT statt DROP: Clients erhalten sofort "net prohibited" statt in # Timeouts zu laufen. Stealth gegenueber dem eigenen LAN ist kein Ziel; # von extern gilt weiterhin die Default-Policy DROP. for net in $PRIVATE_NETS; do - iptables -A FORWARD -i "$INT_IF" -o "$EXT_IF" -d "$net" \ + iptables -w -A FORWARD -i "$INT_IF" -o "$EXT_IF" -d "$net" \ -j REJECT --reject-with icmp-net-prohibited done # Alles uebrige aus dem internen Netz ins externe Netz (= oeffentliches # Internet) erlauben. Die Quelladresse muss aus dem LAN stammen -- gespoofte # Absender werden so zusaetzlich zum rp_filter verworfen. -iptables -A FORWARD -i "$INT_IF" -o "$EXT_IF" -s "$LAN_NET" -j ACCEPT +iptables -w -A FORWARD -i "$INT_IF" -o "$EXT_IF" -s "$LAN_NET" -j ACCEPT # ----------------------------------------------------------------------------- # NAT -- Quelladressen der Clients auf die externe Router-Adresse maskieren # ----------------------------------------------------------------------------- # Nur Verkehr aus dem internen Netz maskieren, nicht beliebige Quellen. -iptables -t nat -A POSTROUTING -s "$LAN_NET" -o "$EXT_IF" -j MASQUERADE +iptables -w -t nat -A POSTROUTING -s "$LAN_NET" -o "$EXT_IF" -j MASQUERADE # ============================================================================= # IPv6 -- komplett dichtmachen @@ -121,21 +121,21 @@ iptables -t nat -A POSTROUTING -s "$LAN_NET" -o "$EXT_IF" -j MASQUERADE # Der Router filtert nur IPv4. Damit IPv6 die Regeln nicht umgeht, wird # saemtlicher IPv6-Verkehr unterbunden (ausser dem fuer den lokalen Link # zwingend noetigen ICMPv6 auf dem Router selbst). -ip6tables -F -ip6tables -X +ip6tables -w -F +ip6tables -w -X # nat/mangle wie bei IPv4 mitleeren (nat existiert nicht auf allen Kerneln). -ip6tables -t nat -F 2>/dev/null || true -ip6tables -t nat -X 2>/dev/null || true -ip6tables -t mangle -F 2>/dev/null || true -ip6tables -t mangle -X 2>/dev/null || true -ip6tables -P INPUT DROP -ip6tables -P FORWARD DROP -ip6tables -P OUTPUT ACCEPT - -ip6tables -A INPUT -i lo -j ACCEPT -ip6tables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT +ip6tables -w -t nat -F 2>/dev/null || true +ip6tables -w -t nat -X 2>/dev/null || true +ip6tables -w -t mangle -F 2>/dev/null || true +ip6tables -w -t mangle -X 2>/dev/null || true +ip6tables -w -P INPUT DROP +ip6tables -w -P FORWARD DROP +ip6tables -w -P OUTPUT ACCEPT + +ip6tables -w -A INPUT -i lo -j ACCEPT +ip6tables -w -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT # ICMPv6 ist fuer ein funktionierendes lokales IPv6 (NDP) noetig; FORWARD # bleibt aber komplett gesperrt, daher kein IPv6-Internet fuer Clients. -ip6tables -A INPUT -p ipv6-icmp -j ACCEPT +ip6tables -w -A INPUT -p ipv6-icmp -j ACCEPT echo "Firewall-Regeln angewendet." From d8f6c854c6b089389d97a6a0d5f423fb57b20816 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 22:36:59 +0000 Subject: [PATCH 16/19] Anchor sed substitutions for the interfaces template The unanchored patterns for address/netmask would also match prefixes of longer values (e.g. 10.0.99.1 inside 10.0.99.100) if the template ever grows. Match the whole address/netmask line instead. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015eHMkP8gw2XMC4ikGMNAw8 --- setup-nat-router.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup-nat-router.sh b/setup-nat-router.sh index 609e946..082bc67 100755 --- a/setup-nat-router.sh +++ b/setup-nat-router.sh @@ -83,8 +83,8 @@ sed -e "s/^auto eth0/auto $INT_IF/" \ -e "s/^iface eth0/iface $INT_IF/" \ -e "s/^auto eth1/auto $EXT_IF/" \ -e "s/^iface eth1/iface $EXT_IF/" \ - -e "s/10\.0\.99\.1/$LAN_IP/" \ - -e "s/255\.255\.255\.0/$LAN_MASK/" \ + -e "s/^\([[:space:]]*address[[:space:]]*\)10\.0\.99\.1$/\1$LAN_IP/" \ + -e "s/^\([[:space:]]*netmask[[:space:]]*\)255\.255\.255\.0$/\1$LAN_MASK/" \ "$SCRIPT_DIR/config/interfaces" > /etc/network/interfaces # Internes Interface sofort aktivieren: bei der Alpine-Installation wird eth0 From c7851595a4b20120f5647bdf2441f6cfaf244c7d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 22:37:12 +0000 Subject: [PATCH 17/19] Clarify configuration summary log message Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015eHMkP8gw2XMC4ikGMNAw8 --- setup-nat-router.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup-nat-router.sh b/setup-nat-router.sh index 082bc67..a435bef 100755 --- a/setup-nat-router.sh +++ b/setup-nat-router.sh @@ -66,7 +66,7 @@ enable_service() { [ "$(id -u)" -eq 0 ] || die "Bitte als root ausfuehren." command -v apk >/dev/null 2>&1 || die "apk nicht gefunden -- dies ist kein Alpine Linux." -log "Setze sed-Platzhalter aus folgenden Werten:" +log "Verwende folgende Konfiguration:" log " internes IF : $INT_IF ($LAN_IP / $LAN_MASK, Netz $LAN_NET)" log " externes IF : $EXT_IF (DHCP via VirtualBox-NAT)" log " DHCP-Bereich: $DHCP_START - $DHCP_END (Lease $DHCP_LEASE)" From a543a2b1944e6dba430d4710b46d7e02f9b00d53 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 22:37:30 +0000 Subject: [PATCH 18/19] Add MIT license Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015eHMkP8gw2XMC4ikGMNAw8 --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9646197 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 roemer2201 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 77d5c12e5fbd04087bf98163e90b5b6622cb7d71 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 22:37:48 +0000 Subject: [PATCH 19/19] Add CI workflow: shellcheck (POSIX sh) and dnsmasq config test Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015eHMkP8gw2XMC4ikGMNAw8 --- .github/workflows/ci.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..90cf01a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: CI + +on: + push: + pull_request: + +jobs: + lint: + name: ShellCheck (POSIX sh) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # Alpine nutzt BusyBox ash, daher strikt als POSIX sh pruefen. + - name: shellcheck + run: shellcheck -s sh setup-nat-router.sh firewall.sh + + dnsmasq-config: + name: dnsmasq config syntax + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # dnsmasq-base enthaelt das Binary ohne den systemd-Dienst + # (der volle dnsmasq-Dienst kollidiert auf dem Runner mit Port 53). + - name: install dnsmasq-base + run: sudo apt-get update && sudo apt-get install -y dnsmasq-base + - name: dnsmasq --test + run: dnsmasq --test --conf-file=config/dnsmasq.conf