Homelab Network Fabric — WireGuard + Tailscale, pfSense + VLANs, Self-Hosted Mesh

NetworkingWireGuardTailscaleHeadscalepfSenseNixOSSplit DNS
Image 1

What it is

Two VPN layers in parallel, both declared from my NixOS flake, stitching every node in my homelab into one routable fabric: a router-level WireGuard site-to-site tunnel (VPS ↔ pfSense) for public-to-home traffic, and a device-level Tailscale mesh (via self-hosted Headscale) for direct peer-to-peer access between every machine I own. pfSense VLANs segment the physical LAN; subnet routers bridge the mesh to non-Tailscale devices.

Why it exists

The two layers answer two different questions:

  • WireGuard (Layer 1) answers "how does my public VPS talk to my home LAN as if they're on the same network?". It's a permanent, always-on tunnel at the router level. Nginx on the VPS proxies subdomains through this tunnel to home-LAN services — no port forwarding on the home router, no dependency on a SaaS tunnel provider.
  • Tailscale / Headscale (Layer 2) answers "how does any single device I own reach any other, from anywhere, without punching holes?". It's a mesh overlay with per-device identity, where every peer gets a 100.64.0.x address regardless of physical location. My laptop in a café can SSH to my NAS through the mesh without caring whether it's at home or abroad.

Building both is intentional. WireGuard handles the high-bandwidth, router-level flows (reverse proxy → home services, NAS ↔ VPS backups). Tailscale handles device identity + ACLs + zero-config connectivity for mobile devices. They overlay the same physical network without conflicting.

Architecture — subnets & routing

Subnet legend

RangePurpose
192.168.8.0/24Main home LAN (workstations, Proxmox, LXCs)
192.168.20.0/24Storage VLAN (NAS, media clients)
172.26.5.0/24WireGuard site-to-site + road warriors
100.64.0.0/10Tailscale / Headscale mesh overlay
10.0.0.0/8rootless Docker slirp4netns NAT (VPS internal)
172.16.0.0/12Docker bridge networks (VPS internal)

What I built

  • WireGuard site-to-site tunnel (Layer 1) — VPS server at 172.26.5.155/24, pfSense peer at 172.26.5.1, allowedIPs = 192.168.8.0/24 · 192.168.20.0/24 · 172.26.5.1/32. pfSense outbound-NAT rule masquerades home traffic back through the tunnel so return paths work.
  • NFTables masquerade for road warriors — migrated off iptables because rootless Docker's iptables manipulation clobbered UFW; NFTables rules operate independently. Rule: oifname eth0 ip saddr 172.26.5.0/24 masquerade.
  • Self-hosted Headscale (Layer 2 control plane) — runs on the VPS, replaces SaaS Tailscale coordination. Every NixOS profile in my flake (workstations, laptops, VPS, NAS, Proxmox LXCs, nix-darwin macOS) enrolls via declarative services.tailscale config. pfSense also joins as a Tailscale node with IP 100.64.0.7.
  • Subnet routers bridge the mesh to non-Tailscale devicesNAS_PROD and LXC_tailscale both advertise 192.168.8.0/24 + 192.168.20.0/24. A peer joining the mesh from outside the LAN can reach home-LAN devices that don't run Tailscale.
  • Split DNS over TailscaleheadscaleDnsSplit = { "*.local.akunito.com" = [ "100.64.0.7" ]; } routes internal domain queries to pfSense's Tailscale IP so names like nas.local.akunito.com resolve from anywhere on the mesh.
  • Nginx reverse proxy via WireGuard — every *.akunito.com subdomain terminates on the VPS Nginx, then proxy_passes through the WG interface to a home-LAN service. Replaced a fleet of Cloudflare Tunnel containers (one per service) with a single Nginx stack.
  • Edge defense — Fail2Ban on the VPS reads real visitor IPs from CF-Connecting-IP, bans propagate to Cloudflare's edge via API, so attackers are blocked at both the host and the CDN.
  • MTU hygiene — all WireGuard peers standardized to MTU 1280; kernel rp_filter = 2 so asymmetric routing through the site-to-site doesn't get dropped.
  • Declarative secrets — WireGuard private keys, preshared keys, Headscale keys all live in git-crypt-encrypted files under secrets/; they're referenced declaratively by profile configs and installed via NixOS activation scripts.
  • pfSense as a declarative neighbor — while pfSense itself isn't NixOS, its Tailscale endpoint is treated as a first-class citizen in the flake (via SSH-managed config + Prometheus exporter on VPS). pfSense backup is pulled nightly to the NAS via rsync over SSH.

Results

  • One public entry point (VPS) + one mesh (Headscale) = full-fleet connectivity. No port forwarding on the home router.
  • Subdomains on WireGuard → home services via Nginx: cheaper, auditable, no SaaS dependency for the tunnel path.
  • Mesh covers every device I own via self-hosted Headscale — phones, laptops, VPS, NAS, LXCs, pfSense — with split DNS for *.local.akunito.com.
  • Declarative end-to-end — the entire network layer rebuilds from the NixOS flake; WireGuard and Tailscale configs are version-controlled alongside service definitions.
  • Cost — ~€7/mo Netcup VPS covers the public-facing layer; Headscale replaces a SaaS tier that would otherwise scale with device count.

Stack

NixOS, WireGuard, Tailscale, Headscale (self-hosted), Nginx, NFTables, pfSense, Certbot, Fail2Ban, Cloudflare API, systemd, git-crypt, ZFS (NAS datasets exported over NFS/SMB).

Status

  • Repo: private — declarative config lives in the VPS_PROD, NAS_PROD, LXC_tailscale, and personal profiles of my-nixos-infrastructure.
  • Running: daily since the flake-ification of the VPS; replaced an Ubuntu LTS + UFW + Cloudflare Tunnel setup during 2024.
  • Scope note: this project originally covered only the VPS WireGuard server; it now represents the full homelab network fabric (both VPN layers, pfSense VLANs, subnet routers, split DNS).
  • Related: my-homelab (what the network connects), my-nixos-infrastructure (how the profiles compose).