CenturyLink Gigabit service on Mikrotik RouterOS with PPPoE and IPv6

I recently helped my friends configure their CenturyLink Gigabit fiber service so they can use their own hardware instead of the provided hardware. This gave them a lot of flexibility in how the network is configured, however CenturyLink requires you to enable PPPoE and use 6RD to use IPv6 instead of natively supporting IP packets, you have to jump through hoops. I’m sure there’s some reason why their network works like that, but I figured I’d document what needs to be done and explain how it works.

Basic Internet Access (PPPoE)

First thing is to get basic IP access working. This will require PPPoE and VLAN configuration. In this example, the cable from the CPE (Customer Premise Equipment, may be called the ONT or network termination unit) will be plugged into the Mikrotik router using the SFP+ port. Don’t connect the internet Ethernet cable yet until you have NAT and DHCP configured on the other ports.

The diagram below explains the three separate layers of interfaces involved. The lowest level is the SFP+ port which is the physical connection to the CPE. The next level interface tags all packets with the VLAN id 201. CenturyLink uses VLAN 201 to differentiate between internet traffic and any other service like television. The inner most layer is the PPPoE interface which provides authentication and authorization of your service. All Internet bound traffic gets forwarded to the pppoe-out1 interface which then causes it to be wrapped with a VLAN tag and forwarded out on the SFP port.

Getting your PPPoE credentials

The first step is to retrieve the PPPoE credentials from the ISP provided router. This differs depending on what model you have, but a good strategy is to navigate to the WAN configuration page of that router and checking the source code or using the browser dev tools to see the password.

Configuration

Configuration is easy once you have the credentials

First, enable strict IP reverse path filtering (RFC3704). This isn’t mandatory, but it’s a good security practice to ensure that your network isn’t used in certain types of Distributed Denial of Service attacks (DDOS). All it does it check to make sure that packets are not being spoofed.

/ip settings set rp-filter=strict

Next, create the VLAN

/interface vlan add interface=sfp-sfpplus1 loop-protect=off name=“clink-vlan” vlan-id=201

Then configure PPPoE

/interface pppoe-client add add-default-route=yes disabled=no interface=“clink-vlan” name=clink-pppoe password={password} use-peer-dns=yes user={username}

/interface list member add interface=pppoe-out1 list=WAN

Plug in the Ethernet cable to the correct port and check to see if your internet works.

IPv6 using 6rd

Unfortunately CenturyLink doesn’t support native dual-stack IPv6 traffic and instead requires you to use IPv6 6rd. This isn’t as clean, but supporting IPv6 is important to enable the rest of the internet to transition to use IPv6 so I recommend enabling it. Otherwise we get into a chicken and egg problem.

6rd works by assigning an IPv6 prefix for the IPv4 address that the ISP receives, then translates into an IPv4 packet and sends to your router. To calculate your 6rd prefix, use this web tool. CenturyLink uses the prefix: 2602::/24 (doc link) then enter your public IP address.

For an example IP address:

CenturyLink exposes an IP address 205.171.2.64 as their 6rd server. This is the same regardless of your geographical location.

This will create a 6to4 interface pointing to CenturyLink’s 6rd server

/interface 6to4 add !keepalive mtu=1480 name=“clink-6rd” remote-address=205.171.2.64

Then create default route pointing through this interface. For some reason the documentation uses 2000::/3 instead ::/0. I don’t happen to know why. If you know, leave a comment.

1
2
/ipv6 route
add distance=1 dst-address=2000::/3 gateway=cl-6rd

Then give the route an IPv6 address in this prefix. Note how I added ::1 to the end. This is just a convention to separate between the prefix abcd::/64 and a host abcd::1/64 in that prefix. Additionally, advertise=yes is set on the bridge interface (the LAN side interface) to redistribute this prefix to all hosts, but not on the 6rd interface.

1
2
/ipv6 address add interface=bridge advertise=yes address=**2602:7B:2D43:5900::1/64**
/ipv6 address add interface=cl-6rd advertise=no address=**2602:7B:2D43:5900::1/64**

Automating it (WIP)

I originally set this up manually, but then I found out that CenturyLink will change your IPv4 address and it’ll break everything, so instead we need to automate this. I found a script from this forum post, this one, and this one. However, I had several issues with them. The first one of them didn’t calculate the IP address correct when the IPv4 had a 0 octet (ex. 123.45.0.5) which happened. The second one didn’t calculate the 6RD address correctly either. The last one finally did work (after some simple fixes), but it still remains to be seen if it will in all situations.

In the script below, I only had to modify the parameters to use the correct WAN and LAN interfaces. If you have multiple VLANs, make sure to add them to the ipv6interfaceLanArray variable.

Create a script with the following code, name it update-6rd-centurylink, and give it read and write permissions.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# Configuration
:local ipv4interface "pppoe-out1"
:local ipv6interfaceWan "6rd"
:local ipv6interfaceLanArray {"bridge"}
:local ipv6addrcomment "6rd"

:local ipv6gatewayDestination "2000::/3"

:local ipv6prefix "2602:"
:local ipv6prefixLen 24

:local ipv6pool "pool-6rd-centurylink"
:local ipv6suffixLanPool "00::"
:local ipv6suffixLanPoolDelta 8

:local ipv6suffixWan "00::1/64"
:local ipv6addressLan "::1/64"

:local ipv4border "205.171.2.64"
:local ipv6mtu 1472

# Set up
:local ipv4address [/ip address get [/ip address find interface=$ipv4interface] address]
:set ipv4address [:pick $ipv4address 0 [:find $ipv4address "/"]]

if ($ipv4address="") do={
  :error "Error getting IPv4 address"
}

# IPv6 6to4 Tunnel
:local ipv6tunnel [/interface 6to4 find where name=$ipv6interfaceWan]

:if ($ipv6tunnel="") do={
  :log info "[6rd] Creating tunnel name=$ipv6interfaceWan"
  :put "[6rd] Creating tunnel name=$ipv6interfaceWan"
  /interface 6to4 add name=$ipv6interfaceWan local-address=$ipv4address remote-address=$ipv4border mtu=$ipv6mtu !keepalive  
} else={
  :local oldipv4address [/interface 6to4 get $ipv6tunnel local-address]
  :if ($oldipv4address!=$ipv4address) do={
    :log info "[6rd] Changing tunnel name=$ipv6interfaceWan from local-address=$oldipv4address to local-address=$ipv4address"
    :put "[6rd] Changing tunnel name=$ipv6interfaceWan from local-address=$oldipv4address to local-address=$ipv4address"
    /interface 6to4 set $ipv6tunnel local-address=$ipv4address
  }
}

# IPv4 -> IPv6-style octet function
:local buildIPv4Octets do={
  :local ipv4addr [:toip6 ("1::" . $ipv4address)]

  :if ($ipv4addr="") do={
    :error "Error converting IPv4 to IPv6 address"
  }

  :local emptyOctet [pick "" 1]
  :local ipv4addrOctetsSetOne ""
  :local ipv4index 3
  :local ipv4Octet ""
  :for ipv4octetCountOne from=1 to=4 step=1 do={
    :set ipv4Octet [:pick $ipv4addr $ipv4index]
    :if (($ipv4Octet=$emptyOctet) or ($ipv4Octet=":")) do={
      :set ipv4addrOctetsSetOne ("0" . $ipv4addrOctetsSetOne)
    } else={
      :set ipv4addrOctetsSetOne ($ipv4addrOctetsSetOne . $ipv4Octet)
      :set ipv4index ($ipv4index + 1)
    }
  }

  :local ipv4addrOctetsSetTwo ""
  :set ipv4index ($ipv4index + 1)
  :for ipv4octetCountTwo from=1 to=4 step=1 do={
    :set ipv4Octet [:pick $ipv4addr $ipv4index]
    :if (($ipv4Octet=$emptyOctet) or ($ipv4Octet=":")) do={
      :set ipv4addrOctetsSetTwo ("0" . $ipv4addrOctetsSetTwo)
    } else={
      :set ipv4addrOctetsSetTwo ($ipv4addrOctetsSetTwo . $ipv4Octet)
      :set ipv4index ($ipv4index + 1)
    }
  }

  :return ($ipv4addrOctetsSetOne . $ipv4addrOctetsSetTwo)
}
:local ipv4addressOctets [$buildIPv4Octets ipv4address=$ipv4address]

# IPv4 -> IPv6 prefix function
:local buildIPv6PrefixFromIPv4 do={
  :local ipv6pre $ipv6prefix
  :local ipv4index 0
  :local ipv6preHadNonZero true
  :local ipv4OctetToCopy ""
  :local ipv4ShouldDoCopy false
  :for ipv6len from=$ipv6prefixLen to=($ipv6prefixLen + 28) step=4 do={
    :if (($ipv6len % 16)=0) do={
      :set ipv6pre ($ipv6pre . ":")
      :set ipv6preHadNonZero false
    }
    :set ipv4OctetToCopy [:pick $ipv4addressOctets $ipv4index]
    :if ($ipv4OctetToCopy="0") do={
      :if ($ipv6preHadNonZero) do={
        :set ipv4ShouldDoCopy true
      } else={
        :set ipv4ShouldDoCopy false
      }
    } else={
      :set ipv4ShouldDoCopy true
      :set ipv6preHadNonZero true
    }
    :if ($ipv4ShouldDoCopy) do={
      :set ipv6pre ($ipv6pre . $ipv4OctetToCopy)
    }
    :set ipv4index ($ipv4index + 1)
  }

  :return $ipv6pre
}
:local ipv6addressPrefix [$buildIPv6PrefixFromIPv4 ipv6prefix=$ipv6prefix ipv6prefixLen=$ipv6prefixLen ipv4addressOctets=$ipv4addressOctets]

# IPv6 address pool
:local ipv6poolPrefix ($ipv6addressPrefix . $ipv6suffixLanPool . "/" . ($ipv6prefixLen + 32))
:local ipv6poolPrefixLength ($ipv6prefixLen + 32 + $ipv6suffixLanPoolDelta)

:local ipv6poolNumber [/ipv6 pool find where name=$ipv6pool]

:local ipv6poolChanged false

:if ($ipv6poolNumber="") do={
  :log info "[6rd] Adding IPv6 pool name=$ipv6pool with prefix=$ipv6poolPrefix prefix-length=$ipv6poolPrefixLength"
  :put "[6rd] Adding IPv6 pool name=$ipv6pool with prefix=$ipv6poolPrefix prefix-length=$ipv6poolPrefixLength"
  /ipv6 pool add name=$ipv6pool prefix=$ipv6poolPrefix prefix-length=$ipv6poolPrefixLength  
} else={
  :local oldipv6poolPrefix [/ipv6 pool get $ipv6poolNumber prefix]
  :if ($oldipv6poolPrefix!=$ipv6poolPrefix) do={
    :set ipv6poolChanged true
    :log info "[6rd] Removing IPv6 addresses prior to pool change; pool name=$ipv6pool"
    :put "[6rd] Removing IPv6 addresses prior to pool change; pool name=$ipv6pool"
    /ipv6 address remove [/ipv6 address find where from-pool=$ipv6pool]
    :log info "[6rd] Changing IPv6 pool name=$ipv6pool from prefix=$oldipv6poolPrefix to prefix=$ipv6poolPrefix prefix-length=$ipv6poolPrefixLength"
    :put "[6rd] Changing IPv6 pool name=$ipv6pool from prefix=$oldipv6poolPrefix to prefix=$ipv6poolPrefix prefix-length=$ipv6poolPrefixLength"
    /ipv6 pool set $ipv6poolNumber prefix=$ipv6poolPrefix prefix-length=$ipv6poolPrefixLength
  }
}

# IPv6 address update function
:local changeIPv6Address do={
  :local ipv6addr [/ipv6 address find where interface=$ipv6interface and comment=$ipv6comment]
  :if ($ipv6addr="") do={
    /ipv6 address add interface=$ipv6interface comment=$ipv6comment address=$ipv6address advertise=$ipv6advertise from-pool=$ipv6pool
    :local newipv6address [/ipv6 address get [/ipv6 address find where interface=$ipv6interface and comment=$ipv6comment] address]
    :log info "[6rd] Created IPv6 address for interface=$ipv6interface with address=$newipv6address"
    :put "[6rd] Created IPv6 address for interface=$ipv6interface with address=$newipv6address"
  } else={
    :if ($ipv6poolChanged) do={
      :local oldipv6address [/ipv6 address get $ipv6addr address]
      /ipv6 address set $ipv6addr address=$ipv6address from-pool=$ipv6pool
      :local newipv6address [/ipv6 address get $ipv6addr address]
      :if ($oldipv6address!=$newipv6address) do={
        :log info "[6rd] Changed IPv6 address for interface=$ipv6interface from address=$oldipv6address to address=$newipv6address"
        :put "[6rd] Changed IPv6 address for interface=$ipv6interface from address=$oldipv6address to address=$newipv6address"
      }
    }
  }
}

# IPv6 addresses
$changeIPv6Address ipv6interface=$ipv6interfaceWan ipv6comment=$ipv6addrcomment ipv6address=($ipv6addressPrefix . $ipv6suffixWan) \
  ipv6advertise=no ipv6pool=$ipv6pool ipv6poolChanged=$ipv6poolChanged

:foreach ipv6interfaceLan in=$ipv6interfaceLanArray do={ 
  $changeIPv6Address ipv6interface=$ipv6interfaceLan ipv6comment=$ipv6addrcomment ipv6address=$ipv6addressLan \
    ipv6advertise=yes ipv6pool=$ipv6pool ipv6poolChanged=$ipv6poolChanged
}

# IPv6 gateway
:local ipv6route [/ipv6 route find where dst-address=$ipv6gatewayDestination and gateway=$ipv6interfaceWan]
:if ($ipv6route="") do={
  :log info "[6rd] Adding route through 6rd gateway to dst-address=$ipv6gatewayDestination"
  :put "[6rd] Adding route through 6rd gateway to dst-address=$ipv6gatewayDestination"
  /ipv6 route add dst-address=$ipv6gatewayDestination gateway=$ipv6interfaceWan distance=1
}

Next, this script needs to be scheduled to run after the PPPoE IP address renews. Unfortunately, Mikrotik does not currently support running a script after the PPPoE connection updates (1). Instead, I schedule this script to be run every 5 minutes to make sure the IPv6 connection stays working.

Create it using the UI:

  • System > Scheduler
  • Click New
  • Interval every 5 minutes
  • Policy: read and write
  • On Event: /system/script run script-6rd-centurylink

or the CLI:

1
2
/system/scheduler add interval=5m name=6rd-update on-event=\
    "/system/script run script-6rd-centurylink" policy=read,write

After that devices on your network should receive an IPv6 address. Test your internet connectivity at ipv6-test.com.

Copyright - All Rights Reserved

Comments

Comments are currently unavailable while I move to this new blog platform. To give feedback, send an email to adam [at] this website url.