marfillaster · notes

Build log · MikroTik RB5009 · per-VLAN IPv6

Per-VLAN IPv6 on RouterOS

GUA + ULA + RA RDNSS per VLAN, IPv6 forward-chain isolation, and SLAAC anti-spoof — the LAN-side layer after either routed-IPv6 path.

Overview

The step after either path post in the RB5009 CGNAT series: plumb the routable IPv6 you just stood up through to every VLAN. At this point the router has outbound routable IPv6 (a working default route over WireGuard, with either eBGP or a static gateway) but no LAN client has a GUA. This post fixes that: per-VLAN GUA + ULA + RA RDNSS, IPv6 forward-chain isolation, and anti-spoof enforcement against the address-list pattern this series uses.

Path-agnostic: the same snippets work whether your routed prefix is a /48 from a VPS, a /56 from Route64, or a self-originated /48 from the multi-homed build. The reader resolves the prefix into three per-VLAN /64 placeholders once at the top, and every snippet below reads cleanly.

Prerequisites:

  • VLAN companion post applied — bridge, vlan-iot, vlan-guest interfaces and the bridge VLAN table.
  • One of the three paths complete: VPS-routed /48, Route64 /56, or the multi-homed build — you have a default IPv6 route over WireGuard, and no per-VLAN GUA is plumbed on any LAN interface yet (only the tunnel link carries a GUA at this point).
  • DNS companion post applied if you want RA RDNSS to advertise the router as the resolver — the advertise-dns=self posture below assumes that piece is in place.

1. Conventions and placeholders

Three per-VLAN /64 prefixes are placeholder-driven so the snippet stays literal regardless of /48 or /56 origin. Resolve each once before pasting:

PlaceholderMeaning
<GUA_LAN>The /64 prefix for VLAN 1 (main LAN), written so <GUA_LAN>::1/64 is the router IP.
<GUA_IOT>The /64 prefix for VLAN 10 (IoT), written so <GUA_IOT>::1/64 is the router IP.
<GUA_GUEST>The /64 prefix for VLAN 20 (Guest), written so <GUA_GUEST>::1/64 is the router IP.
<ULA_PREFIX>Same ULA from the index §2.

Fill from the path post you just completed:

From this path<GUA_LAN><GUA_IOT><GUA_GUEST>
VPS /48 (<LAN_PREFIX>=2001:db8)<LAN_PREFIX>:1<LAN_PREFIX>:10<LAN_PREFIX>:20
Route64 /56 (<R64_56>=2001:db8:abcd:c0)<R64_56>01<R64_56>10<R64_56>20
Multi-homed /48 (<OWN_48>=2001:db8:eff9)<OWN_48>:1<OWN_48>:10<OWN_48>:20

The VLAN numbers reused as IPv6 slice IDs (:1, :10, :20 or 01, 10, 20) mean the mapping stays obvious in tcpdump regardless of which path delivered the prefix.

2. Per-VLAN addresses and RA RDNSS

Each VLAN gets a GUA from the routed prefix and a stable ULA from the locally-generated fd… prefix. RA RDNSS uses advertise-dns=self, so the router advertises the address it holds on the interface rather than a hardcoded one — the DNS companion post explains why that survives a renumber and how the ULA stays the memorized resolver.

bash

bash

/ipv6/address
add interface=bridge     address=<GUA_LAN>::1/64    advertise=yes comment="LAN GUA"
add interface=bridge     address=<ULA_PREFIX>:1::1/64  advertise=yes comment="LAN ULA"
add interface=vlan-iot   address=<GUA_IOT>::1/64    advertise=yes comment="IOT GUA"
add interface=vlan-iot   address=<ULA_PREFIX>:10::1/64 advertise=yes comment="IOT ULA"
add interface=vlan-guest address=<GUA_GUEST>::1/64  advertise=yes comment="GUEST GUA"
add interface=vlan-guest address=<ULA_PREFIX>:20::1/64 advertise=yes comment="GUEST ULA"

/ipv6/nd
add interface=bridge     advertise-dns=self managed-address-configuration=no other-configuration=no
add interface=vlan-iot   advertise-dns=self managed-address-configuration=no other-configuration=no
add interface=vlan-guest advertise-dns=self managed-address-configuration=no other-configuration=no

The index §5 ULA-only update adds a ULA-only VLAN 30 on top of this base — same /ipv6/nd posture, but no GUA, so its clients use native ISP IPv4 for anything internet-facing. Apply it after this post if you want the main client SSID off the metered (VPS) or broker (Route64) tunnel.

3. IPv6 forward-chain isolation

Mirror the IPv4 isolation the VLAN companion post established. IoT and Guest get on-link DHCPv6 and DNSv6 to the router but cannot reach the trusted LAN; Guest cannot reach IoT either.

bash

bash

/ipv6/firewall/filter
add chain=input action=accept in-interface=vlan-iot   protocol=udp dst-port=547 comment="IOT: DHCPv6"
add chain=input action=accept in-interface=vlan-iot   protocol=udp dst-port=53  comment="IOT: DNSv6"
add chain=input action=accept in-interface=vlan-guest protocol=udp dst-port=547 comment="GUEST: DHCPv6"
add chain=input action=accept in-interface=vlan-guest protocol=udp dst-port=53  comment="GUEST: DNSv6"

add chain=forward action=drop in-interface=vlan-iot   out-interface=bridge   connection-state=new comment="IOT !-> LAN (v6)"
add chain=forward action=drop in-interface=vlan-guest out-interface=bridge   connection-state=new comment="GUEST !-> LAN (v6)"
add chain=forward action=drop in-interface=vlan-guest out-interface=vlan-iot connection-state=new comment="GUEST !-> IOT (v6)"

4. Anti-spoof

SLAAC makes prefix forgery trivially cheap — a client on vlan-iot can configure any source address it wants. The address-list filters make the forgery not work: each interface only forwards traffic whose source falls in that VLAN's legitimate /64 (the GUA, the ULA, or link-local).

bash

bash

/ipv6/firewall/address-list
add list=lan-legit   address=<GUA_LAN>::/64
add list=lan-legit   address=<ULA_PREFIX>:1::/64
add list=lan-legit   address=fe80::/10
add list=iot-legit   address=<GUA_IOT>::/64
add list=iot-legit   address=<ULA_PREFIX>:10::/64
add list=iot-legit   address=fe80::/10
add list=guest-legit address=<GUA_GUEST>::/64
add list=guest-legit address=<ULA_PREFIX>:20::/64
add list=guest-legit address=fe80::/10

/ipv6/firewall/filter
add chain=forward action=drop in-interface=bridge     src-address-list=!lan-legit   comment="LAN: anti-spoof"
add chain=forward action=drop in-interface=vlan-iot   src-address-list=!iot-legit   comment="IOT: anti-spoof"
add chain=forward action=drop in-interface=vlan-guest src-address-list=!guest-legit comment="GUEST: anti-spoof"

5. Verification

Path-side verification (BGP session, birdc, netwatch) lives in the post you just finished. This post adds the LAN-side checks. The one-glance path-agnostic check is the screenshot in the index §7.

bash

bash

# From a client on each VLAN, after a Wi-Fi reconnect or DHCP renew:
ip -6 addr show                              # expect a SLAAC GUA + ULA + link-local
ping6 -c 2 2606:4700:4700::1111              # Cloudflare DNS — verifies egress
ping6 -c 2 <GUA_LAN>::1                      # router's GUA on this VLAN

# Inter-VLAN isolation (from IoT or Guest, MUST fail):
ping6 -c 2 <GUA_LAN>::1                      # IoT/Guest -> LAN gateway: should drop

# Anti-spoof drop counter (on the router, watch it tick when a client
# forges a source address it shouldn't have):
/ipv6/firewall/filter/print stats where comment~"anti-spoof"

# DNS via the advertised ULA (RDNSS):
dig @<ULA_PREFIX>:1::1 cloudflare.com AAAA

A client that gets a GUA, can ping a public v6 address, cannot reach the LAN gateway from IoT/Guest, and resolves via the ULA-advertised RDNSS has all four layers of this build working: tunnel up, default route learned, GUA assigned, isolation enforced.

References

Share

Comments

View or add comments