---
name: project_cascades_isolated_vlan_pattern
description: Cascades pfSense — the only isolated-VLAN template is the GUEST VLAN (VLAN50/igc1.50); VLAN20 is NOT isolated; verify with pfctl -sr not config.xml
metadata:
  type: project
---

On the Cascades pfSense (`192.168.0.1`, Plus 25.07), the **template for an isolated VLAN is the GUEST VLAN (VLAN 50 / `igc1.50`)** — four `quick`, **Protocol=Any** interface rules: block -> `192.168.0.0/22`, block -> `10.0.0.0/8`, block -> `172.16.0.0/12`, then pass -> `any`; DHCP hands out **public DNS `8.8.8.8, 1.1.1.1`** (DNS resolves over the internet egress, NOT to the firewall — the 10.0.0.0/8 block would kill firewall DNS). No `RFC1918` alias exists; isolation uses literal CIDRs.

**VLAN 20 (Internal / `igc1.20`) is NOT isolated** — its only user rule is `opt238net -> lan`; all other traffic (incl. to internal) rides a floating `pass inet all` catch-all. Do not use VLAN 20 as an isolation template.

**Two traps, both burned time 2026-06-17:**
1. **config.xml lies** — it showed RFC1918 block rules on a friendly "opt239" that are NOT in the enforced ruleset (friendly-name/macro offset + inactive rules). **Always verify against the live enforced ruleset: `pfctl -sr | grep igc1.<vlan>`**, never trust the config-file rule dump alone.
2. **Protocol=Any is mandatory** on the block rules. A GUI build that sets Protocol=TCP leaves UDP (SIP/RTP/DNS) un-blocked to internal — it leaks via the floating `pass inet all`. pf prints port 53 as `domain`, not `53`.

**VOICE VLAN 30 (`igc1.30`/opt241, `10.0.30.0/24`)** was built 2026-06-17 to this exact pattern (cloud-PBX phones + Vertical LogMeIn desktop, HIPAA isolation). Scripted changes go via the pfSense PHP config API (`require config.inc; write_config(); filter_configure(); services_dhcpd_configure()`) — supported path, not config.xml surgery. Full runbook: `clients/cascades-tucson/docs/network/voice-vlan-cutover.md`. See [[howard-home-lan-shadow]] for VPN reach.
