Build log · MikroTik RB5009 · Route64, no VPS

Routed IPv6 for a segmented IPv4-only LAN behind CGNAT

Free routed /56 over Route64's WireGuard tunnel — a native global /64 per VLAN, no VPS, with a fast fail-to-IPv4 on outage. The no-VPS companion to the CGNAT build log.

Overview

This is the no-VPS companion to the CGNAT build log. That build recovers IPv6 by paying a $3/month VPS to route a /48 over WireGuard. This one assumes you already have a working, segmented IPv4 LAN behind carrier-grade NAT — VLANs, inter-VLAN firewall, DHCP — and you want real routed IPv6 without a recurring bill and without an endpoint to operate.

Route64 is a free tunnel broker that hands out a routed /56 over WireGuard. Because the transport is plain outbound UDP, it traverses residential CGNAT — verified on a live Converge line, not inferred. A /56 is 256 /64s: enough to give every VLAN its own native global prefix straight from the delegation, with no helper daemon and no second prefix for clients to choose between. The single trade-off is honest and designed-for: Route64 is the only IPv6 uplink, so when it is down the LAN fails fast and cleanly back to IPv4 rather than blackholing.

Every numbered section is paste-ready against a RouterOS v7 box that already has the VLAN segmentation from the companion post. The italic notes are the rationale — the trade-off being made and why.

1. Topology and what you need first

                     Internet (IPv4 + IPv6)
                            │
              ┌─────────────┴─────────────┐
              │   Route64 PoP (WireGuard) │
              │   routes a /56 to you     │
              └─────────────┬─────────────┘
                            │ WireGuard / outbound UDP
                            │ (only IPv6 transits the tunnel)
              ┌─────────────┴─────────────┐
              │   Residential fiber+CGNAT │
              │   (IPv4 outbound only)    │
              └─────────────┬─────────────┘
                            │
                ┌───────────┴───────────┐
                │       RB5009          │
                │  wg-route64  ::2/64   │  ← tunnel link
                │  bridge   <56>:1::1   │  ← LAN  GUA + ULA
                │  vlan-iot <56>:10::1  │  ← IoT  GUA + ULA
                │  vlan-gst <56>:20::1  │  ← Guest GUA + ULA
                └───────────────────────┘

This post starts where the segmentation post ends. You should already have: the bridge/vlan-iot/vlan-guest interfaces, the IPv4 inter-VLAN firewall, per-VLAN DHCPv4, and a working DoH resolver advertised on a ULA. If you do not, build sections 3, 4 and 6 of the CGNAT post first; only the IPv6 layer differs here.

2. Conventions and placeholders

Substitute every <PLACEHOLDER> before pasting. Comments are single tokens so the netwatch scripts need no nested quotes.

PlaceholderMeaning
<R64_PRIVKEY>Client (RB5009) WireGuard private key from the Route64 tunnel page.
<R64_SRV_PUBKEY>Route64 server public key.
<R64_POP_IP>Route64 PoP endpoint IPv4.
<R64_POP_PORT>Route64 PoP endpoint UDP port (also the local listen-port).
<R64_LINK>Tunnel-link /64 prefix. Client is <R64_LINK>::2, gateway <R64_LINK>::1.
<R64_56>Routed /56 written without its trailing subnet byte, e.g. 2001:db8:abcd:c0.
<ULA_PREFIX>Your locally-generated ULA, e.g. fd96:7d0b:7dc2. Same one the DNS section uses.
<R64_TUNNEL_ID>Numeric Route64 tunnel ID (DynDNS hostname).
<R64_USER> / <R64_APIKEY>Route64 account user and API key for the DynDNS endpoint.

<R64_56> is the one to read carefully. Route64 delegates a /56 such as 2001:db8:abcd:c000::/56; the usable LAN /64s are 2001:db8:abcd:c001::/642001:db8:abcd:c0ff::/64. Write <R64_56> as the part before the subnet byte (2001:db8:abcd:c0) so <R64_56>01::1 expands to 2001:db8:abcd:c001::1. This guide uses 01/10/20 to echo the VLAN numbering.

3. Route64 dashboard

Create a tunnel of type WireGuard and note: the PoP endpoint:port, the server public key, the client private key, the tunnel-link /64 (the point-to-point pair, client ::2, gateway ::1), the routed /56 delegated for LAN use — a different prefix from the link /64 — the numeric tunnel ID, and an API key for the DynDNS endpoint. The portal asks for a public IPv4; behind CGNAT that value is not load-bearing (see the design note above).

4. WireGuard client

The RB5009 always initiates; ::/0 in allowed-address lets the peer carry any IPv6 (the path is a routing decision, not set here). The tunnel-link address is advertise=no — it is point-to-point, no SLAAC.

MikroTik — Route64 WireGuard client

bash

1/interface/wireguard add name=wg-route64 mtu=1420 \ 2 listen-port=<R64_POP_PORT> private-key="<R64_PRIVKEY>" 3/interface/wireguard/peers add interface=wg-route64 \ 4 public-key="<R64_SRV_PUBKEY>" \ 5 endpoint-address=<R64_POP_IP> endpoint-port=<R64_POP_PORT> \ 6 allowed-address=::/0 persistent-keepalive=15s 7/ipv6/address add address=<R64_LINK>::2/64 interface=wg-route64 advertise=no

WireGuard, not 6in4: the obvious free broker is Hurricane Electric, but its 6in4 tunnel (IP protocol 41) has no UDP/TCP ports for a carrier's NAT44 to track and is dropped behind CGNAT — it transmits fine, nothing ever returns, confirmed on the live line. Route64's WireGuard is stateful outbound UDP: the RB5009 always initiates and the responder replies to whatever source sent a valid key-authenticated handshake, so the registered portal IPv4 is hygiene, not a dependency. persistent-keepalive=15s then keeps the carrier's NAT mapping fresh and Route64's learned endpoint under 15 s old, so the tunnel roams across a changing CGNAT egress without re-registration; a deliberately wrong portal pin was tested on both an established and a fully cold handshake and both worked.

5. Native per-VLAN IPv6 from the /56

Each VLAN gets a real global /64 carved straight from the routed /56, plus the stable ULA that the DNS section advertises as the resolver. Pick the subnet byte to mirror the VLAN — 01 for LAN, 10 for IoT, 20 for guest.

MikroTik — GUA + ULA + RDNSS per VLAN

bash

1/ipv6/address 2add interface=bridge address=<R64_56>01::1/64 advertise=yes comment="LAN GUA" 3add interface=bridge address=<ULA_PREFIX>:1::1/64 advertise=yes comment="LAN ULA" 4add interface=vlan-iot address=<R64_56>10::1/64 advertise=yes comment="IOT GUA" 5add interface=vlan-iot address=<ULA_PREFIX>:10::1/64 advertise=yes comment="IOT ULA" 6add interface=vlan-guest address=<R64_56>20::1/64 advertise=yes comment="GUEST GUA" 7add interface=vlan-guest address=<ULA_PREFIX>:20::1/64 advertise=yes comment="GUEST ULA" 8 9/ipv6/nd 10set [find interface=bridge] ra-interval=10s-30s advertise-dns=self 11set [find interface=vlan-iot] ra-interval=10s-30s advertise-dns=self 12set [find interface=vlan-guest] ra-interval=10s-30s advertise-dns=self

ra-interval=10s-30s is short on purpose. It is the only knob bounding how fast section 7's fail-to-IPv4 reaches clients: when Route64 drops, the Router-Lifetime-0 advertisement only takes effect on the next scheduled RA. Thirty seconds is the floor for that, three minutes of extra multicast is the cost; 10s-30s is the balance. The ULA is advertised as the DNS server because it is on-link for clients and survives a Route64 outage even when the global prefix does not.

There is no second IPv6 path, so the Route64 default is just the default. It carries a comment because section 7's netwatch enables and disables it by that token.

MikroTik — Route64 default route

bash

1/ipv6/route add dst-address=::/0 gateway=<R64_LINK>::1%wg-route64 \ 2 distance=1 comment=r64def

7. Fast fail-to-IPv4

netwatch pings the tunnel gateway — reachable only through wg-route64, so it tracks Route64's own health. On failure it disables the default route and sets ra-lifetime=0 on every GUA-bearing VLAN. A Router Lifetime of zero tells hosts "I am not a default router": they purge the IPv6 default and Happy Eyeballs to IPv4 within one RA interval. On recovery it restores the route and a normal ra-lifetime. Comments are single tokens.

MikroTik — netwatch arms/disarms the IPv6 uplink

bash

1/tool/netwatch add name=r64health type=icmp host=<R64_LINK>::1 \ 2 interval=5s timeout=2s comment=r64health \ 3 up-script="/ipv6/route enable [find comment=r64def]; /ipv6/nd set [find interface=bridge] ra-lifetime=30m; /ipv6/nd set [find interface=vlan-iot] ra-lifetime=30m; /ipv6/nd set [find interface=vlan-guest] ra-lifetime=30m; :log info route64-up" \ 4 down-script="/ipv6/route disable [find comment=r64def]; /ipv6/nd set [find interface=bridge] ra-lifetime=0s; /ipv6/nd set [find interface=vlan-iot] ra-lifetime=0s; /ipv6/nd set [find interface=vlan-guest] ra-lifetime=0s; :log info route64-down-failedtoIPv4"

Verified on a live build: both transitions fire and ra-lifetime=0s is accepted (RouterOS renders it as none). End-to-end the failover is ≈ netwatch detection (~8 s at interval=5s timeout=2s) plus one RA (≤ 30 s) ≈ ~38 s to clean IPv4, symmetric on recovery. This withdraws the router as a default router for all IPv6, so router-forwarded inter-VLAN IPv6 also pauses during the outage — by design, it falls to IPv4 with everything else. Same-VLAN IPv6 and the ULA resolver are on-link and keep working throughout.

8. IPv6 firewall and anti-spoofing

Mirror the IPv4 isolation in IPv6, then drop traffic whose source prefix does not belong on the ingress VLAN. SLAAC makes prefix forgery trivially cheap; the address-list filters make it not work. Substitute the same VLAN policy you already run on IPv4.

MikroTik — IPv6 isolation + anti-spoof

bash

1/ipv6/firewall/filter 2add chain=input action=accept in-interface=vlan-iot protocol=udp dst-port=53 comment="IOT: DNSv6" 3add chain=input action=accept in-interface=vlan-guest protocol=udp dst-port=53 comment="GUEST: DNSv6" 4 5add chain=forward action=drop in-interface=vlan-iot out-interface=bridge connection-state=new comment="IOT !-> LAN (v6)" 6add chain=forward action=drop in-interface=vlan-guest out-interface=bridge connection-state=new comment="GUEST !-> LAN (v6)" 7add chain=forward action=drop in-interface=vlan-guest out-interface=vlan-iot connection-state=new comment="GUEST !-> IOT (v6)" 8 9/ipv6/firewall/address-list 10add list=lan-legit address=<R64_56>01::/64 11add list=lan-legit address=<ULA_PREFIX>:1::/64 12add list=lan-legit address=fe80::/10 13add list=iot-legit address=<R64_56>10::/64 14add list=iot-legit address=<ULA_PREFIX>:10::/64 15add list=iot-legit address=fe80::/10 16add list=guest-legit address=<R64_56>20::/64 17add list=guest-legit address=<ULA_PREFIX>:20::/64 18add list=guest-legit address=fe80::/10 19 20/ipv6/firewall/filter 21add chain=forward action=drop in-interface=bridge src-address-list=!lan-legit comment="LAN: anti-spoof" 22add chain=forward action=drop in-interface=vlan-iot src-address-list=!iot-legit comment="IOT: anti-spoof" 23add chain=forward action=drop in-interface=vlan-guest src-address-list=!guest-legit comment="GUEST: anti-spoof"

9. DynDNS hygiene (optional)

Not required — the tunnel survives a wrong pin on both established and cold handshakes. Kept only to keep the portal accurate, on a 15-minute scheduler (Route64 rate-limits endpoint changes to one per 10 min; nochg is unmetered, so 15 min never trips it). start-time=startup corrects a stale pin after a reboot.

MikroTik — Route64 DynDNS scheduler

bash

1/system/script add name=route64-ddns source=":do { :local r [/tool fetch url=\"https://manager.route64.org/nic/update?hostname=<R64_TUNNEL_ID>\" user=\"<R64_USER>\" password=\"<R64_APIKEY>\" mode=https output=user as-value]; :log info (\"route64-ddns: \" . (\$r->\"data\")); } on-error={ :log warning \"route64-ddns: fetch error\"; }" 2/system/scheduler add name=route64-ddns start-time=startup interval=15m \ 3 on-event="/system/script/run route64-ddns"

10. Verification

End-to-end checks

bash

1# Tunnel handshake is recent and bytes are moving 2/interface/wireguard/peers print where interface=wg-route64 3 4# Egress from a real LAN GUA (NOT the tunnel link) — proves native routing 5/ping 2606:4700:4700::1111 src-address=<R64_56>01::1 count=4 6/ping 2001:4860:4860::8888 src-address=<R64_56>10::1 count=3 7 8# Single default, via Route64 9/ipv6/route print where dst-address=::/0 10 11# netwatch health + the fail/recover log trail 12/tool/netwatch print where comment=r64health 13/log/print where message~"route64-(up|down)"

A client test that survives reboots: from a LAN host, curl -6 https://ifconfig.co returns an address inside your <R64_56> /56, and pulling the WireGuard peer's endpoint (or briefly disabling wg-route64) drops the host to IPv4 within ~40 s with no manual action.

11. Caveats

  • Single uplink. A Route64 outage is a full IPv6 outage; the network fails fast to IPv4 by design.
  • No inbound services on the GUA. Reachability depends on Route64's PoP and ToS; treat the prefix as egress-grade. Anything you must reach from outside belongs behind something you operate.
  • ULA addressing survives an outage; routed IPv6 does not. Hosts keep their <ULA_PREFIX> addresses and same-VLAN/​on-link IPv6 (including the ULA DNS resolver) through a Route64 outage.
  • /56, not /48. 256 /64s — ample for a home's VLANs. A /48's 65k only matters if you sub-delegate at scale, which a home LAN does not.
  • Free broker. No SLA, no portal 2FA at time of writing, and use is governed by Route64's terms of service. Acceptable for a home LAN's egress IPv6; weigh it for anything you care about.

References

Share

Comments

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