MikroTik RouterOS v7: DS-Lite AFTR auto-configuration
Automatically configure the DS-Lite (RFC 6333) IPv4-in-IPv6 tunnel endpoint on a MikroTik router by reading the AFTR name the ISP sends via DHCPv6 option 64 (OPTION_AFTR_NAME, RFC 6334), instead of hardcoding it.
Tested on RouterOS 7.23.1 (RB5009) against NetCologne / NetAachen (Germany), but it should work with any DS-Lite ISP that advertises the AFTR via DHCPv6 option 64.
Why
DS-Lite gives you IPv6 natively plus an IPv4-in-IPv6 tunnel to a carrier-grade NAT gateway called the AFTR (Address Family Transition Router). The router learns the AFTR endpoint from DHCPv6 option 64, which carries a fully qualified domain name.
RouterOS does not configure DS-Lite automatically: its DHCPv6 client neither requests option 64 by default nor wires the result into a tunnel. The usual workaround is to capture the AFTR name once and hardcode it — which breaks silently whenever the ISP changes it.
This script closes that gap. It requests option 64, decodes the FQDN from the raw DHCPv6 reply, and updates the tunnel’s remote-address whenever the advertised name changes.
How it works
- A DHCPv6 client option (code 6 = the Option Request Option) tells the server we want option 64 (AFTR name) and option 23 (DNS servers).
- The DHCPv6 client’s
scriptruns on each DHCPv6 event. When the reply contains option 64, the script decodes the DNS wire-format FQDN and, if it differs from the last applied value, sets it as the IPIPv6 tunnel’sremote-address. - The IPIPv6 interface resolves the FQDN itself, so no DNS lookup is done in the script.
Prerequisites
A working DS-Lite setup except for the AFTR address, i.e. you already have:
- WAN up (e.g. PPPoE over the ISP’s internet VLAN)
- A DHCPv6-PD client bound and delegating your IPv6 prefix
- An
ipipv6tunnel interface created, with IPv4 routing/NAT over it - IPv4 currently working via a hardcoded AFTR (this script removes the need to hardcode)
Throughout, replace these names with yours:
| Placeholder | Meaning |
|---|---|
pppoe-netcologne | your WAN / DHCPv6-client interface |
ipipv6-netcologne | your DS-Lite IPIPv6 tunnel interface |
Setup
1. Request option 64 (and DNS) via the DHCPv6 client
/ipv6 dhcp-client option
add code=6 name=oro-aftr value=0x00170040The value is the Option Request Option payload: a list of DHCPv6 option codes as 16-bit big-endian values, concatenated.
0017= 23 =OPTION_DNS_SERVERS0040= 64 =OPTION_AFTR_NAME
code=6 option replaces the client’s default ORO rather than appending to it. That’s why DNS (0017) is re-included — drop it and you may lose DNS. Prefix delegation is not affected: PD is driven by request=prefix / IA_PD, which is independent of the ORO.Attach it to the DHCPv6 client:
/ipv6 dhcp-client set [find interface=pppoe-netcologne] dhcp-options=oro-aftr2. Add the script to the DHCPv6 client
Set the following as the DHCPv6 client’s script. In WebFig/WinBox, paste only the body (no surrounding braces). From the terminal, wrap it in script={ ... }.
:if ([:typeof $options] = "array") do={
:local raw ($options->"64")
:if ([:typeof $raw] != "nothing") do={
:local ok "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-"
:local fqdn ""
:local lbl ""
:local i 0
:local n [:len $raw]
:while ($i < $n) do={
:local c [:pick $raw $i ($i + 1)]
:if ([:find $ok $c] >= 0) do={
:set lbl ($lbl . $c)
} else={
:if ([:len $lbl] > 0) do={
:if ([:len $fqdn] > 0) do={ :set fqdn ($fqdn . ".") }
:set fqdn ($fqdn . $lbl)
:set lbl ""
}
}
:set i ($i + 1)
}
:if ([:len $lbl] > 0) do={
:if ([:len $fqdn] > 0) do={ :set fqdn ($fqdn . ".") }
:set fqdn ($fqdn . $lbl)
}
:if ([:len $fqdn] > 0) do={
:global lastAftr
:if ($fqdn != $lastAftr) do={
/interface ipipv6 set [find name=ipipv6-netcologne] remote-address=$fqdn
:set lastAftr $fqdn
:log info ("DS-Lite: AFTR endpoint set to " . $fqdn)
} else={
:log info ("DS-Lite: AFTR endpoint confirmed (" . $fqdn . ")")
}
} else={
:log warning "DS-Lite: AFTR option received but could not be parsed"
}
}
}3. Trigger and verify
Force a fresh bind:
/ipv6 dhcp-client disable [find interface=pppoe-netcologne]
/ipv6 dhcp-client enable [find interface=pppoe-netcologne]Watch the log:
/log print where topics~"script"You should see DS-Lite: AFTR endpoint set to <fqdn>. Confirm the tunnel followed:
/interface ipipv6 print detail where name=ipipv6-netcologneremote-address is the FQDN; current-remote-address is the resolved IPv6 of the AFTR.
Notes & gotchas
Keepalive must be absent on the tunnel. IPIPv6 tunnel keepalive only works MikroTik-to-MikroTik. Against an ISP AFTR it silently fails; the tunnel briefly shows
running, then the IPv4 connected route goes inactive (DIc) and IPv4 dies while IPv6 keeps working. Setting keepalive to0/empty is not enough — it must be removed:/interface ipipv6 set [find name=ipipv6-netcologne] !keepaliveKeep the ORO option (don’t rely on unrequested replies). RFC 6334 §4 says a server will not send the AFTR option unless the client lists it in the ORO, and §5 says a conforming client MUST request it. Some ISPs (NetCologne included) return it even without the request, but that is non-conforming server leniency. Requesting it costs nothing and keeps the script portable.
Raw option value is a byte string. On RouterOS,
$options->"64"returns the option value where each position is one byte: length octets are non-printable, label octets are literal characters. This script avoids reading byte values numerically (which proved unreliable) and instead splits on character class — printable[A-Za-z0-9-]runs are labels, anything else is a separator. This correctly reconstructs the FQDN from the RFC 6334 / RFC 3315 §8 wire format.$optionsis RouterOS v7 only. The DHCPv6 client exposes the$optionsarray only on v7. On v6 it is empty and this approach cannot work.The client fires the script on multiple events. Some carry no option array (e.g. a prefix-only event). The outer
:if ([:typeof $options] = "array")guard makes those fall through silently — expected, not an error.Change detection uses a global.
lastAftrpersists across script runs so the tunnel is only re-pointed when the AFTR actually changes (re-settingremote-addressre-initializes the tunnel and briefly drops IPv4). The global resets on reboot, so the first DHCPv6 event after a reboot re-applies once — harmless. To survive reboots, store the last value in the interfacecommentinstead and compare against that.