Mikrotik - Dynamic IPv6 prefix delegation - Update firewall rules ------------------------------------------------------------------ ### Introduction ----------------- This text explains how to configure a Mikrotik router as IPv6 DHCP client in the WAN interface to fetch a dynamic prefix via Prefix Delegation (PD), and redistribute it on the LAN(s) interface(s) via Stateless Address Autoconfiguration (SLAAC). Because the IPv6 prefix received for the LAN network is dynamic and can change in each refresh, there are a script that reconfigure the dst-address in the IPv6 firewall filter rules every time the prefix changes. This firewall rules permit (or block) specific traffic to the servers in the LAN network. Also, the configuration includes the IPv4 section (DHCP client in the WAN, and DHCP server in the LAN) for a dual stack network and miscellaneous configuration. This configuration is used typically in a home environment with a cablemodem internet service provider wich support prefix delegation. ### Requirements ----------------- * Every LAN network will have a prefix length of 64 (/64 netmask) automatically. The script not work or have undesirable side effects with others prefix lengths. * Every LAN network must have a only one IPv6 address obtained from a delegated prefix. You can set a one or more fixed secondary IPv6 address, but no other obtained from a pool. * Every IPv6 firewall filter rule must have the out-interface name configured. The out-interface name is used to identify the rules to be changed and to identify which new IPv6 prefix is used. If you need a fixed rule with no dst-address change, leave the out-interface unconfigured. * Tested and working in Mikrotik RouterOS 6.41.3. ### Configuration ------------------ # # This configuration is an example. Feel free to make changes based on your # needs. This example uses a one WAN interface to the cablemodem ISP and # two LAN interfaces to the PCs and servers. # # # Names of the interfaces. # /interface ethernet set [ find default-name=ether1 ] name=LAN1 set [ find default-name=ether2 ] name=LAN2 set [ find default-name=ether3 ] name=WAN # # Pools for the IPv4 DHCP server. # # The IPv4 DHCP server leases the IP from this pools. # /ip pool add name=POOL1 ranges=10.10.10.64/26 add name=POOL2 ranges=10.20.20.64/26 # # IPv4 DHCP servers for LANs. # /ip dhcp-server add address-pool=POOL1 disabled=no interface=LAN1 lease-time=2h name=DHCP1 add address-pool=POOL2 disabled=no interface=LAN2 lease-time=2h name=DHCP2 # # Options for the IPv4 DHCP server. # /ip dhcp-server network add address=10.10.10.0/24 dns-server=10.10.10.1 domain=DOMAIN1.MM gateway=10.10.10.1 netmask=24 ntp-server=10.10.10.1 add address=10.20.20.0/24 dns-server=10.20.20.1 domain=DOMAIN2.MM gateway=10.20.20.1 netmask=24 ntp-server=10.20.20.1 # # Store leases for the IPv4 DHCP server. # /ip dhcp-server config set store-leases-disk=never # # IPv4 address of the LAN interfaces. # /ip address add address=10.10.10.1/24 interface=LAN1 add address=10.20.20.1/24 interface=LAN2 # # IPv4 DHCP client in the WAN. # /ip dhcp-client add dhcp-options=clientid,hostname disabled=no interface=WAN # # DNS relay. # /ip dns set allow-remote-requests=yes cache-max-ttl=10m cache-size=4096KiB query-total-timeout=4s # # IPv4 bogon (martians) prefixes. # # The bogon or martians prefix are prefix no routeables # or not used in internet. # /ip firewall address-list add address=0.0.0.0/8 list=BOGONv4 add address=10.0.0.0/8 list=BOGONv4 add address=100.64.0.0/10 list=BOGONv4 add address=127.0.0.0/8 list=BOGONv4 add address=169.254.0.0/16 list=BOGONv4 add address=172.16.0.0/12 list=BOGONv4 add address=192.0.0.0/24 list=BOGONv4 add address=192.0.2.0/24 list=BOGONv4 add address=192.168.0.0/16 list=BOGONv4 add address=198.18.0.0/15 list=BOGONv4 add address=198.51.100.0/24 list=BOGONv4 add address=203.0.113.0/24 list=BOGONv4 add address=224.0.0.0/3 list=BOGONv4 # # IPv4 firewall rules: input. # # Traffic destined to the router. # /ip firewall filter add action=accept chain=input connection-state=established,related add action=accept chain=input in-interface=LAN1 add action=accept chain=input in-interface=LAN2 add action=drop chain=input in-interface=WAN src-address-list=BOGONv4 add action=accept chain=input in-interface=WAN dst-port= protocol=tcp <...> add action=accept chain=input in-interface=WAN dst-port= protocol=udp <...> add add action=drop chain=input # # IPv4 firewall rules: forward. # # Traffic wich pass through the router. # /ip firewall filter add action=accept chain=forward connection-state=established,related add action=accept chain=forward in-interface=LAN1 add action=accept chain=forward in-interface=LAN2 add action=drop chain=forward in-interface=WAN src-address-list=BOGONv4 add action=accept chain=forward dst-address= dst-port= in-interface=WAN protocol=tcp <...> add action=accept chain=forward dst-address= dst-port= in-interface=WAN protocol=udp <...> add add action=drop chain=forward # # IPv4 port forward. # # Maps the external ports with the internal IPv4 address # and ports. # /ip firewall nat add action=dst-nat chain=dstnat dst-address-type=local dst-port= protocol=tcp to-addresses= to-ports= add action=dst-nat chain=dstnat dst-address-type=local dst-port= protocol=udp to-addresses= to-ports= add # # IPv4 dynamic source NAT. # /ip firewall nat add action=masquerade chain=srcnat out-interface=WAN # # IPv6 address of the LAN interfaces. # # The network part of the IPv6 address (prefix) is taken from the pool # (POOL6) obtained with prefix delegation. The host part of the IPv6 # address is configured manually. # # In this example, the host part of the IPv6 address of the interfaces # is ::1. The netmask it will be /64 automatically from the pool. # /ipv6 address add address=::1 from-pool=POOL6 interface=LAN1 add address=::1 from-pool=POOL6 interface=LAN2 # # IPv6 DHCP client in the WAN interface. # # The DHCP client request an IPv6 address for the WAN interface and an IPv6 # prefix for the pool POOL6. The IPv6 default route is created in the routing # table and the FW6RULES script is executed when the bind expires and the # DHCP client obtains another IPv6 prefix for the pool. The FW6RULES script # update the IPv6 firewall filter rules to the new dst-address. # /ipv6 dhcp-client add add-default-route=yes interface=WAN pool-name=POOL6 request=address,prefix script=FW6RULES # # IPv6 bogon (martians) prefixes. # # The bogon or martians prefix are prefix no routeables # or not used in internet. This list is a brief summary # of the complete list: https://bit.ly/2uxN3G8 and # https://bit.ly/2IZy2QN # /ipv6 firewall address-list add address=::/3 list=BOGONv6 add address=4000::/2 list=BOGONv6 add address=8000::/2 list=BOGONv6 add address=c000::/3 list=BOGONv6 add address=e000::/4 list=BOGONv6 add address=f000::/5 list=BOGONv6 add address=f800::/6 list=BOGONv6 add address=fc00::/7 list=BOGONv6 add address=fe00::/9 list=BOGONv6 add address=fec0::/10 list=BOGONv6 add address=ff00::/8 list=BOGONv6 # # IPv6 firewall rules: input. # # Traffic destined to the router. # # The ICMP and UDP traffic from and to fe80::/10 and ff02::/16 are used by # the IPv6 DHCP client to obtain the address and prefix delegation. # /ipv6 firewall filter add action=accept chain=input connection-state=established,related add action=accept chain=input dst-address=fe80::/10 in-interface=WAN protocol=icmpv6 src-address=fe80::/10 add action=accept chain=input dst-address=ff02::/16 in-interface=WAN protocol=icmpv6 src-address=fe80::/10 add action=accept chain=input dst-address=fe80::/10 dst-port=546 in-interface=WAN protocol=udp src-address=fe80::/10 add action=accept chain=input in-interface=LAN1 add action=accept chain=input in-interface=LAN2 add action=drop chain=input in-interface=WAN src-address-list=BOGONv6 add add action=drop chain=input # # IPv6 firewall rules: forward. # # Traffic wich pass through the router. # # In the out-interface of every rule you must configure the correct LAN # interface. This field is used by the script to identify the rule, the # out interface, and the new IPv6 prefix of the interface when changes. # # The dst-address in the rule is the host part and mask of the IPv6 address # of the internal servers. Initially you can set the full IPv6 address (actual # prefix /64 + host part /64) and mask or only the host part (/64) and mask. # Later, the FW6RULES script changes the dst-address of the rule to the right # full IPv6 address. # # The host part of the IPv6 address in the server must be fixed to the same # value configured in the firewall rule. This value must be the same across # the server restarts. In the server, you must disable IPv6 privacy extension # and it is convenient to configure the host part of the IPv6 address manually. # # In GNU/Linux you can disable the IPv6 privacy extension with this: # # echo "net.ipv6.conf.all.use_tempaddr=0" > /etc/sysctl.d/95-ipv6-privacy.conf # echo "net.ipv6.conf.default.use_tempaddr=0" >> /etc/sysctl.d/95-ipv6-privacy.conf # # With this, the server set the same host part of the IPv6 address between # reboots. # # Also, you can force the host part of the IPv6 address manually with the # tokenized interface identifer support. In the /etc/network/interfaces: # # iface eth0 inet6 auto # pre-up /sbin/ifconfig eth0 up # pre-up /sbin/ip token set ::aaaa:bbbb:cccc:dddd dev eth0 # # For example: if the IPv6 prefix obtained is 1111:2222:3333:4444::/64, the # full IPv6 address of the server will be 1111:2222:3333:4444:aaaa:bbbb:cccc:dddd/64. # The FW6RULES script detects this change and reconfigure the firewall rule. # For more info, see here: https://bit.ly/2pQRZR3 # # In summary: # in dst-address: put the host part and mask of the server(s). # in out-interface: put the name of the LAN interface. # run the script manually only once: /system script run FW6RULES # /ipv6 firewall filter add action=accept chain=forward connection-state=established,related add action=accept chain=forward in-interface=LAN1 add action=accept chain=forward in-interface=LAN2 add action=drop chain=forward in-interface=WAN src-address-list=BOGONv6 add action=accept chain=forward dst-address=::aaaa:bbbb:cccc:dddd/128 dst-port= in-interface=WAN out-interface=LAN1 protocol=tcp <...> add action=accept chain=forward dst-address=::aaaa:bbbb:cccc:dddd/128 dst-port= in-interface=WAN out-interface=LAN1 protocol=udp <...> add action=accept chain=forward dst-address=::1111:2222:3333:4400/120 dst-port= in-interface=WAN out-interface=LAN2 protocol=tcp <...> add action=accept chain=forward dst-address=::1111:2222:3333:4400/120 dst-port= in-interface=WAN out-interface=LAN2 protocol=udp <...> add action=accept chain=forward dst-address=::8888:eeee:5555:0/112 dst-port= in-interface=LAN1 out-interface=LAN2 protocol=tcp <...> add action=accept chain=forward dst-address=::8888:eeee:5555:0/112 dst-port= in-interface=LAN1 out-interface=LAN2 protocol=udp <...> add add action=drop chain=forward # # IPv6 Neighbor Discovery. # # Set the interfaces in which the router advertisement is sent and # the interval. # /ipv6 nd set [ find default=yes ] disabled=yes add interface=LAN1 mtu=1500 ra-interval=10s-30s add interface=LAN2 mtu=1500 ra-interval=10s-30s # # IPv6 Neighbor Discovery. # # Set the preferred and valid lifetime of the prefixes advertised. # /ipv6 nd prefix default set preferred-lifetime=2m valid-lifetime=2m30s # # Clock. # # Set the NTP client, server and the timezone. # /system ntp client set enabled=yes primary-ntp= secondary-ntp= /system ntp server set enabled=yes /system clock set time-zone-autodetect=no time-zone-name= # # Hostname of the router and no banner at login. # /system identity set name=MIKROTIK /system note set show-at-login=no # # End of the configuration. # ### Script ----------- # # Create the script with read and write permissions. # /system script add name=FW6RULES policy=read,write # # Edit the source code of the script. # # Note: below is the script in raw mode wich you can copy and paste # without the comments. # /system script edit FW6RULES source # # Begin of the code. # #-- Wait for the DHCP completion. :delay 5s; #-- #-- Function: return the uncompressed value of an IPv6 address #-- or a IPv6 address + mask. #-- #-- Parameters: #-- $1: IPv6 address to expand. #-- #-- Examples: #-- Param $1 Return #-- ----------- --------- #-- ::/128 0:0:0:0:0:0:0:0/128 #-- ::1/128 0:0:0:0:0:0:0:1/128 #-- 2002:3003::/112 2002:3003:0:0:0:0:0:0/112 #-- 2112:3113::4114:0/120 2112:3113:0:0:0:0:4114:0/120 #-- 2332:2:3:4:5:6:7:8/128 2332:2:3:4:5:6:7:8/128 #-- :: 0:0:0:0:0:0:0:0 #-- ::1 0:0:0:0:0:0:0:1 #-- 2002:3003:: 2002:3003:0:0:0:0:0:0 #-- 2112:3113::4114:0 2112:3113:0:0:0:0:4114:0 #-- 2332:2:3:4:5:6:7:8 2332:2:3:4:5:6:7:8 #-- :local EXPANDIP do={ #-- Original IPv6 address. :local ORIGINAL "$1"; #-- Uncompressed IPv6 address. :local EXPANDED "$ORIGINAL"; #-- Search for "::". If not found, then the IPv6 address #-- is already uncompressed. :local POSTWO [:find $ORIGINAL "::" -1]; :if ($POSTWO >= 0) do={ #-- Split the IPv6 address with the separator "::". :local PREFIX [:pick $ORIGINAL 0 $POSTWO]; :local SUFFIX [:pick $ORIGINAL ($POSTWO + 2) 99]; #-- If any string is empty, set to "0". This happens when #-- the IPv6 address starts and/or ends with "::". :if ([:len $PREFIX] = 0) do={ :set PREFIX "0"; } :if ([:len $SUFFIX] = 0) do={ :set SUFFIX "0"; } #-- If the suffix starts with "/", insert a "0" #-- before de mask. This happens when the IPv6 #-- address ends with "::/NN". :if ($SUFFIX ~ "^/") do={ :set SUFFIX ("0" . $SUFFIX); } #-- Count the ":" character of the IPv6 address. :local CNTTWO 0; :for NDX from=0 to=([:len $ORIGINAL] - 1) do={ :if ([:pick $ORIGINAL $NDX] = ":") do={ :set CNTTWO ($CNTTWO + 1); } } #-- Calculate how much is needed to fill #-- the IPv6 address with ":0:" in the middle #-- to uncompress. :set CNTTWO (8 - $CNTTWO); #-- Calculate the mid string with ":0:" to #-- fill the IPv6 address. :local MEDFIX ""; :if ($CNTTWO > 0) do={ :for NDX from=1 to=$CNTTWO do={ :set MEDFIX ($MEDFIX . ":0"); } } :set MEDFIX ($MEDFIX . ":"); #-- Calculate the expanded IPv6 address. :set EXPANDED ($PREFIX . $MEDFIX . $SUFFIX); } #-- Finish. :return $EXPANDED; } #-- #-- Function: return the prefix or host part of an #-- expanded IPv6 address. #-- #-- The prefix and host boundary always is /64 regardless the #-- mask of the IPv6 address. #-- #-- Parameters: #-- $1: expanded IPv6 address. #-- $2: false: return the prefix part (first 64 bits). #-- true: return the host part (last 64 bits) and mask. #-- #-- Examples: #-- $1 $2=false $2=true #-- ------------------ ------------ ------------ #-- 1111:2222:3333:4444:aaaa:bbbb:cccc:dddd/128 1111:2222:3333:4444 aaaa:bbbb:cccc:dddd/128 #-- 1111:2222:3333:4444:aaaa:bbbb:cccc:dddd 1111:2222:3333:4444 aaaa:bbbb:cccc:dddd #-- 0:1:2:3:4:5:6:7/120 0:1:2:3 4:5:6:7/120 #-- 0:1:2:3:4:5:6:7 0:1:2:3 4:5:6:7 #-- a:b:c:d:1:2:3:4/64 a:b:c:d 1:2:3:4/64 #-- :local GETNETHOST do={ #-- Parameters. :local DIRIP "$1"; :local ISHOST "$2"; #-- Search for the fourth two points #-- (the prefix / host boundary). :local POSDT -2; :for NUMDT from=1 to=4 do={ :set POSDT [:find $DIRIP ":" ($POSDT + 1)]; } #-- String with the result. :local RESULT ""; #-- Check for prefix / host part. :if ($ISHOST = true) do={ #-- Calculate the host part. :set RESULT [:pick $DIRIP ($POSDT + 1) 99]; } else={ #-- Calculate the prefix part. :set RESULT [:pick $DIRIP 0 $POSDT]; } #-- Finish. :return $RESULT; } #-- #-- Get the IPv6 prefix of every LAN interface. #-- #-- Fill the NETLIST array with the name and the expanded #- IPv6 prefix: #-- #-- NETLIST = { "NAME1" -> "PREFIX1"; #-- "NAME2" -> "PREFIX2"; #-- .... -> .... } #-- #-- Considers only interfaces with an IPv6 pool associated. #-- If a interface have more than one IPv6 address from a pool, #-- the algorithm takes into account only the last IPv6 address #-- obtained for that interface and change the firewall rule #-- with that prefix. #-- #-- Example: #-- NETLIST = { "LAN1" -> "2002:3440:3efa:3299"; #-- "LAN2" -> "2003:0:0:0"; } #-- :local NETLIST ({}); #-- Loop through all interfaces with global IPv6 address. :foreach INTNDX in=[/ipv6 address find global] do={ #-- Get the name of the pool of the interface. :local POOLNAM [/ipv6 address get number=$INTNDX from-pool]; #-- Check if is a valid pool name. #-- If the DHCP proccess fails or the interface not obtain the #-- IPv6 address from a pool, the pool name is invalid. :if ([:len $POOLNAM] > 0) do={ #-- Get the index of the pool. :local POOLNDX [/ipv6 pool find name=$POOLNAM]; #-- Check if is a valid index. :if ([:len $POOLNDX] > 0) do={ #--- Check if is a valid IPv6 prefix. #--- If the DHCP proccess fails, the pool not have a valid prefix. :if ([:len [/ipv6 pool get number=$POOLNDX prefix]] > 0) do={ #-- The pool is valid, then the IPv6 address of the interface #-- is valid. Get the IPv6 address of the interface. :local INTNETW [/ipv6 address get number=$INTNDX address]; #-- Check if the netmask is /64. :if ($INTNETW ~ "/64\$") do={ #-- Get the name of the interface. :local INTNAME [/ipv6 address get number=$INTNDX interface]; #-- Fill the array. Expand the IPv6 address and get the prefix part. :set ($NETLIST->$INTNAME) [$GETNETHOST [$EXPANDIP $INTNETW] false]; } } } } } #-- #-- Check every IPv6 firewall filter rule. Change the dst-address of the #-- rules when is necessary. #-- #-- The rules must be configured with the out-interface to match the NETLIST #-- array. #-- #-- Get every firewall filter rule (only forward chain). :foreach RULENDX in=[/ipv6 firewall filter find chain="forward"] do={ #-- Get the out-interface of the rule. :local RULELAN [/ipv6 firewall filter get number=$RULENDX out-interface]; #-- Get the new IPv6 prefix of the out-interface of the rule. :local NEWPRFX ($NETLIST->$RULELAN); #-- Check if the out-interface have a valid IPv6 prefix. :if ([:len $NEWPRFX] > 0) do={ #-- Get the destination address of the rule. :local RULEDST [$EXPANDIP [/ipv6 firewall filter get number=$RULENDX dst-address]]; #-- Calculate the new IPv6 address of the rule #-- with the new IPv6 prefix. :local NEWADDR ($NEWPRFX . ":" . [$GETNETHOST $RULEDST true]); #-- Check if the dst address of the rule is different #-- of the new calculated address. :if ($RULEDST != $NEWADDR) do={ #-- Change the dst-address of the rule. :local RET [/ipv6 firewall filter set numbers=$RULENDX dst-address=$NEWADDR]; #-- Log the change. :log info ("fw6rules: ipv6 fw rule " . $RULENDX . " changed, " . $RULELAN . " new dstaddr " . $NEWADDR); } } } #-- Finish. :log info ("fw6rules: executed"); ### Raw script --------------- /system script add name=FW6RULES policy=read,write source="\ \n:delay 5s;\ \n\ \n:local EXPANDIP do={\ \n :local ORIGINAL \"\$1\";\ \n :local EXPANDED \"\$ORIGINAL\";\ \n :local POSTWO [:find \$ORIGINAL \"::\" -1];\ \n :if (\$POSTWO >= 0) do={\ \n :local PREFIX [:pick \$ORIGINAL 0 \$POSTWO];\ \n :local SUFFIX [:pick \$ORIGINAL (\$POSTWO + 2) 99];\ \n :if ([:len \$PREFIX] = 0) do={ :set PREFIX \"0\"; }\ \n :if ([:len \$SUFFIX] = 0) do={ :set SUFFIX \"0\"; }\ \n :if (\$SUFFIX ~ \"^/\") do={ :set SUFFIX (\"0\" . \$SUFFIX); }\ \n\ \n :local CNTTWO 0;\ \n :for NDX from=0 to=([:len \$ORIGINAL] - 1) do={\ \n :if ([:pick \$ORIGINAL \$NDX] = \":\") do={\ \n :set CNTTWO (\$CNTTWO + 1);\ \n }\ \n }\ \n :set CNTTWO (8 - \$CNTTWO);\ \n\ \n :local MEDFIX \"\";\ \n :if (\$CNTTWO > 0) do={\ \n :for NDX from=1 to=\$CNTTWO do={\ \n :set MEDFIX (\$MEDFIX . \":0\");\ \n }\ \n }\ \n :set MEDFIX (\$MEDFIX . \":\");\ \n :set EXPANDED (\$PREFIX . \$MEDFIX . \$SUFFIX);\ \n }\ \n :return \$EXPANDED;\ \n }\ \n\ \n:local GETNETHOST do={\ \n :local DIRIP \"\$1\";\ \n :local ISHOST \"\$2\";\ \n :local POSDT -2;\ \n :for NUMDT from=1 to=4 do={\ \n :set POSDT [:find \$DIRIP \":\" (\$POSDT + 1)];\ \n }\ \n :local RESULT \"\";\ \n :if (\$ISHOST = true) do={\ \n :set RESULT [:pick \$DIRIP (\$POSDT + 1) 99];\ \n } else={\ \n :set RESULT [:pick \$DIRIP 0 \$POSDT];\ \n }\ \n :return \$RESULT;\ \n }\ \n\ \n:local NETLIST ({});\ \n:foreach INTNDX in=[/ipv6 address find global] do={\ \n :local POOLNAM [/ipv6 address get number=\$INTNDX from-pool];\ \n :if ([:len \$POOLNAM] > 0) do={\ \n :local POOLNDX [/ipv6 pool find name=\$POOLNAM];\ \n :if ([:len \$POOLNDX] > 0) do={\ \n :if ([:len [/ipv6 pool get number=\$POOLNDX prefix]] > 0) do={\ \n :local INTNETW [/ipv6 address get number=\$INTNDX address];\ \n :if (\$INTNETW ~ \"/64\\\$\") do={\ \n :local INTNAME [/ipv6 address get number=\$INTNDX interface];\ \n :set (\$NETLIST->\$INTNAME) [\$GETNETHOST [\$EXPANDIP \$INTNETW] false];\ \n }\ \n }\ \n }\ \n }\ \n }\ \n\ \n:foreach RULENDX in=[/ipv6 firewall filter find chain=\"forward\"] do={\ \n :local RULELAN [/ipv6 firewall filter get number=\$RULENDX out-interface];\ \n :local NEWPRFX (\$NETLIST->\$RULELAN);\ \n :if ([:len \$NEWPRFX] > 0) do={\ \n :local RULEDST [\$EXPANDIP [/ipv6 firewall filter get number=\$RULENDX dst-address]];\ \n :local NEWADDR (\$NEWPRFX . \":\" . [\$GETNETHOST \$RULEDST true]);\ \n :if (\$RULEDST != \$NEWADDR) do={\ \n :local RET [/ipv6 firewall filter set numbers=\$RULENDX dst-address=\$NEWADDR];\ \n :log info (\"fw6rules: ipv6 fw rule \" . \$RULENDX . \" changed, \" . \$RULELAN . \" new dstaddr \" . \$NEWADDR);\ \n }\ \n }\ \n }\ \n\ \n:log info (\"fw6rules: executed\");\ \n" ### End -------- If you wants a new feature or found a bug, please open an issue in BitBucket: https://bitbucket.org/mangelo/snippets/src Thanks.