Growing the infrastructure · Part 1 of 4

Following a packet through my Proxmox bridges and Sophos

I traced one connection from the laptop's uplink through three Proxmox bridges and Sophos. This is the map I use to explain where each rule applies, and where traffic never meets the firewall at all.

I wanted to understand which path a connection was taking, so I started at the Ethernet uplink and followed it through Proxmox. That gave me a clearer way to explain where Sophos applies its rules, and it turned a list of containers into something I could reason about.

This entry is that walkthrough, using my lab record of 8 November 2026. Addresses, MAC addresses and identifiers are left out; bridge names, VLAN IDs and guest IDs are kept because they help you follow the path.

A laptop behind CGNAT

The whole lab runs on one laptop: an Acer Aspire A514 with an i5-1135G7 and about 36 GiB of RAM, running Proxmox VE 9.2 (the version my record reports). Upstream is a shared ISP router behind carrier-grade NAT, so I cannot put it in bridge mode or forward ports into the lab. That one constraint explains a lot of the design: the Minecraft server reaches players through VPS tunnels, and remote access uses connectors that dial out.

Three bridges, one firewall

A Proxmox bridge behaves like a virtual switch. Anything attached to the same bridge can reach anything else on it without passing a firewall, so which bridge a guest sits on matters as much as its rules.

  • vmbr0, the WAN bridge, sits on the laptop’s onboard NIC. Sophos’s WAN port is here, and so is the host’s recovery management address.
  • vmbr1, the LAN bridge, sits on a USB Ethernet adapter and carries the legacy untagged LAN. Most containers, the Proxmox web UI and both private access connectors live here.
  • vmbr2, the VLAN trunk, is an internal VLAN-aware bridge attached to a trunk port on Sophos. Seven VLANs are defined on it: 5 MGMT, 10 SERVERS, 20 LAB_TEST, 30 TRUSTED, 40 SOC, 50 DMZ and 60 IOT_GUEST.

Sophos Firewall runs as VM 102 with one virtual NIC on each bridge. It is the gateway for the LAN and for every VLAN, with IPS and third-party threat feeds enabled.

Packet flow
  1. WANISP, behind CGNAT · Configured
  2. Laptop uplinkonboard NIC · Configured
  3. WAN bridgevmbr0 · Configured
  4. Sophos FirewallVM 102 · Running
  5. LAN bridgevmbr1 · untagged · Configured
Lab map · addresses and hostnames left out

Outside traffic crosses the WAN bridge and Sophos before it reaches the LAN bridge. Once two guests share a bridge, their traffic does not cross Sophos at all, which is why I do not claim that everything passes through the firewall.

Open the full lab console

What the VLANs actually hold

All seven VLANs are configured on Sophos Port3, the interface that sits on vmbr2, and each has its own /24 gateway. The legacy untagged LAN on vmbr1 is a separate network with its own addressing. Most guests still live on that LAN; the SafeLine container (LXC 202, stopped) is tagged on VLAN 20, and VLAN 50 is the DMZ where this journal now runs, in CT 410. No access point or switch port tags these VLANs, so they are internal guest networks inside the laptop, not Wi-Fi or wired segments yet.

VLANs on Port3 / vmbr2as configured on 6 Nov 2026
VLANNameGatewayUsed for
5MGMT/24 on SophosManagement interfaces
10SERVERS/24 on SophosServer guests
20LAB_TEST/24 on SophosThesis and Kali environment; SafeLine (LXC 202) is tagged here
30TRUSTED/24 on SophosTrusted devices
40SOC/24 on SophosDetection tools
50DMZ/24 on SophosThis journal’s origin, CT 410 (confirmed)
60IOT_GUEST/24 on SophosGuests; internal only, no AP or switch tags it

Lease example, VLAN 20: gateway 192.0.2.1/24 · lease 192.0.2.57 · DNS 192.0.2.1

Example addresses (RFC 5737), not my lab’s. The legacy untagged LAN on vmbr1 is separate and not shown.

Where an exception has to sit

Sophos reads its rules top to bottom and stops at the first one that matches. That makes the order a design decision, not a list:

Firewall rule ordertop to bottom · first match wins
  1. 1DMZ_SITE_UPDATESallowDMZ (network object) → WAN · HTTP, HTTPS, custom Cloudflare service (TCP/UDP 7844)Lets the DMZ reach the internet for package updates and for the Cloudflare tunnel.The one exception the DMZ needs, placed above the drops so it is evaluated first. Its source is the whole DMZ object, not only CT 410.
  2. 2DROP_NEW_VLANSdrop · logthe newly added VLAN zones (list truncated on screen) → Any · AnyDrops and logs traffic from the new VLANs until each one has its own allow rule.A new VLAN starts closed. The screenshot cuts the source list short, so I only claim the zones I can read.
  3. 3DROP_LAB20_OUTBOUNDdrop · logLAB_TEST (VLAN 20) → Any · AnyDrops and logs everything leaving the thesis and Kali network.Test guests must not reach the internet or my management interfaces.
  4. 4Allow_Minecraft_WireGuardallowMinecraft LXC host object → the VPS host object · custom WireGuard serviceLets the Minecraft server’s WireGuard tunnel reach the public VPS.A scoped exception for one host and one service.
  5. 5LAN_TO_WANallowlegacy LAN → WAN · AnyLets the legacy untagged LAN reach the internet.The broad rule goes low, so narrower rules above it decide first.
  6. 6Automatic MTA policygeneratedas generated → as generated · SMTP servicesCreated by Sophos for its mail transfer agent; accept, as shown.Generated by the firewall; I left it as it was created.
  7. 7Implicit dropdropAny → Any · AnyAnything no rule above matched is dropped.The default when nothing allows the traffic.

A scoped exception only works if Sophos evaluates it before an applicable drop. DMZ_SITE_UPDATES sits first for that reason. If VLAN 20 ever needs DNS, that allow rule goes above DROP_LAB20_OUTBOUND, or the drop matches first and the allow never runs.

DMZ_SITE_UPDATES sits first because it is the exception the DMZ needs: HTTP, HTTPS and the Cloudflare tunnel service out to the WAN. The two drops come next, scoped to the networks I want closed: new VLANs, and VLAN 20’s outbound traffic. Allow_Minecraft_WireGuard sits below them and still works, because the Minecraft server’s tunnel comes from the LAN, which neither drop matches. The order would matter the moment I wanted VLAN 20 to reach one thing, say a DNS server: that allow has to go above DROP_LAB20_OUTBOUND, or the drop matches first and the allow never runs. I tested what the VLAN 20 drop actually matched in the NOC case.

What is running

Proxmox guests as recorded on 8 Nov 2026
IDTypeRoleState when recordedNote
100LXCtemplate-lxcStopped on 8 Nov 2026
102VMSophos FirewallRunning on 8 Nov 2026
201LXCOlder Cloudflare TunnelStopped on 8 Nov 2026
202LXCSafeLineStopped on 8 Nov 2026tagged VLAN 20
203LXCCloudflare TunnelRunning on 8 Nov 2026
204LXCNetBird routing peerRunning on 8 Nov 2026
205LXCwazuhStopped on 8 Nov 2026stopped on 1 Nov 2026; not listed on 8 Nov
206LXCBunkerWeb / private proxyRunning on 8 Nov 2026proxy routes not verified
207LXCMinecraft and WireGuard tunnelsRunning on 8 Nov 2026
208LXCTwingate connectorRunning on 8 Nov 2026
209LXCNextDNS forwarderRunning on 8 Nov 2026
301VMKali Red (exercise VM)Stopped on 8 Nov 2026
302VMKali Blue (exercise VM)Stopped on 8 Nov 2026
410LXChomelab-journal (Caddy + cloudflared)Running on 8 Nov 2026VLAN 50 DMZ, confirmed; added after the 8 Nov record
bashProxmox host · root shellExample output · substituted names and addresses

Purpose: list VMs and their state

I wanted to confirm which VMs were up before tracing anything, because a stopped firewall would make the rest of the walkthrough meaningless.

qm list
Show output
      VMID NAME        STATUS
     102 sophos      running
     301 kali-red    stopped
     302 kali-blue   stopped
  1. 102 sophos the gateway for every bridge; if this is down, nothing routes

Laid out from my lab record rather than pasted from a session. Containers have their own command, pct list.

bashProxmox host · root shellExample output · substituted names and addresses

Purpose: see which bridge each Sophos NIC uses

To explain where Sophos applies its rules, I needed to know which bridge each of its ports was on.

qm config 102 | grep ^net
Show output
net0: virtio=<mac>,bridge=vmbr1
net1: virtio=<mac>,bridge=vmbr0
net2: virtio=<mac>,bridge=vmbr2
  1. bridge=vmbr2 the trunk: no tag on the NIC, so every VLAN reaches Sophos's trunk port

MAC addresses redacted. The mapping of NICs to bridges is from my record.

Ways in that are not the front door

Because of CGNAT, every way in starts with an outbound connection.

The Cloudflare Tunnel in LXC 203 is running and kept for recovery access; an older tunnel, LXC 201, is stopped. NetBird in LXC 204 is a routing peer and the Twingate connector in LXC 208 runs as a native service; both grant the Proxmox and Sophos web UIs to an admin group only. I compare them in Who can reach what.

The other containers on the LAN bridge

  • SafeLine in LXC 202 is stopped on VLAN 20.
  • BunkerWeb in LXC 206 is running with its dashboard reachable. Its proxy routes are not verified, so I do not count it as protecting anything on this map.
  • NextDNS in LXC 209 is the DNS forwarder for the LAN. How that path works is in What my DNS logs can and cannot tell me.

What the map is good for

Following one connection from the cable inward gave me a short answer to the question of where rules apply: Sophos applies its rules where traffic crosses from one bridge or VLAN to another, and nowhere else. Two guests on the same bridge talk directly, and Sophos never sees it. The lab console draws the same map with the other paths on it.

Takeaway: before trusting a firewall rule to protect a service, trace which bridge each end sits on. If they share one, the rule is not in the path.

Skills used

  • Reading Linux bridge and VLAN layout on Proxmox
  • Working out which hops a firewall can and cannot see
  • Mapping private access paths onto the network

All skills, grouped by what job postings ask for

Tools in this entry

Every tool and its documentation