John Elliott

IPv6 on Mikrotik with

Hack the planet!

I recently finished a mini IPv6 project and want to tell part of the story. It is another networking adventure, this time a hack with my colleague Cody Harris that came together over a couple days.1

Cody’s idea was to use Hurricane Electric (HE)’s free service to 〰️leverage〰️ the peering relationship between HE and its peers to get something resembling free IP transit between a home router in our city and any of HE’s numerous peers.

Read Cody’s post for details explaining the network layout and packet structure in depth.

The goal of the tunnel madness was to test the throughput of the tunnels, but because many users are on residential connections a side-quest emerges: how do we maintain the tunnel across outages and reboots? This post dives in to that automation for a MikroTik home router.2

Home Router Configuration

Routing plan

Once we’re clear on the goal state by reading Cody’s post, we understand that we’re putting one tunnel inside another.

To specify this in our router we need to understand the ideas of virtual interfaces and routes.3 Virtual interfaces behave much like physical Ethernet interfaces, and for this tunnel setup we’re adding two interfaces, one for WireGuard and one for the IPv6 tunnel. To put tunnels within tunnels we add routes to the routing table that set the outer tunnel as a gateway address of the inner tunnel.

In our case we use the destination router’s native IPv6 address as the peer endpoint address of the WireGuard tunnel. It’s that setting—the endpoint address—that makes the WireGuard tunnel “run inside” the 6to4 tunnel.

OK, well what about the 6to4 tunnel then? It just shares the same IPv4 default gateway as any other IPv4 traffic, e.g. from our laptop or TV, and its job is to provide the IPv6 prefix to act as the IPv6 default gateway for the router. This gives us access to the IPv6 internet.

There’s no obvious link between the two tunnels, which is why examining the v4 and v6 routing tables and understanding routing tables is important to building this setup.

Automation plan

The automation side of this is all to just maintain the pesky local-address entry on the 6to4 interface containing the dynamic IPv4 address from our ISP. We go through considerably more work configuring this one detail than the routing stuff above.

There are 4 main steps to the automation.

  1. Detect when our 6to4 interface stops working
  2. Update our local-address of our side
  3. Check HE’s side for an outdated address of our router
  4. Update HE’s side with our new IP via API if needed

So we’ll need a script that can check APIs, interfaces, and do updates, but to keep that going we’ll need something like a cron job, and to really check that the service is down and something needs doing, we’ll need a reliable way to to know.

The best way to do this is to employ three RouterOS features: scripts, scheduler, and netwatch. Scripts are where we can hit APIs and update config, scheduler lets us re-run scripts at an interval much like a cron job, and netwatch is a specialized feature to check if our setup is healthy by hitting remote hosts and scheduling our script to run if something goes down.4


Enough talk, time to implement now that we know what we’re doing.

6to4 tunnels

Getting the HE end of the tunnel set up starts by registering and creating it on After you create a tunnel you are assigned a block of IPv6 addresses. Hurricane Electric provides example configurations for 27 platforms including MikroTik.

The 6to4 tunnels provided by are standardized and RouterOS supports them along with WireGuard tunnels. Tunnels appear as network interfaces within the router and follow similar rules to physical interfaces. HE offers simple REST APIs and we’ll use the fetch function of RouterOS to interact with them.

Creating a 6to4 tunnel

When we initially create a tunnel with HE we register our public IPv4 address that the tunnel traffic will come from. Our address is a key piece of information because it tells HE what address to accept traffic for our tunnel from and reject everything else.

We are responsible for keeping HE updated on our current IP address, and since we have a residential internet connection with a dynamic IP address we’ll need the automation to update HE as it changes to keep our tunnel running.

Adding 6to4 router config

First we log in to the router5 and add the 6to4 interface for the tunnel:

/interface 6to4 add comment="Hurricane Electric IPv6 Tunnel Broker" \
disabled=no local-address= mtu=1280 name=sit1 \

Above is what we’ll need to maintain in the near future.

Next we add a route to reach the IPv6 internet addresses via the tunnel address:

/ipv6 route add disabled=no distance=1 dst-address=2000::/3 \
gateway=2001:DB8::1 scope=30 target-scope=10

Then we add the IPv6 address for our side ending in ::2:

/ipv6 address add address=2001:DB8::2/64 advertise=no \
disabled=no eui-64=no interface=sit1

Tunnel update script

This automation revolves around two small scripts. The MikroTik scripting language feels quirky, but they are pretty readable and well-integrated with the device. The code brings together a REST API and a few configuration commands.

All of the following code for the automation is online if you’d prefer to skip there.

Here’s the script. It’s not the most elegant code as I basically stopped developing it after cleaning off some gratuitous debug logging.

# NB: `:nothing` above allows pasting this script into a CLI

# -------------------------------------------------------------------
# Update Hurricane Electric IPv6 Tunnel Client endpoints
# -------------------------------------------------------------------

:local HEtunnelinterface "tun-he"
:local HEtunnelid "tunnel-id-num"
:local HEuserid "he-user"
:local HEUpdatekey "update-key"
:local HEupdatehost ""
:local HEtunnelinfohost ""
:local HEupdatepath "/nic/update"
:local WANinterface "WAN"
:local outputfile ("HE-" . $HEtunnelid . ".txt")

# Get WAN interface IP address
:local WANipv4addr [/ip address get [/ip address find interface=$WANinterface] address];
:set WANipv4addr [:pick [:tostr $WANipv4addr] 0 [:find [:tostr $WANipv4addr] "/"]];

# Error out if we can't find WAN address
:if ([:len $WANipv4addr] = 0) do={
  :log warning ("Could not get IP for interface " . $WANinterface);
  :error ("Script error: Could not get IP for WAN interface " . $WANinterface);

# Get current tunnel interface ip address
:local tunLocalAddr [/interface 6to4 get $HEtunnelinterface local-address];

:if ([:len $tunLocalAddr] = 0) do={
   :log warning ("Could not get IP for interface " . $HEtunnelinterface);
   :error ("Script error: Could not get IP for interface " . $HEtunnelinterface);

# Update our interface local-address
/interface 6to4 {
  :if ([get ($HEtunnelinterface) local-address] != $WANipv4addr) do={
    :log debug ("Updating " . $HEtunnelinterface . " local-address with new IP " . $WANipv4addr . "...");
    set ($HEtunnelinterface) local-address=$WANipv4addr;

# Check if out interface is out of date
:set tunLocalAddr [/interface 6to4 get $HEtunnelinterface local-address];
:if ($WANipv4addr = $tunLocalAddr) do={
  # Check and update HE's side
  # Doc:
  # E.g. request URL
  :local getTunInfoURL ("https://" . $HEuserid . ":" . $HEUpdatekey . "@" . \
      $HEtunnelinfohost . "/tunnelInfo.php?tid=" . $HEtunnelid);

  # Send update to HE
  # Response should be ~850 chars including headers; under the 4kb variable limit of RouterOS
  :local getTunInfoURLResult [/tool fetch mode=https url=$getTunInfoURL as-value output=user];
  :if ($getTunInfoURLResult->"status" = "finished") do={
     :local body ($getTunInfoURLResult->"data")
     # Check result body for our ip address
     # HE address just updated above
     :local clientv4XML ("<clientv4>" . $WANipv4addr . "</clientv4>");

     :put ("XML to match against: " . $getTunInfoURLResult->"data");
     :put ("String to match: " . $clientv4XML);
     :local XMLMatch [:find $body $clientv4XML -1];

     :if ($XMLMatch) do={
       :put ("Client IPv4 Address: " . $WANipv4addr);
     } else= {
       # Use API to update HE side with our wan address
       :put ("Need to update HE about our address" . $HEtunnelinterface . ":" . $WANipv4addr);

       # Now tell HE about the update
       :local htmlcontent;
       :put ("Updating IPv6 Tunnel " . $HEtunnelid . " Client IPv4 address to new IP " . $WANipv4addr . "...");

       # Doc:
       # e.g.:
       :local fetchurl ("https://" . $HEuserid . ":" . $HEUpdatekey . "@" . \
           $HEupdatehost . $HEupdatepath . \
           "?ipv4b=" . $WANipv4addr . \
           "&hostname=" . $HEtunnelid);

       # Send update
       /tool fetch mode=https url=$fetchurl dst-path=($outputfile);

       # Handle response from HE API
       :set htmlcontent [/file get $outputfile contents];
       /file remove $outputfile;
       :log info ("Tunnelbroker update resp: " . $htmlcontent);
      # End HE update API
} else={
  :log info "WAN IP on tunnel still not up to date";

Adding Netwatch to check tunnel health

Netwatch is a RouterOS feature to run scripts when a host goes down. We’ll use it to monitor HE ipv6 side of the tunnel and running of our update script. By using ICMP ping we won’t have to constantly hit HE’s API with HTTP.

We could just implement what Netwatch does inside our script, but our use case seems perfect for Netwatch so I decided to use it. Netwatch may consolidate pings across watchers and lets you check how long things have been up.

Here’s what is looks like to check in on netwatch in the CLI:

[admin@router] /tool/netwatch> pr
;;; tunnel endpoint reachable
0 simple  2001:DB8::1  20s       up      2011-11-11 11:11:11

Here’s how we add the netwatch configuration:

/tool netwatch
add comment=" tunnel endpoint reachable" \
    disabled=no down-script="" host=2001:DB8::1 \
    interval=20s test-script="" type=simple up-script=\
    "system scheduler remove check-tunnelbroker-ip"
/tool netwatch edit [find where comment~"^Tunnel"] down-script

Now we’re popped into a nano-like editor that makes it easy to paste in a longer script. In production environments you’d likely use templating to just place the script in the empty down-script="" above.

Note the last bit—the /system scheduler part—this is what schedules our endpoint check script to run and try to reconfigure the connection until netwatch’s up-script comes along and un-schedules it.

# Check if we have the interface disabled
:local HEtunnelinterface "sit1";
if ([/interface 6to4 get $HEtunnelinterface disabled]) do={
  :log info ("Tunnel interface " . $HEtunnelinterface \
      . " disabled won't update");
/system scheduler add disabled=no interval=10s \
name=check-tunnelbroker-ip on-event=tunnelbroker-update \

So, that’s it. The easiest way to test whether it’s working without going offline is to intentionally misconfigure the 6to4 tunnel’s local-address to an invalid address like and watch the tunnel break.

Here’s how I print a summary of my tunnelbroker configs:

    /in 6to4/ex; /system/scheduler ex;
    /tool/netwatch ex;
    /system/script/pr without-paging where name=tunnelbroker-update;

Was it worth it?

Well, yeah. It was fun to use the tunnel-in-tunnel for a few speed tests. I turned off the WireGuard “transit tunneling” after the benchmarks.

I was delighted that it worked (and again, go read Cody’s post for rationale and results), but here in this post my main goal was to document how to keep the 6to4 tunnel up with our most common CPEs6, Mikrotik RouterOS devices.

The automated setup was the most valuable for me. Hurricane Electric’s service is worlds faster than the infamous tunnel service my ISP offers. For the rare times I need to connect to an IPv6 host I can use the tunnel without having to SSH jump through another host with IPv6.

I decided to share the code on GitHub:

  1. Cody mentioned the idea on a Wednesday, and on that Friday we were sitting together and decided to just try an implementation rather than musing about it further. I was on my laptop and we got about 90% of the way done before folks began to yawn and we called it a night. The next morning it was an hour or so before I was able to send the first packets over our contraption of tunnels and vpn protocols. ↩︎

  2. Hurricane Electric has extensive data on how broadly the service is used world-wide. ↩︎

  3. RouterOS has a Linux kernel and routing terms and behavior often shine through. ↩︎

  4. Initially I thought I’d just schedule a script to run indefinitely, but netwatch is purpose-built for this sort of check and adds flexibility to iterate on health checks without writing more code. ↩︎

  5. Webfig—the RouterOS web-ui—can do all of the same config if the CLI is impractical ↩︎

  6. CPE stands for Customer Premises Equipment, ISP-speak for your router or boxes needed to get internet at home ↩︎