Get rid of NAT64 setup
ci/woodpecker/push/flux-reconcile-source Pipeline was successful
ci/woodpecker/push/coredns-build Pipeline was successful

This commit is contained in:
2026-06-16 00:29:18 +02:00
parent b993115b41
commit 679ebb3465
13 changed files with 316 additions and 419 deletions
+26 -130
View File
@@ -1,147 +1,43 @@
# Mikrotik containers
RouterOS containers running on the CRS418 providing network services that RouterOS cannot handle natively.
RouterOS containers running on the CRS418 providing network services that
RouterOS cannot handle natively.
## CoreDNS (DNS64)
## CoreDNS
Replaces the built-in RouterOS DNS forwarder. Implements DNS64 (RFC 6147): synthesizes AAAA records from A records for IPv4-only destinations, enabling IPv6-only clients to reach them via NAT64. Native dual-stack sites keep their real AAAA records.
Replaces the built-in RouterOS DNS forwarder. Plain forwarding resolver with
selective AAAA suppression: AAAA is suppressed by default so clients prefer IPv4
(avoiding the HE tunnel's datacenter-flagged egress), while our own zone and any
whitelisted domains keep AAAA for native IPv6.
Source: [`coredns/`](coredns/)
Image built by Woodpecker CI, pushed to `gitea.lumpiasty.xyz/<owner>/coredns-mikrotik`.
Source: [`coredns/`](coredns/). Image built by Woodpecker CI
([`.woodpecker/coredns-build.yaml`](../.woodpecker/coredns-build.yaml)), pushed to
`gitea.lumpiasty.xyz/lumpiasty/coredns-mikrotik`.
The Corefile is baked into the image — edit [`coredns/Corefile`](coredns/Corefile)
and push; the pipeline rebuilds and pushes a new image. Custom-built with a
minimal plugin set (~6-8 MB vs the official ~20 MB image) to fit the CRS flash.
See [docs/coredns.md](../docs/coredns.md) for design rationale, including why
the earlier NAT64/DNS64 approach was removed.
### Why not the official coredns/coredns image?
The official image ships ~40 plugins and weighs ~20 MB compressed. A custom build with the 7 plugins we actually need fits in ~6-8 MB — important for the CRS internal flash.
### Corefile
## Deployment
The Corefile is baked into the image. To change DNS behaviour, edit [`coredns/Corefile`](coredns/Corefile) and push — the Woodpecker pipeline rebuilds and pushes a new image automatically.
The router configuration (container definitions, veth interfaces, bridge ports,
DNS settings, firewall) is managed declaratively via Ansible, not by manual CLI
commands. See [`ansible/roles/routeros/`](../ansible/roles/routeros/) and run:
## Tayga (NAT64)
Stateless NAT64 translator (RFC 7915). Receives IPv6 packets destined for `64:ff9b::/96`, rewrites them to IPv4, and returns translated responses. RouterOS does **not** implement NAT64 natively — the official docs state this explicitly.
Official image: `ghcr.io/apalrd/tayga` — no custom build needed.
---
## RouterOS setup
The commands below wire both containers into the network. Adapt interface names and IPv6 prefix to your actual allocation. The HE tunnel broker prefix in use is `2001:470:61a3::/48`; the examples below use a dedicated /64 from the management range for container interfaces.
### 1. Enable container mode (one-time, requires physical access)
```
/system/device-mode/update container=yes
```sh
cd ansible && ansible-playbook playbooks/routeros.yml
```
### 2. Network interfaces
Containers do not auto-start on first image pull; after the initial deploy,
start manually once (subsequent boots are handled by `start-on-boot=yes`):
```
# CoreDNS — dedicated veth, no IPv6 needed (DNS listens on IPv4 of the veth)
/interface/veth/add name=veth-dns address=172.31.0.2/30 gateway=172.31.0.1
/interface/bridge/add name=br-dns
/interface/bridge/port/add bridge=br-dns interface=veth-dns
/ip/address/add address=172.31.0.1/30 interface=br-dns
# Tayga — needs both IPv4 (for its own address) and IPv6 (for the NAT64 traffic path)
/interface/veth/add name=veth-nat64 address=172.31.1.2/30 gateway=172.31.1.1
/interface/bridge/add name=br-nat64
/interface/bridge/port/add bridge=br-nat64 interface=veth-nat64
/ip/address/add address=172.31.1.1/30 interface=br-nat64
/ipv6/address/add address=2001:470:61a3:500::1/64 advertise=no interface=br-nat64
```
### 3. NAT for container internet access
```
/ip/firewall/nat/add chain=srcnat src-address=172.31.0.0/29 action=masquerade comment="container egress"
```
### 4. Tayga container
```
/container/config/set registry-url=https://ghcr.io tmpdir=flash/tmp
/container/envs/add list=ENV_TAYGA key=TAYGA_CONF_IPV4_ADDR value=172.31.1.2
/container/envs/add list=ENV_TAYGA key=TAYGA_CONF_DYNAMIC_POOL value=192.0.0.0/24
/container/envs/add list=ENV_TAYGA key=TAYGA_CONF_PREFIX value=64:ff9b::/96
/container/envs/add list=ENV_TAYGA key=TAYGA_IPV6_ADDR value=2001:470:61a3:500::2
/container/add \
remote-image=ghcr.io/apalrd/tayga:latest \
interface=veth-nat64 \
envlist=ENV_TAYGA \
root-dir=flash/tayga \
start-on-boot=yes \
logging=yes \
name=tayga
```
### 5. CoreDNS container
```
/container/config/set registry-url=https://gitea.lumpiasty.xyz
/container/add \
remote-image=gitea.lumpiasty.xyz/<owner>/coredns-mikrotik:latest \
interface=veth-dns \
root-dir=flash/coredns \
start-on-boot=yes \
logging=yes \
name=coredns
```
### 6. Routes
```
# IPv6 traffic for the NAT64 prefix goes to Tayga
/ipv6/route/add dst-address=64:ff9b::/96 gateway=2001:470:61a3:500::2 comment="NAT64 via Tayga"
# IPv4 return traffic from Tayga's dynamic pool back to LAN clients
/ip/route/add dst-address=192.0.0.0/24 gateway=172.31.1.2 comment="Tayga dynamic pool"
# Masquerade Tayga's IPv4 pool to WAN
/ip/firewall/nat/add chain=srcnat src-address=192.0.0.0/24 action=masquerade comment="Tayga pool egress"
```
### 7. Point the router's DNS resolver at CoreDNS
```
/ip/dns/set servers=172.31.0.2 allow-remote-requests=yes
```
### 8. PREF64 in Router Advertisements
Tells CLAT-capable clients (iOS, Android, macOS) the NAT64 prefix without requiring DNS64 prefix discovery.
```
/ipv6/nd/set [find] pref64=64:ff9b::/96
```
### 9. DHCP option 108 — IPv6-only preferred (RFC 8925)
Signals to capable clients that they may disable IPv4 and rely on CLAT/NAT64. Clients that don't understand option 108 ignore it and continue with dual-stack.
```
# 0x0000001c = 28 seconds — short for testing; use 0x00015180 (86400) in production
/ip/dhcp-server/option/add name=v6only-preferred code=108 value=0x0000001c
/ip/dhcp-server/option/sets/add name=v6only-set options=v6only-preferred
# Attach the option set to your DHCP server:
/ip/dhcp-server/set [find] dhcp-option-set=v6only-set
```
### Verification
From a client on the LAN (IPv6-only or dual-stack):
```bash
# Should return a synthesized 64:ff9b::/96 AAAA for an IPv4-only host
dig AAAA ipv4.google.com @172.31.0.2
# Should succeed — goes via NAT64
ping 64:ff9b::1.1.1.1
# Full path test from an IPv6-only client
curl -6 https://ipv4only.arpa/
/container/start [find name=coredns]
```
+30 -21
View File
@@ -1,37 +1,46 @@
# Our own zone bypasses DNS64: internal services have native IPv6 (LB pool
# routed via HE prefix), so clients should get real AAAA records and connect
# directly instead of hairpinning through NAT64.
# CoreDNS as a plain forwarding resolver with selective AAAA suppression.
#
# This MUST live here, not as a RouterOS static FWD entry: RouterOS FWD
# entries return NOERROR with an empty answer instead of relaying NXDOMAIN,
# which breaks getaddrinfo search-domain processing (resolver stops at the
# first NODATA search candidate and never tries the absolute name -> apps
# fail with ENOTFOUND for names that exist).
lumpiasty.xyz:53 {
# Background: the ISP provides no native IPv6, only a Hurricane Electric tunnel.
# HE addresses are flagged as datacenter ranges by some sites (endless CAPTCHAs,
# bot detection). To avoid this, IPv6 (AAAA) resolution is suppressed by default
# so clients use IPv4, while a whitelist keeps AAAA for domains where native
# IPv6 is wanted (our own services reachable over the HE prefix, and any domain
# explicitly trusted over IPv6).
#
# NAT64/DNS64 was tried and removed: it forced most traffic through a userspace
# Tayga translator, capping throughput at ~250 Mbps on the RB-class CPU (line
# rate is 1 Gbps) and adding two containers as a SPOF — all to avoid IPv6 egress
# we don't want in the first place. Plain AAAA suppression achieves the same
# IPv4-preferred outcome with zero datapath overhead.
#
# TODO: replace the global template suppression + whitelist with a plugin that
# suppresses AAAA only when the domain has no A record (so IPv6-only
# destinations still work). No such in-tree plugin exists yet.
# Whitelist: domains that keep AAAA resolution (native IPv6 via HE tunnel).
(aaaa_allowed) {
forward . 1.1.1.1 8.8.8.8 {
prefer_udp
}
cache 300
errors
log
}
.:53 {
# Synthesize AAAA from A records for all destinations.
# translate_all: override real AAAA records too, so all traffic exits
# via NAT64 (our IPv4 WAN) rather than the HE tunnel broker.
# This eliminates datacenter flagging and CAPTCHA loops from HE addresses.
dns64 {
prefix 64:ff9b::/96
translate_all
allow_ipv4
}
# Our own zone: services have native IPv6 on the HE prefix, keep AAAA.
lumpiasty.xyz:53 {
import aaaa_allowed
}
# Default: forward everything, but suppress AAAA so clients use IPv4 and
# avoid the HE tunnel's datacenter-flagged egress.
.:53 {
template IN AAAA {
rcode NOERROR
}
forward . 1.1.1.1 8.8.8.8 {
prefer_udp
}
cache 300
errors
log
+1 -1
View File
@@ -1,7 +1,7 @@
errors:errors
log:log
health:health
template:template
cache:cache
dns64:dns64
forward:forward
reload:reload