Warum überhaupt
Die Firewall am Standort ist ein Blechkasten. Irgendwann muss die Hardware ersetzt werden — Mainboard, Netzteil, Speicher. Wer das an einem Abend macht, hat eine Ausfallzeit, die er nicht beeinflussen kann.
Also erst der Umzug in eine VM auf dem vorhandenen Proxmox-Host: Der Host läuft, die Storage-Pools liegen da, Snapshots sind billig. Damit ist die Firewall von der Hardware entkoppelt, und für den späteren Blech-Tausch bleibt Zeit. Kein Termin, kein Fenster, kein Risiko — die VM läuft währenddessen weiter.
Das ist der eigentliche Grund. Nicht „Container sind modern", sondern: Zeit kaufen für eine Migration, die sonst ein Ausfall wäre.
Ehrlich bleibt: der Cutover selbst kostet ein paar Minuten Ausfall. Nicht wegen eines Kabels — das steckt man vorher um. Sondern weil die alte Büchse heruntergefahren und die VM mit genau derselben Konfiguration aktiviert wird: gleiche LAN-IP, gleiche WAN-IP, gleiche Tunnel-Adressen. Bis der Wechsel durch ist, gibt es kein Gateway. Was man gewinnt, ist der zweite Schritt: der spätere Hardware-Tausch der Büchse braucht keinen Termin mehr, weil die Firewall dann schon woanders läuft.
Das Netz-Design
Ein Proxmox-Host, zwei Bridges:
eno1 ── vmbr0 ── LAN 192.168.66.4/24 (VLAN-aware, aber untagged benutzt)
eno2 ── vmbr1 ── Trunk 802.1Q: 100 = WAN, untagged = IoT
Die OPNsense-VM bekommt drei virtio-NICs:
| NIC | Bridge | Proxmox-Tag | Rolle | IP |
|---|---|---|---|---|
| vtnet0 | vmbr0 | — | LAN | 192.168.66.1/24 |
| vtnet1 | vmbr1 | 100 | WAN | 192.168.178.240/24 |
| vtnet2 | vmbr1 | — | IoT | 192.168.77.1/24 |
Der springende Punkt ist vtnet1 und vtnet2: beide hängen an derselben Bridge, also an demselben Kabel eno2. Was sie trennt, ist ein einziger Tag.
Wie das VLAN den Weg zur FritzBox baut
Die FritzBox steht im Netz 192.168.178.0/24 und macht den Uplink. Die Firewall-VM ist ihr einziger Client auf diesem Segment.
Auf dem Proxmox-Host ist vmbr1 VLAN-aware konfiguriert:
# /etc/network/interfaces
auto vmbr1
iface vmbr1 inet manual
bridge-ports eno2
bridge-stp off
bridge-fd 0
bridge-vlan-aware yes
bridge-vids 2-4094
bridge-vids 2-4094 macht aus eno2 einen Trunk: Jede VLAN-ID darf getaggt raus. Die VM-NIC mit tag=100 erzeugt am Tap-Port genau ein VLAN:
# bridge vlan show
port vlan-id
tap104i0 1 PVID Egress Untagged # LAN, vmbr0
tap104i1 100 PVID Egress Untagged # WAN, vmbr1
tap104i2 1 PVID Egress Untagged # IoT, vmbr1
Das ist der Teil, an dem man sich leicht verrennt: Im Gast sieht man kein VLAN. vtnet1 hat ein ganz normales Ethernet-Interface mit einer IP, kein vlan100-Pseudo-Interface, kein Tag in ifconfig. Das Tag setzt und entfernt der Host:
- VM → Host: Paket kommt untagged am Tap an, der Tap gehört zu PVID 100, die Bridge legt Tag 100 an.
- Host → Kabel: Das Frame verlässt
eno2als 802.1Q-Frame mit VLAN 100. - Switch: Der Port, an dem die FritzBox hängt, ist Access auf VLAN 100 — dort wird das Tag wieder entfernt. Die FritzBox sieht ein völlig normales Ethernet-Segment und merkt nichts von VLANs.
- Rückweg identisch, nur andersrum.
Auf dem Kabel liegen damit zwei Netze, die sich nie sehen: getaggt VLAN 100 (WAN zur FritzBox) und untagged (IoT-Geräte). Der IoT-Verkehr kann nicht versehentlich ins WAN-Segment bluten, weil dazwischen ein Tag fehlt.
Im Gast prüft man das nicht mit VLAN-Tools, sondern mit dem, was zählt — erreicht die VM ihr Gateway?
# auf der OPNsense
arp -an | grep 192.168.178.1
netstat -rn -f inet | grep default
? (192.168.178.1) at 0a:de:ad:be:ef:01 on vtnet1 expires in 1199 seconds
default 192.168.178.1 UGS vtnet1
Ein ARP-Eintrag auf vtnet1 heißt: L2 funktioniert, das Tag wird auf beiden Seiten korrekt behandelt. Kommt kein ARP-Eintrag, obwohl die IP stimmt, ist fast immer der Switch-Port falsch (Access-VLAN vs. Trunk) oder das Tag in der VM-Konfiguration.
# auf dem Proxmox-Host: welches Tag hängt an welchem Tap?
bridge vlan show | head -20
Expected Output (OK): Der WAN-Tap zeigt 100 PVID Egress Untagged, der Trunk eno2 listet 2-4094 als getaggt.
Der Switch-Teil: Port 9 ist die FritzBox
Der Switch ist ein web-managed HPE OfficeConnect 1920-24G. Über die CLI kommt man an die VLANs nicht heran:
User view commands:
initialize Delete the startup configuration file and reboot system
ipsetup Assign an IP address to VLAN-interface 1
password Specify password of local user
ping Ping function
quit Exit from current command view
reboot Reboot system/board/card
summary Display summary information of the device.
telnet Establish one TELNET connection
upgrade Upgrade the system boot file and the Boot ROM program
Kein display, kein show — die VLAN-Verwaltung liegt komplett im Web-Frontend.
Zugang gibt es also, nur nicht über SSH. Wer wie selbstverständlich https://<switch> aufruft, lauscht in die Röhre: Es gibt keinen HTTPS-Listener. Der Kasten spricht HTTP auf Port 80 und leitet / per 301 auf index.htm weiter — mehr nicht:
HTTP/1.1 301 Moved Permanently
Location: index.htm
Server: Httpd
Wer nur https probiert, hält den Switch für unerreichbar und baut sich einen Workaround (so wie ich, über SNMP). Der Login läuft über http://<switch>/index.htm mit dem Admin-Konto plus Verifikationscode.
Bleibt die Web-UI (Login mit Verifikationscode) — oder, wenn man nur lesen will, SNMP. Read-only-Community, Standard-MIBs, keine Sonderbehandlung:
# welche VLANs gibt es?
snmpwalk -v2c -c public 192.168.66.26 1.3.6.1.2.1.17.7.1.4.3.1.1
# PVID pro Port (das "untagged-VLAN" des Ports)
snmpwalk -v2c -c public 192.168.66.26 1.3.6.1.2.1.17.7.1.4.5.1.1
Expected Output (gekürzt):
VLAN 0001, VLAN 0070, VLAN 0100
.1.3.6.1.2.1.17.7.1.4.5.1.1.8 = Gauge32: 70
.1.3.6.1.2.1.17.7.1.4.5.1.1.9 = Gauge32: 100
Damit ist die Topologie belegt:
| Port | PVID | Rolle |
|---|---|---|
| Port 8 | 70 | Trunk zum Proxmox-Host: untagged IoT, getaggt VLAN 100 |
| Port 9 | 100 | Access zur FritzBox |
Und hier steckt die Falle, die man in keinem VLAN-Tutorial findet: Das Label ist lokal. Dasselbe untagged Segment heißt auf dem Switch VLAN 70 und auf der Proxmox-Bridge VLAN 1. Beide Seiten sind sich einig, weil sie sich auf nichts einigen müssen — untagged ist untagged. Nur der eine getaggte Verkehr braucht eine gemeinsame Nummer, und das ist 100.
Wer nach der VLAN-Nummer des IoT-Netzes sucht und auf der Bridge „1" findet, sucht am falschen Gerät. Die einzige Nummer, die über Gerätegrenzen hinweg gilt, ist die, die im Frame steht.
Ein Hinweis, der Zeit spart: Beim Cutover übernimmt die VM die Adressen der alten Firewall — sonst müssten alle Clients, Reservierungen und Regeln angepasst werden. Genau deshalb dürfen beide nie gleichzeitig im selben Segment stehen: zwei Geräte mit derselben IP und dasselbe Gateway doppelt im ARP ist ein Adresskonflikt, und der sieht wie ein Firewall-Fehler aus. Erst die alte Instanz sauber herunterfahren, dann die neue hochziehen.
Die Cutover-Reihenfolge
Der Umzug lief an einem Abend, in dieser Reihenfolge (nachvollziehbar an den Config-Backups auf der Box):
| Zeit | Schritt |
|---|---|
| 14:29 | WireGuard-Baustellen zuerst: bestehende Tunnel geradeziehen |
| 17:53 | Tunnel-/Interface-Zuweisung in der neuen VM |
| 18:04 | Cutover-Vorbereitung: Regeln, Aliase, Interfaces vollständig |
| 18:23 | Cutover — alte Büchse aus, VM mit denselben Adressen aktiv |
| 18:45 | NordVPN-Peer (der Tunnel über den Provider) |
| 19:07 | DNS-Pfad über VPN |
| 20:05 | API-Zugang für Automatisierung |
| +1 Tag | DNSBL/unbound, Feinschliff |
Die Lehre steckt im ersten Punkt: Alles, was nicht zwingend zum Cutover gehört, wird vorher gemacht. Tunnel, Regeln, Aliase und Interfaces waren fertig, bevor das Kabel umgesteckt wurde. Zwischen 18:23 und dem ersten Test lagen Minuten, nicht Stunden.
Und trotzdem gab es einen Zwischenzustand: Für etwa zehn Hosts wurde der Verkehr vorübergehend komplett über den WAN-Uplink geschickt („vorerst WAN, Site-to-Site noch nicht"). Das ist legitim, wenn der Tunnel noch nicht steht — aber diese Regeln haben kein Verfallsdatum. Einer dieser Hosts lief Wochen später in genau so eine Regel und kam nicht mehr ins Core-Netz. Solche Übergangslösungen brauchen ein Ticket und ein Datum, sonst werden sie zur Zeitbombe.
Warum eine VM hier funktioniert, ein Container aber nicht
Eine Firewall braucht echte Netzwerkinterfaces mit eigenen MACs, ARP, Promiscuous-Verhalten und Kernel-Routing. Ein Container teilt den Kernel und das Interface-Modell des Hosts — durchreichen lässt sich das nur mit Tricks (macvlan, Host-Netz), und dann fehlt die saubere Trennung, die eine Firewall gerade ausmacht.
Die VM dagegen hat drei virtio-NICs, eigene MACs und ein eigenes Netz-Stack. Für den Host ist sie ein normaler Gast, für den Switch ein normaler Teilnehmer.
Was man dabei kaputt macht
Drei Dinge sind beim Umzug aufgefallen, alle unsichtbar:
- Die MAC des Gateways steckt in
netconsole, inwakeonlanund in DHCP-Reservierungen. Neues Gateway = neue MAC.netconsoleist fire-and-forget: Es gibt keinen Retry und keine Warnung, die Crash-Forensik war monatelang „aktiv" und lieferte nichts. - Die Gateway-Regeln sind die halbe Wahrheit. Nach dem Umzug war die Regelbasis scheinbar in Ordnung, aber eine per-Host-Policy-Route (
route-toauf die WAN-Schnittstelle) hebelte die Tunnel-Route für genau einen Host aus. Kein Log, kein Fehler — nur ein Host, der cross-subnet nichts erreicht. - Config schreiben ist nicht Config wirken. Ein
filter reloadmit Rückgabecode 0 sagt nur, dass der Reload lief. Ob die kompilierte Regelbasis noch dem entspricht, was man wollte, steht inpfctl -sr.
Checkliste: Firewall auf Proxmox umziehen
- Nicht-Cutover-Arbeit vorher erledigen (Tunnel, Regeln, Aliase, Interfaces)
- Bridges VLAN-aware (
bridge-vlan-aware yes,bridge-vids 2-4094), Trunk-Port am Switch - VM-NIC mit
tag=für das WAN-VLAN, Access-Port am Switch zum Router - Adressen der alten Firewall übernehmen — und sicherstellen, dass nie beide gleichzeitig laufen (sonst Adresskonflikt)
- Im Gast prüfen: ARP auf dem WAN-Interface + Default-Route — nicht nach VLAN-Interfaces suchen
- Übergangsregeln („vorerst WAN") mit Ticket und Ablaufdatum versehen
- MAC-abhängige Dienste nachziehen:
netconsole,wakeonlan, DHCP-Reservierungen, Switch-ACLs - Nach dem Cutover:
pfctl -srgegen die Erwartung diffen, nicht nur den Reload-RC ansehen - Switch-Konfiguration dokumentieren (bei mir noch offen)