Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
57615a7
Add CLAUDE.md with project context and conventions
claude May 29, 2026
30047be
Add network, sysctl and dnsmasq config templates
claude May 29, 2026
c6eb52f
Add idempotent iptables/ip6tables firewall ruleset
claude May 29, 2026
9d593a9
Add main setup-nat-router.sh orchestrator script
claude May 29, 2026
02788dc
Add README with VirtualBox setup and usage guide
claude May 29, 2026
1f718ff
Make rc-update service enabling idempotent
claude Jul 6, 2026
056b8ef
Bring up internal interface before starting dnsmasq
claude Jul 6, 2026
1e3dbaf
Do not abort setup when sysctl rejects individual keys
claude Jul 6, 2026
9fceca8
Restrict FORWARD accept and MASQUERADE to the LAN source network
claude Jul 6, 2026
2f5d5a4
Block all reserved/bogon destination ranges, not just RFC 1918
claude Jul 6, 2026
bc214fb
Enable DNS rebind protection in dnsmasq
claude Jul 6, 2026
9a0c917
Add conf.default.* counterparts to hardening sysctls
claude Jul 6, 2026
9a7df0c
Flush ip6tables nat and mangle tables like their IPv4 counterparts
claude Jul 6, 2026
4a6f2c4
Reject blocked private destinations instead of silently dropping
claude Jul 6, 2026
2064cfa
Use iptables -w to wait for the xtables lock
claude Jul 6, 2026
d8f6c85
Anchor sed substitutions for the interfaces template
claude Jul 6, 2026
c785159
Clarify configuration summary log message
claude Jul 6, 2026
a543a2b
Add MIT license
claude Jul 6, 2026
77d5c12
Add CI workflow: shellcheck (POSIX sh) and dnsmasq config test
claude Jul 6, 2026
65501d2
Merge pull request #2 from roemer2201/claude/repository-review-arod2d
roemer2201 Jul 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
71 changes: 71 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -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.
179 changes: 179 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# 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 <REPO-URL> 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: sofort "Network unreachable/prohibited"
```

## 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/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`.
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
```
41 changes: 41 additions & 0 deletions config/dnsmasq.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# /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
# 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

# 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
20 changes: 20 additions & 0 deletions config/interfaces
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions config/sysctl-router.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# /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.
# '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
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
Loading
Loading