Build log · MikroTik · VPS-routed /48 · CHR relay

MikroTik CHR relay for routed IPv6 over CGNAT

Same VPS-routed /48 design as the Ubuntu/BIRD path, implemented with RouterOS CHR, WireGuard, BGP filters, and relay firewall rules.

Overview

This is the MikroTik CHR variant of the VPS path in the MikroTik CGNAT series. The relay VPS is RouterOS CHR, but the routing design stays the same: WireGuard carries transit, eBGP exchanges intent, and the home router advertises the routed /48 while learning only a default route from the VPS.

Use this when you want both ends of the tunnel to be RouterOS. The tradeoff is that provider bootstrap is more manual than a Linux template: CHR is usually written to the VPS disk from rescue, then configured over VNC or SSH.

Design decisions

Keep the routed /48 off the WAN as a connected prefix. If the provider assigns an address such as <LAN_PREFIX>::a/48 to the VPS NIC, configure that specific address as /128 on CHR. The aggregate route for <LAN_PREFIX>::/48 should be learned from the home router over BGP, not treated as directly connected on ether1.

Use RouterOS routing filters on both routers. The CHR imports only <LAN_PREFIX>::/48 from the home router and advertises only ::/0 back. The home router imports only ::/0 and advertises only the home aggregate.

Originate the default route from the CHR BGP template. RouterOS can send ::/0 with output.default-originate=always; it does not need a synthetic blackhole default route for this design. The real CHR internet default remains the provider route on ether1.

Pin the WireGuard endpoint to a real underlay. If the VPS endpoint is IPv6 only, the home router needs an independent route to that endpoint outside the CHR tunnel. Otherwise the endpoint may become reachable only through the default route learned from the tunnel it is trying to establish.

1. Topology recap

                       Internet (IPv4 + IPv6)
                              │
                ┌─────────────┴─────────────┐
                │  VPS — MikroTik CHR       │
                │  ether1: provider WAN     │
                │  wg-vps: <LAN_PREFIX>:0::1│
                │  RouterOS BGP to home     │
                └─────────────┬─────────────┘
                              │ WireGuard / UDP 51820
                              │ (IPv6 transit + eBGP)
                              │
                          home     wg-vps
                          <LAN_PREFIX>:0::2

2. Conventions and placeholders

Series-wide placeholders (<ULA_PREFIX>) live in the index §2. This variant adds:

PlaceholderMeaning
<WAN_IF>CHR public interface. On KVM CHR this is usually ether1.
<VPS_IP> / <VPS_PREFIXLEN>Provider IPv4 address and prefix length, e.g. 194.127.178.92/24.
<VPS_GW4>Provider IPv4 gateway.
<VPS_LINK_GUA>Provider IPv6 link address, e.g. 2a0a:8dc0:1000:1e6::2/126.
<VPS_GW6>Provider IPv6 gateway, e.g. 2a0a:8dc0:1000:1e6::1.
<VPS_WG_ENDPOINT>Address the home router dials for WireGuard. Prefer IPv4 when available.
<LAN_PREFIX>Routed IPv6 /48, written without trailing zeros, e.g. 2001:db8.
<VPS_PUBKEY> / <HOME_PUBKEY>WireGuard public keys, one per side.
<VPS_AS> / <HOME_AS>Private ASNs (RFC 6996, 64512–65534) for the eBGP session.
<VPS_ROUTER_ID>CHR BGP router ID written like an IPv4 address.

If your provider metadata exposes <LAN_PREFIX>::a/48 as the routed-prefix address on the VPS, add it to CHR as <LAN_PREFIX>::a/128.

3. Bootstrap CHR from rescue

From the provider rescue environment, confirm the target disk before writing:

Rescue shell — identify the target disk

bash

1lsblk 2uname -m

For an x86_64 KVM VPS, download the CHR raw image and write it to the main virtual disk. This destroys the existing disk contents.

Rescue shell — write CHR raw image

bash

1cd /tmp 2wget -O chr.img.zip https://download.mikrotik.com/routeros/7.22.3/chr-7.22.3.img.zip 3busybox unzip -p chr.img.zip chr-7.22.3.img | dd of=/dev/vda bs=4M conv=fsync status=progress 4sync 5reboot

After CHR boots, log in on the provider console as admin with no password and set a temporary password. Configure enough IPv4 WAN access for SSH before uploading your key:

CHR console — temporary WAN and SSH access

bash

1/interface/ethernet/set [find default-name=ether1] disable-running-check=no 2 3/ip/dhcp-client/remove [find interface=<WAN_IF>] 4/ip/address/add address=<VPS_IP>/<VPS_PREFIXLEN> interface=<WAN_IF> comment=provider-wan 5/ip/route/add dst-address=0.0.0.0/0 gateway=<VPS_GW4> comment=provider-ipv4-default 6 7/ip/dns/set servers=1.1.1.1,8.8.8.8 allow-remote-requests=no 8/ip/service/enable ssh 9/ip/service/set ssh port=22 address=0.0.0.0/0

Then upload your SSH public key and disable SSH password authentication:

macOS — upload SSH key after temporary SSH works

bash

1scp ~/.ssh/id_rsa.pub admin@<VPS_IP>:id_rsa.pub

CHR — import key and disable SSH passwords

bash

1/user/ssh-keys/import user=admin public-key-file=id_rsa.pub 2/ip/ssh/set password-authentication=no

4. CHR — provider addressing

These commands add the provider IPv6 link address, the routed-prefix anchor, and the IPv6 default route. Keep the provider console open while applying them.

CHR — IPv6 WAN

bash

1/ipv6/address/add address=<VPS_LINK_GUA> interface=<WAN_IF> advertise=no comment=provider-wan-v6 2/ipv6/address/add address=<LAN_PREFIX>::a/128 interface=<WAN_IF> advertise=no comment=routed-prefix-anchor 3/ipv6/route/add dst-address=<VPS_GW6>/128 gateway=<WAN_IF> comment=provider-gw6-onlink 4/ipv6/route/add dst-address=::/0 gateway=<VPS_GW6> comment=provider-ipv6-default

Verify before touching WireGuard:

CHR — WAN checks

bash

1/ping <VPS_GW4> count=3 2/ping 1.1.1.1 count=3 3/ping <VPS_GW6> count=3 4/ping 2606:4700:4700::1111 count=3 5/ipv6/route/print detail where dst-address=::/0

5. CHR — WireGuard relay

Create the CHR WireGuard interface first, then copy its public key into the home router's peer config as <VPS_PUBKEY>.

CHR — WireGuard relay

bash

1/interface/wireguard/add name=wg-vps listen-port=51820 mtu=1420 2/ipv6/address/add address=<LAN_PREFIX>:0::1/64 interface=wg-vps advertise=no 3 4/interface/wireguard/print detail where name=wg-vps 5 6/interface/wireguard/peers/add interface=wg-vps name=home \ 7 public-key="<HOME_PUBKEY>" \ 8 allowed-address=<LAN_PREFIX>:0::2/128,<LAN_PREFIX>::/48 \ 9 persistent-keepalive=25s

If the home router initiates to the VPS over IPv4, no IPv6 input rule is required for the WireGuard listener. If it initiates over IPv6, allow UDP 51820 to the CHR WAN address as well.

6. CHR — BGP route exchange

The CHR side imports only the home aggregate from the home router and originates a default route back to it.

CHR — BGP policy and session

bash

1/routing/filter/rule/add chain=bgp-in-home \ 2 rule="if (dst == <LAN_PREFIX>::/48) { accept } reject" 3/routing/filter/rule/add chain=bgp-out-home \ 4 rule="if (dst == ::/0) { accept } reject" 5 6/routing/bgp/instance/add name=chr-vps as=<VPS_AS> router-id=<VPS_ROUTER_ID> 7/routing/bgp/template/add name=tpl-home as=<VPS_AS> output.default-originate=always 8/routing/bgp/connection/add name=home instance=chr-vps \ 9 remote.address=<LAN_PREFIX>:0::2 remote.as=<HOME_AS> \ 10 local.address=<LAN_PREFIX>:0::1 local.role=ebgp \ 11 templates=tpl-home afi=ipv6 \ 12 input.filter=bgp-in-home output.filter-chain=bgp-out-home

output.default-originate=always is what makes the home router learn a default route from CHR. The CHR's own upstream default route should still be the provider route on ether1.

7. CHR — relay firewall

Start with a small input surface. Forwarding is allowed only between the WAN and wg-vps; application policy stays on the home router.

CHR — minimal relay firewall

bash

1/ipv6/firewall/filter/add chain=input action=accept connection-state=established,related comment=established 2/ipv6/firewall/filter/add chain=input action=drop connection-state=invalid comment=invalid 3/ipv6/firewall/filter/add chain=input action=accept protocol=icmpv6 comment=icmpv6 4/ipv6/firewall/filter/add chain=input action=accept in-interface=wg-vps protocol=tcp dst-port=179 comment="BGP from home" 5/ipv6/firewall/filter/add chain=input action=drop in-interface=<WAN_IF> comment="drop WAN input" 6 7/ipv6/firewall/filter/add chain=forward action=accept connection-state=established,related comment=forward-established 8/ipv6/firewall/filter/add chain=forward action=drop connection-state=invalid comment=forward-invalid 9/ipv6/firewall/filter/add chain=forward action=accept in-interface=wg-vps out-interface=<WAN_IF> comment="LAN to internet" 10/ipv6/firewall/filter/add chain=forward action=accept in-interface=<WAN_IF> out-interface=wg-vps comment="internet return to LAN" 11/ipv6/firewall/filter/add chain=forward action=drop comment="drop other forwarded IPv6"

Add a separate input accept rule for UDP/51820 if the WireGuard endpoint is reachable over IPv6.

8. Home router — WireGuard client and BGP

Start from RouterOS defconf on the home router and add a dedicated WireGuard interface, CHR peer, aggregate route, filters, and BGP session. Use CHR-specific names so exports stay readable.

Home router — WireGuard + BGP to CHR VPS

bash

1/interface/wireguard add name=wg-vps listen-port=51820 mtu=1420 2/interface/wireguard/peers add interface=wg-vps name=chr-vps \ 3 public-key="<VPS_PUBKEY>" \ 4 endpoint-address=<VPS_WG_ENDPOINT> endpoint-port=51820 \ 5 allowed-address=::/0 \ 6 persistent-keepalive=25s 7 8/ipv6/address add address=<LAN_PREFIX>:0::2/64 interface=wg-vps advertise=no 9 10/ipv6/route add dst-address=<LAN_PREFIX>::/48 blackhole distance=254 \ 11 comment="aggregate-for-bgp" 12 13/ipv6/firewall/address-list add list=bgp-networks-chr-vps \ 14 address=<LAN_PREFIX>::/48 comment="aggregate to CHR VPS" 15 16/routing/filter/rule add chain=bgp-in-chr-vps \ 17 rule="if (dst == ::/0) { accept } reject" 18/routing/filter/rule add chain=bgp-out-chr-vps \ 19 rule="if (dst == <LAN_PREFIX>::/48) { accept } reject" 20 21/routing/bgp/instance add name=default-bgp as=<HOME_AS> router-id=<HOME_ROUTER_ID> 22/routing/bgp/template add name=tpl-vps as=<HOME_AS> 23/routing/bgp/connection add name=chr-vps instance=default-bgp \ 24 remote.address=<LAN_PREFIX>:0::1 remote.as=<VPS_AS> \ 25 local.address=<LAN_PREFIX>:0::2 local.role=ebgp \ 26 templates=tpl-vps afi=ipv6 \ 27 input.filter=bgp-in-chr-vps \ 28 output.network=bgp-networks-chr-vps output.filter-chain=bgp-out-chr-vps

9. Verification

CHR and home router smoke tests

bash

1# CHR: 2/interface/wireguard/peers/print detail where name=home 3/routing/bgp/session/print 4/ipv6/route/print detail where dst-address=<LAN_PREFIX>::/48 5/ping <LAN_PREFIX>:0::2 count=3 6 7# Home router: 8/interface/wireguard/peers/print detail where name=chr-vps 9/routing/bgp/session/print 10/ipv6/route/print where dst-address=::/0 11/ping 2606:4700:4700::1111 count=3

The important return-routing check is on CHR:

CHR — return route should use wg-vps

bash

1/ipv6/route/print detail where dst-address=<LAN_PREFIX>::/48

Expect the BGP route to point to the home router over wg-vps. If CHR treats the whole /48 as connected on ether1, remove the WAN /48 address and keep only the routed-prefix anchor as /128.

References

Share

Comments

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