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.
Build log · MikroTik · VPS-routed /48 · CHR relay
Same VPS-routed /48 design as the Ubuntu/BIRD path, implemented with RouterOS CHR, WireGuard, BGP filters, and relay firewall rules.
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.
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.
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
Series-wide placeholders (<ULA_PREFIX>) live in the
index §2. This variant adds:
| Placeholder | Meaning |
|---|---|
<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.
From the provider rescue environment, confirm the target disk before writing:
Rescue shell — identify the target disk
bash
1lsblk
2uname -mFor 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
5rebootAfter 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/0Then 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.pubCHR — 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=noThese 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-defaultVerify 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=::/0Create 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=25sIf 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.
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-homeoutput.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.
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.
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-vpsCHR 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=3The 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>::/48Expect 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.
Comments
Comments are powered by GitHub Discussions and require a free GitHub account to post.