Build log · MikroTik RB5009 · VLAN segmentation

Trusted, IoT, and Guest VLANs on a MikroTik RB5009

Split a flat LAN with two UniFi APs on hybrid trunks and a reviewable east-west firewall. Pure IPv4 plus 802.1Q — the first layer of the CGNAT build log.

Overview

This is the first-layer companion to the CGNAT build log: split a flat home LAN into a trusted main network, an IoT VLAN, and a Guest VLAN on a single MikroTik RB5009, with two UniFi 6 APs hanging off hybrid-trunk ports.

It depends on nothing further up the stack. There is no IPv6, no VPS, and no WireGuard here — this is plain IPv4 plus 802.1Q VLANs and a reviewable firewall. The CGNAT build's IPv6, DoH, and failover layers all sit on top of exactly this segmentation; none of them are needed to stand it up, and this post is the part you can apply on day one regardless of what your WAN looks like.

Every numbered section is paste-ready against a defconf RouterOS v7 box. The italic notes are the rationale — the trade-off being made and why.

1. Topology and address plan

                       Internet / WAN
                              │
                ┌─────────────┴─────────────┐
                │  MikroTik RB5009 — edge   │
                │  • per-VLAN L3 gateway    │
                │  • DHCPv4 per VLAN        │
                │  • east-west firewall     │
                └──┬──────────┬──────────┬──┘
                   │          │          │
        ether2/3 (hybrid trunks)  ether4/5 (LAN-only access)
                   │
        ┌──────────┴──────────┐
        │  UniFi 6 APs ×2     │
        │  untagged = mgmt    │
        │  tag 10 = IoT SSID  │
        │  tag 20 = Guest SSID│
        └──────────┬──────────┘
                   │
       LAN 1   IoT 10   Guest 20
   192.168.88/24  .89/24  .90/24

One box does the LAN-side work: it terminates the WAN (whatever it is), gives each VLAN an L3 gateway, runs DHCPv4 per VLAN, and enforces isolation. Two UniFi 6 APs hang off bridge ports configured as hybrid trunks — untagged frames carry AP management on the main LAN, tagged frames carry IoT and Guest SSID traffic.

One always-on device is enough. The UniFi controller can also run on this same RB5009 as RouterOS containers — no second box — which is its own companion post: Running the UniFi controller on the router itself.

Address plan

VLANTaggingRoleIPv4
VLAN 1untaggedmain LAN, AP mgmt192.168.88.0/24
VLAN 10taggedIoT SSID192.168.89.0/24
VLAN 20taggedGuest SSID192.168.90.0/24

Each VLAN is a separate L3 boundary at the router. When the IPv6 layer of the CGNAT build log is added later, each VLAN simply gains a matching :1::/64 / :10::/64 / :20::/64 prefix on the same interfaces created here — the IoT and Guest VLAN IDs deliberately reuse their numbers as the IPv6 slice IDs so the mapping stays obvious in tcpdump.

2. Conventions and placeholders

The snippets assume the defconf bridge name bridge and ports ether2ether5; rename the interfaces where they appear to match your box. The rest of the text is literal RouterOS — there are no placeholders to substitute in this layer.

3. Bridge VLAN table and L3 gateways

Turn on bridge VLAN filtering, declare the two VLAN interfaces, mark the AP uplinks as trunks, and give each VLAN a gateway address.

VLAN bridge + gateways

bash

1/interface/bridge set [find name=bridge] vlan-filtering=yes 2 3/interface/vlan add interface=bridge name=vlan-iot vlan-id=10 4/interface/vlan add interface=bridge name=vlan-guest vlan-id=20 5 6# Bridge VLAN table — hybrid trunks on ether2/ether3 (to APs), 7# access-only LAN on ether4/ether5. 8/interface/bridge/vlan 9add bridge=bridge vlan-ids=1 untagged=bridge,ether2,ether3,ether4,ether5 comment="main LAN untagged" 10add bridge=bridge vlan-ids=10 tagged=bridge,ether2,ether3 comment="IoT to UniFi APs" 11add bridge=bridge vlan-ids=20 tagged=bridge,ether2,ether3 comment="Guest to UniFi APs" 12 13/ip/address add address=192.168.89.1/24 interface=vlan-iot 14/ip/address add address=192.168.90.1/24 interface=vlan-guest

Main LAN stays VLAN 1 untagged so AP adoption and existing wired devices stay boring; only IoT and Guest are tagged, sharing the same uplinks to the APs. That makes the trunk ports genuinely hybrid (untagged + tagged on one wire) — the cost being that they must be documented as such, which the bridge VLAN table above does. APs boot and adopt on the main LAN with zero AP-side VLAN config. Each VLAN also terminates on its own gateway, so inter-VLAN traffic is routed and filterable rather than bridged — that is what makes the Guest-can't-reach-IoT rule a one-liner.

4. DHCP scopes

Each VLAN gets its own pool, server, and network record, with the router itself acting as DNS.

IoT + Guest DHCPv4

bash

1/ip/pool add name=iot-pool ranges=192.168.89.100-192.168.89.200 2/ip/pool add name=guest-pool ranges=192.168.90.100-192.168.90.200 3 4/ip/dhcp-server add name=iot-dhcp interface=vlan-iot address-pool=iot-pool lease-time=1d 5/ip/dhcp-server add name=guest-dhcp interface=vlan-guest address-pool=guest-pool lease-time=1d 6 7/ip/dhcp-server/network add address=192.168.89.0/24 gateway=192.168.89.1 dns-server=192.168.89.1 8/ip/dhcp-server/network add address=192.168.90.0/24 gateway=192.168.90.1 dns-server=192.168.90.1

The router is the resolver for now. Replacing the upstream side of that with encrypted DoH — and moving clients onto an RDNSS resolver address that survives a renumber — is its own companion post: Encrypted DNS with a stable resolver address on RouterOS. That layer is optional and sits on top of this one.

5. Firewall — input services and east-west isolation

The input chain accepts only the router services the VLANs actually need; the forward chain drops new flows back into trusted networks. Established replies are not affected, which is what makes narrow per-host exceptions practical later.

Input + forward firewall

bash

1# Input — place BEFORE defconf's "drop all not coming from LAN". 2/ip/firewall/filter 3add chain=input action=accept in-interface=vlan-iot protocol=udp dst-port=67-68 comment="IOT: DHCPv4" 4add chain=input action=accept in-interface=vlan-iot protocol=udp dst-port=53 comment="IOT: DNS UDP" 5add chain=input action=accept in-interface=vlan-iot protocol=tcp dst-port=53 comment="IOT: DNS TCP" 6add chain=input action=accept in-interface=vlan-guest protocol=udp dst-port=67-68 comment="GUEST: DHCPv4" 7add chain=input action=accept in-interface=vlan-guest protocol=udp dst-port=53 comment="GUEST: DNS UDP" 8add chain=input action=accept in-interface=vlan-guest protocol=tcp dst-port=53 comment="GUEST: DNS TCP" 9 10# Forward — place BEFORE fasttrack / established accepts. 11/ip/firewall/filter 12add chain=forward action=drop in-interface=vlan-iot out-interface=bridge connection-state=new comment="IOT !-> LAN" 13add chain=forward action=drop in-interface=vlan-guest out-interface=bridge connection-state=new comment="GUEST !-> LAN" 14add chain=forward action=drop in-interface=vlan-guest out-interface=vlan-iot connection-state=new comment="GUEST !-> IOT"

Isolation is enforced in forward, not by starving clients of DHCP/DNS in input. Mixing the two layers makes the rules unreviewable later.

6. Verification

Associate a client to each SSID (and a wired client on the main LAN) and confirm the boundary actually holds.

Segmentation smoke tests

bash

1# On a client in each VLAN: 2ip -4 addr show # expect the right /24 per SSID 3ping 192.168.89.1 # own gateway: must succeed 4ping 192.168.88.1 # MUST fail from IoT and Guest 5nslookup cloudflare.com 192.168.89.1 # router resolves for IoT

A client on the IoT or Guest SSID that gets a lease in its own subnet, reaches its own gateway and the internet, but cannot ping the main-LAN gateway, is the whole proof: the VLANs are isolated boundaries, not just separate address ranges.

7. The IoT printer exception

The default policy from §5 is isolation; this is the canonical narrow exception, kept deliberately small. Pin the printer's IP in the IoT scope, allow trusted LAN clients to initiate to that one address, and reflect mDNS between LAN and IoT so AirPrint discovery works. Guest stays excluded on purpose.

LAN → IoT printer + mDNS reflector

bash

1/ip/dhcp-server/lease add server=iot-dhcp mac-address=AA:BB:CC:DD:EE:FF \ 2 address=192.168.89.200 comment="Brother printer" 3 4/ip/firewall/filter add chain=forward action=accept connection-state=new \ 5 in-interface=bridge out-interface=vlan-iot dst-address=192.168.89.200 \ 6 place-before=[find where comment="IOT !-> LAN"] \ 7 comment="LAN -> printer" 8 9/ip/dns set mdns-repeat-ifaces=bridge,vlan-iot 10/ip/firewall/filter add chain=input action=accept in-interface=vlan-iot \ 11 protocol=udp dst-address=224.0.0.251 dst-port=5353 \ 12 comment="IOT: mDNS to router"

The exception is one host, one direction (LAN initiates; the established reply is what carries the print job back). It is placed before the IOT !-> LAN drop from §5 so only this destination punches through — everything else on the IoT VLAN stays isolated.

References

Share

Comments

Comments are powered by GitHub Discussions and require a free GitHub account to post.