Skip to content

Instantly share code, notes, and snippets.

@Hermanverschooten
Last active March 9, 2022 19:10
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Hermanverschooten/40c701b7f52e256502c9fe78473912e4 to your computer and use it in GitHub Desktop.
Save Hermanverschooten/40c701b7f52e256502c9fe78473912e4 to your computer and use it in GitHub Desktop.
Ubuntu 18.04 as a router with IPv6
#!/bin/sh -e
#
# rc.local
#
# This script is executed at the end of each multiuser runlevel.
# Make sure that the script will "exit 0" on success or any other
# value on error.
#
# In order to enable or disable this script just change the execution
# bits.
#
# By default this script does nothing.
#pon destiny
echo 0 > /proc/sys/net/ipv6/conf/lan/disable_ipv6
sysctl -w net.ipv6.conf.telenet.accept_ra=2
sysctl -p
exit 0

Ubuntu 18.04 as a router with IPv6

With the release of Ubuntu 18.04, it was decided to replace the traditional network configuration with the YAML-based netplan. Although the configuration initially seems a lot easier and more user-friendly, when you add IPv6 and routing in the mix, it soon goes terribly wrong. Most explanations on netplan show you how to set it up on a host machine, or with static routing, but none show the hassle of setting it up as a router with a dynamic IPV6 address-space given by your ISP.

The initial config

Create a .yaml file in /etc/netplan, or edit the one that's already there.

network:
  version: 2
  renderer: networkd
  ethernets:
    enp1s0f1:
      match:
        maccaddress: 00:15:XX:XX:XX:01
      addresses:
        - 192.168.1.1/24
        - 2001:abba:abba:abba::1/64
      nameservers:
        addresses:
          - 8.8.8.8
          - 8.8.4.4
      set-name: lan
    enp1s0f0:
      match:
        maccaddress: 00:15:XX:XX:XX:00
      set-name: wan
      dhcp4: true
      dhcp6: true
      accept-ra: true
      macaddress: 01:02:03:04:05:06

So we have 2 network interfaces, enp1s0f0 wich will be our wan-interface and enp1s0f1 our lan-interface. To be certain I added the mac-addresses of the interfaces in a match section, then I renamed the interfaces with set-name. So a ifconfig will show a lan and a wan interface.

We defined the lan with 2 static ip addresses, assume that the IPv6 address is the lowest address in the /64 subnet given to you by your ISP.

My ISP expects the DHCP IPv4 request to come from a registered MAC address (01:02:03:04L05:06 in this case). I'll get to the IPv6 in a minute...

Ok, so far so good, netplan apply activates the configuration. Beware however it does not remove any previous active configurations, so you old interfaces still exist with their ip addresses, ... And while you are testing, sometimes you'll get multiple of the same address on interfaces, and what more. If you are sure of your config, I prefer a reboot.

##Enabling routing After the reboot, test your IP connectivity and if it is working, then it is time to add the routing.

As explained in all tutorials, you activate routing by sysctl -w net.ipv4.ip_forward=1, followed by sysctl -p, and addin the necessary masquerading rules to iptables. If all is OK, you should now be able to access the internet from a workstation, providing it has the correct ip and gateway. You should setup a dhcp-server, for a tutorial unverified, look here. Don't forget to make the changes permanent by adding them to /etc/sysctl.conf or they will be gone after a reboot.

Now to enable IPv6 routing, we should just do the same... sysctl -w net.ipv6.conf.all.forward=1, sysctl -p. But sadly that is not enough... To do routing in an IPv6 environment you need to do router-anouncements (or short RA), this can be done with radvd. So we apt install this, and configure it:

interface lan {
  AdvManagedFlag off;   # no DHCPV6 server here.
  AdvOtherConfigFlag off;       # not even for options;
  AdvSendAdvert on;
  AdvDefaultPreference high;
  AdvLinkMTU 1280;
  prefix 2001:abba:abba:abba::/64
  {
    AdvOnLink on;
    AdvAutonomous on;
  };
  RDNSS 2001:abba:abba:abba::1 { };
};

This config assumes you have a DNS server running on this host as well... (the RDNSS line).

Checking on a client machine on the network, yes we have an IPV6, and I can ping6 this host. Trying to ping a host on the internet using IPv6 fails... Huh, what is going on?

DHCP on IPv6 and DUID

My ISP (Telenet in Belgium) requires me to fill in a DUID, a DHCP Unique Identifier. This is can be done using the wide_mkduid.pl script with the the name of the interface. It shows the DUID, enter it on the webpage. Reboot the server so it refreshes it's dhcp info, and try again. Still no luck. Checking the docs for netplan, shows a dhcp-identifier, sadly the only option you can specify is mac, and it only works for IPv4. Ah, but networkd has an option for this, add/edit the file /etc/systemd/networkd.conf and add

[DHCP]
DUIDType=vendor
DUIDRawData=00:00:<followed by the DUID>

Try again. Still not working!

Back to the drawing board! It seems netplan can not configure this in the right way to ask networkd to make a correct dhcp request with the DUID. I know from using ubuntu 16.04 that wide-dhcpv6-client can, but how do we get this working?

Step 1 - Remove IPv6

Replace dhcp6 and accept-ra, from the enp1s0f0 section of the netplan yaml-file, with link-local: [ ]. This will effectively disable IPv6 on the interface. Why, because netplan hogs port 546 on that interface even if we say we do not want it to do dhcp6, and dhcp6c needs to be able to bind to it. This is apparently no longer neccessary, just set both dhcp6 and accept_ra to false. If you leave the link-local: [], the wide-dhcpv6-client will error out with client6_send: transmit failed: Cannot assign requested address.

Step 2 - Add dhcp6c

Create or edit the file /etc/wide-dhcpv6/dhcp6c.conf:

# Default dhpc6c configuration: it assumes the address is autoconfigured using
# router advertisements.

profile default
{
#  information-only;

#  request domain-name-servers;
#  request domain-name;

  script "/etc/wide-dhcpv6/dhcp6c-script";
};

interface wan {
   send ia-pd 0;
   send ia-na 0;
};

id-assoc pd 0 {
  prefix-interface lan {
    sla-len 8;
    sla-id 0;
    ifid 1;
  };
};

id-assoc na 0 {
};

Remember when we ran wide_mkduid.pl? It not only showed us the DUID, but it also created a dhcp6c_duid file in folder where we ran it. Move this file to /var/lib/dhcpv6/.

Step 3 - Make it work

So after -yet another- reboot, you check your lan interface, ifconfig lan, only to find that it has no more IPv6. Yes, by removing the IPv6 options in Step 1, networkd disabled IPv6 on the interface, as can be seen in /proc/sys/net/ipv6/conf/wan/disable_ipv6, it now contains 1.

echo 0 > /proc/sys/net/ipv6/conf/wan/disable_ipv6, and we have a link-local IPv6 address, systemctl restart wide-dhcpv6-client, and we have a public ip, but no access.

Some more research and I found that linux disables accept_ra when you enable forwarding on an interface!???

So sysctl -w net.ipv6.conf.wan.accept_ra=2, followed by sysctl -p. And we have internet over IPv6, locally and for the rest of the network!

To make these changes permanent, I created a /etc/rc.local file, and made it executable.

Step 4 - The final reboot

Yes, a final reboot, to check that everything is working.

Instead of making the changes to /etc/sysctl.conf you could also add these commands to /etc/rc.local to be executed upon boot.

#!/usr/bin/perl -w
#### client DUID generator for WIDE-DHCPv6
#### (C)2007 Jeffrey F. Blank <jfb@mtu.edu> / Michigan Technological University
use Config;
use Getopt::Std;
use POSIX;
$FN = getcwd() . '/dhcp6c_duid';
getopts('hm:t:', \%opts);
if ( defined($opts{h}) ) {
&usage;
exit 0;
}
if ( (defined($opts{m}) && $#ARGV >= 0) ||
(!defined($opts{m}) && ($#ARGV != 0 || $ARGV[0] =~ /^-/o)) )
{
&usage;
exit 1;
}
if ( defined($opts{t}) ) {
# timestamp specified; check its format (positive int or "now")
$opts{t} = time() if $opts{t} eq 'now';
if ( $opts{t} !~ /^\d+$/o ) {
&usage;
exit 1;
}
# LLT DUID type
$duidtype = 1;
} else {
# LL DUID type
$duidtype = 3;
}
if ( defined($opts{m}) ) {
# MAC address specified; use it instead of running 'ifconfig'
$l = $opts{m};
} else {
# interface name specified; run 'ifconfig' to retrieve its MAC address
# start with a default of /sbin/ifconfig and update it if found in $PATH
$ifconfig = '/sbin/ifconfig';
@path = split(/:/o, $ENV{PATH});
foreach(@path) {
if ( -e "$_/ifconfig" ) {
$ifconfig = "$_/ifconfig";
last;
}
}
# popen ifconfig command and read its output
open(IFC, "$ifconfig $ARGV[0]|")
or die "$0: can't popen $ifconfig: $!\n";
if ( ! (@ifc=<IFC>) ) {
# no need to print an error, as ifconfig probably already did
exit ($? >> 8);
}
close(IFC);
# we expect the MAC address to be preceded by "hwaddr" or "ether"
# and colon-separated
@ifc = grep { /(ether|hwaddr)\s*[0-9a-f]{1,2}(:[0-9a-f]{1,2}){5}/oi } @ifc;
if ( $#ifc != 0 ) {
print STDERR "$0: cannot decipher 'ifconfig' output\n";
exit 3;
}
chomp ($l=shift @ifc);
$l =~ s/^.*(hwaddr|ether)\s*//o;
$l =~ s/\s.*//oi;
}
# form the first two words of the DUID data: DUID type and link type.
# link-type is assumed to be ethernet(6)!
$duid_data = chr(0) . chr($duidtype) . chr(0) . chr(6);
if ( defined($opts{t}) ) {
# create string from byte values, host byte order
for ( $i=24; $i >= 0; $i -= 8 ) {
$duid_data .= chr(($opts{t} >> $i) & 0xff);
}
}
@mb = split(/:/o, $l);
foreach(@mb) {
$duid_data .= chr(hex($_));
}
# first two bytes are DUID length, so figure that out
$duidlen = length($duid_data);
open(DUID, ">$FN") or die "$0: can't create $FN: $!\n";
# DUID length must be in network byte order, so check what perl thinks its
# byte order is. could use htons() from Net::Inet, but that's not included
# in at least some base installations.
if ( substr($Config{byteorder}, 0, 1) eq '1' ) {
# reverse bytes on little-endian hosts
printf DUID "%c%c", $duidlen & 0xff, $duidlen >> 8;
} else {
# big-endian host; DUID length is already in network byte order
printf DUID "%c%c", $duidlen >> 8, $duidlen & 0xff;
}
# DUID itself is written in host byte order
print DUID $duid_data;
close(DUID) or die "$0: error closing dhpc6c_duid: $!\n";
# print out DUID for potential use in server config file
$fmt = "successfully created $FN\nDUID is %02x" . (':%02x' x ($duidlen - 1)) . "\n";
@duid_bytes = ();
for ( $i=0; $i < $duidlen; $i++ ) {
push @duid_bytes, ord(substr($duid_data, $i, 1));
}
printf $fmt, @duid_bytes;
### end main
##############
sub usage {
print STDERR "usage:\t$0 [ -t <time> ] { -m <macaddr> | <ifname> }\n" .
"\tif specified, <macaddr> must be 6 colon-separated hex values\n" .
"\tif specified, <time> must be an integer or 'now'\n";
}
1;
@Hermanverschooten
Copy link
Author

Thanks for the feedback, I have to try that sometime.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment