Glen Turner (vk5tu) wrote,
Glen Turner

IPv6 at home, under the hood with Debian Wheezy and Internode


One thing which isn't well understood is the big number of moving parts required to get IPv6 working with your ISP. This is a quick review of what happens under the hood of your ADSL router.

Four addresses, four addressing mechanisms

Firstly, PPP Over Ethernet link control protocol allocates the link-local addressing for the PPP link. It assigns a local link-local address to the customers interface and a remote link-local address to the ISP's interface. That gives us a medium we can use to exchange IPv6 packets between the ISP and the ADSL router.

Secondly, stateless address autoconfiguration (SLAAC) assigns a global address to the PPP link between the ISP and the ADSL router. That requires both ICMPv6 and link-layer multicast to be in working order, a point often forgotten when configuring IPv6 firewalls. When autoconfiguration installs this global address from the ISP it also installs the IPv6 default route from the ISP.

Thirdly, DHCPv6 runs over the PPP link to inform the ADSL router of the IPv6 address prefixes to use for the customer LANs on the interior-facing interfaces of the ADSL router. This DHCPv6 exchange also configures routing upon the upstream ISP equipment. The implication is that if DHCPv6 stops, then your internet stops when the DHCPv6 lease expires.

The implication is that you need to take some care with which addresses are used to advertise services. If they are advertised on the prefix delegated by the ISP and that delegation disappears then the service is unreachable. For example, you may not be able to print if your ADSL link goes down. Advertising services on the link-layer address is also a bit fraught, as then the service isn't available to downstream subnets within the site.

Fourthly, there is also IPv6 link-layer addressing on each of the interior interfaces of the ADSL router, configured in the usual way for ethernet interfaces

The scenario

So much for the theory, let's have a look at the practice. I use a RaspberryPi computer as the ADSL router. It contains these interfaces:

  • eth0   An interior LAN interface. A fast ethernet interface to a fast ethernet switch. The house's access point, computers and printers plug into this switch.

  • eth1   An exterior interface to the ISP. This is a USB interface to a D-Link DSL-526B ADSL modem, configured as a bridge. The USB port carries ethernet frames to the modem using the CDC protocol.

    The Raspberry Pi's BCM2835 system-on-a-chip has one USB port. This USB bus connects across the printed circuit board to a LAN9512, which places a USB hub with one ethernet controller and two USB ports onto the bus. As a result there is no performance using the ADSL modem's ethernet port or its CDC USB port: either way the traffic enters the Raspberry Pi's BCM2835 SoC as ethernet-over-USB.

Raspbian, a Debian Wheezy Stable tuned to the ARM variant contained within the Raspberry Pi's BCM2835, is the obvious choice of operating system for this sort of fun. Just for the record it has ppp-2.4.5-5.1, ifupdown-0.7.8, and raspberrypi-bootloader-1.20130617-1 containing Linux kernel "3.6.11+ #474".


In Linux hosts don't forward out-of-the box. Let's turn that on by editing /etc/sysctl.conf


What's this special value "2"? Originally the value was "1", but this disabled autoconfiguration on all interfaces. That is, you couldn't appear to be a router on some interfaces and appear to be a host on other interfaces. But that's exactly the mental model of a ADSL router. (This is the trouble you make when you ignore the UNIX tenet that the system administrator knows what they are doing.)

There's a similar trap with enabling autoconfiguration, it disables routing. So another special value (sigh):

net.ipv6.conf.all.accept_ra = 2
net.ipv6.conf.default.accept_ra = 2

The keyword all sets the value on all current interfaces. The keyword default sets the value on all future interfaces. We need both since we don't know when sysctl -p is run to make the file's contents take effect.

Interior interface, eth0

Debian configures its interfaces in /etc/network/interfaces. It typically has private IPv4 addressing which iptables NATs to the outside, looking something like this:

auto eth0
iface eth0 inet static

My ISP, Example.Net, uses static prefix delegations. So I can statically address the interior interface. This is good as we can run services on the interior interface without its global addressing being removed.

If your ISP doesn't do this then you should allocate a random /64 subnet from fc00::/7, RFC4193 has the details. You'll then need to bind local services running on the ADSL gateway -- such as printer spooling -- to the RFC4193 subnet rather than to the prefix delegated globally-reachable address. You won't be able to access these services from the Internet, but that was your ISP's intention in allocating a dynamic address, but at least you'll be able to reach the local services when the ADSL link is down.

Let's say I was allocated 2001:0db8:1234:abcd::/56. (Not really, because this is within the documentation prefix. But hey, this is documentation.) Set /etc/network/interfaces to add:

auto eth0
iface eth0 inet6 static
    address 2001:0db8:1234:abcd::1
    netmask 64
    autoconf 0
    accept_ra 0
    privext 0

You see that we've used subnet 00. Use whatever subnet you want of the allocation you have been given. It's just that zero leads to less typing. Note IPv6's convention of placing the router on address ::1.

You'll see that we've turned off autconfiguration for this interface. That makes sense, as we've statically configured the interface. But as usual with Linux flipping autoconf bits then flips forwarding bits :-( This hack puts things back to rights:

    post-up /sbin/sysctl -w net.ipv6.conf.eth0.forwarding=2 
    post-up /sbin/sysctl -w net.ipv6.conf.default.forwarding=2 
    post-up /sbin/sysctl -w net.ipv6.conf.default.accept_ra=2 

Exterior interface, eth1

We don't need IP addressing on the ethernet interface to the ADSL modem. This ethernet link just carries PPP frames.

allow-hotplug eth1
iface eth1 inet manual
  pre-up /sbin/ifconfig eth1 up
  post-down /sbin/ifconfig eth1 down
We then add an ISP to that configuration:

allow-hotplug eth1
iface eth1 inet manual
  pre-up /sbin/ifconfig eth1 up
  up ifup ppp0=example
  down ifdown ppp0=example
  post-down /sbin/ifconfig eth1 down

iface example inet ppp
  provider example

PPPoE over exterior interface, ppp0

Configured in /etc/ppp/peers/example:

You'd have all of the regular features:
plugin eth1
user ""
bsdcomp 15
deflate 15

together with authentication in /etc/ppp/chap-secrets:

"" * "Zgz4T3CRCGhH6G7Zf8mb0vIO"

Now add these to /etc/ppp/peers/example for IPv6:

+ipv6 ipv6cp-use-ipaddr
ipv6 ,

Note that comma carefully, it is needed.

Restart PPP with ifdown ppp0=example && ifup ppp0=example and look in the log messages for link-layer address assignments.

pppd[]: pppd 2.4.5 started by root, uid 0
pppd[]: PPP session is 3
pppd[]: Connected to 00:11:22:33:44:55 via interface eth1
pppd[]: Using interface ppp0
pppd[]: Connect: ppp0 <--> eth1
pppd[]: CHAP authentication succeeded
pppd[]: peer from calling number 00:11:22:33:44:55 authorized
pppd[]: local  LL address fe80::255:44ff:fe33:2211  
pppd[]: remote LL address fe80::211:22ff:fe33:4455
pppd[]: local  IP address
pppd[]: remote IP address
pppd[]: primary   DNS address
pppd[]: secondary DNS address

ISP link global addressing and default route, ppp0

ICMPv6 is used to implement IPv6 stateless automatic address configuration (SLAAC). The router advertises the subnet's prefix and the hosts add their own host addressing to that. To aid in boot time the host and prompt the router to immediately send an advertisement.

Note that these advertisements are link-layer multicasts. Some with global source addresses. It is very easy to shoot yourself in the foot with your firewall rules here. The symptom is a delayed loss of the default route, seemingly for no apparent reason.

The use of SLAAC is set using Linux's sysctl. You want these, which should already be set:

net.ipv6.conf.ppp0.disable_ipv6 = 0
net.ipv6.conf.ppp0.forwarding = 2
net.ipv6.conf.ppp0.autoconf = 1
net.ipv6.conf.ppp0.accept_ra = 2
net.ipv6.conf.ppp0.accept_ra_defrtr = 1
net.ipv6.conf.ppp0.accept_ra_pinfo = 1
net.ipv6.conf.ppp0.accept_redirects = 1
net.ipv6.conf.ppp0.accept_source_route = 0

To check operation use ip -f inet6 addr show to view the interface's addressing:

$ ip -f inet6 addr show dev ppp0
    inet6 2001:44b8:10:868:192b:b4b7:d760:9879/64 scope global dynamic 
       valid_lft 5237sec preferred_lft 5237sec
    inet6 fe80::192b:b4b7:d760:9879/10 scope link 
       valid_lft forever preferred_lft forever

Use ip -f inet6 route show to view the interface's routes.

$ ip -f inet6 route show dev ppp0
2001:44b8:10:868::/64  proto kernel  metric 256  expires 4585sec
fe80::/64  proto kernel  metric 256 
fe80::/10  metric 1 
fe80::/10  proto kernel  metric 256 
default via fe80::224:14ff:fe9a:9810  proto ra  metric 1024  expires 4525sec

If a default route isn't present then tcpdump -i ppp0 -n -v is the fastest diagnostic tool.

The SLAAC-assigned interface is dynamic. Furthermore the ISP might block access to it from the global internet. Unlike IPv4, do not use the address of the exterior interface for providing services on IPv6. Use one of the addresses from the prefix delegation. Subnet 0 is a good choice for this purpose.

ISP prefix delegation for the interior interfaces

Prefix delegation gives some addressing for you to use within your home network. You can allocate differing subnets for differing purposes. Some people like to run wired and wireless networks on different subnets to minimise the number of wireless broadcasts, which are transmitted at a low speed and thus rob wireless time from hosts doing real work.

You need to know in advance how many bits of subnetting (technically, "site local aggregator addressing") the ISP is giving you. 4, 8 and 16 are common values. For a simple network you can look at these as subnet numbers. If you use VLANs then it's a good plan to have the SLA address match the VLAN number. If you have a simple network then just use subnet 0 to make your typing simpler.

You also need to decide which IP address (technically, "interface identifier") you will use for your router's interior interfaces. ...::1 is a common and good choice and what we use in this example.

As a concrete example, we will use subnet 1 (to match the default VLAN ID) and address ::1 for the single interior interface eth0.

The ISC DHCPv6 client doesn't work over PPP links, due to a long-standing bug.

I used the WIDE DHCPv6 client. Debian packages this well, with all the necessary helper scripts out of the box.

Set /etc/wide-dhcpv6/dhcp6c.conf:

profile default
  request domain-name-servers;
  request domain-name;
  script "/etc/wide-dhcpv6/dhcp6c-script";
interface ppp0 {
  # Request a prefix delegation
  send ia-pd 1;
id-assoc pd 1 {
  prefix-interface eth0 {
    # 8 bits for subnetting
    sla-len 8;
    # Our subnet is VLAN 1
    sla-id 1;
    # Our interface has address ...::1
    ifid 1;

WIDE DHCPv6 is very quiet. If you want to debug it then hack the /etc/init.d/wide-dhcpv6-client script to add the -D debug parameter.

If you run internal services then you can set up a eth:0 interface and use subnet 0 for it. It's common for services to bind to particular addresses: ...::100 might be a web server, ::101 might be the mail server, and so on.


We are not NATing, so no firewall is required for operation. Of course we do want to keep the greater internet out of our pictures of Aunt Mabel's wedding reception. So we do want to run a stateful firewall.</p>

I find it attractive to run a stateful firewall for connections which are forwarded through the router, but not those connecting to it. I then configure those services running on the router to reject improper usage. In this case you want the expectation-making rule in the FORWARD chain, as well as the expectation-enforcement rule for outgoing and incoming traffic.

ip6tables -A FORWARD -i eth0 -o ppp0 -m state --state NEW -j ACCEPT
ip6tables -A FORWARD -i eth0 -o ppp0 -m state --state RELATED,ESTABLISHED -j ACCEPT
ip6tables -A FORWARD -i ppp0 -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT

This simple rule checks for FORWARDed ICMPv6, discarding incoming ping6:

-A check-forward-icmp -p ipv6-icmp -m icmp6 --icmpv6-type 1 -j accept-forward-icmp
-A check-forward-icmp -p ipv6-icmp -m icmp6 --icmpv6-type 2 -j accept-forward-icmp
-A check-forward-icmp -p ipv6-icmp -m icmp6 --icmpv6-type 3 -j accept-forward-icmp
-A check-forward-icmp -p ipv6-icmp -m icmp6 --icmpv6-type 4 -j accept-forward-icmp
-A check-forward-icmp -i ppp0 -p ipv6-icmp -m icmp6 --icmpv6-type 128 -j reject-forward-icmp
-A check-forward-icmp -p ipv6-icmp -m icmp6 --icmpv6-type 128 -j accept-forward-icmp
-A check-forward-icmp -p ipv6-icmp -m icmp6 --icmpv6-type 129 -j accept-forward-icmp
-A check-forward-icmp -j reject-forward-icmp

Some comments

This is obviously more complex than with IPv4. Which should not be the case considering that IPv4 is NATing and IPv6 is routing. The major complexity and fragility is the Prefix Delegation, and doing this through SLAAC would have been a better design.

The software design is poor. The Linux kernel does its best to defeat being simultaneously a host and a router, which is the typical design for a ADSL "router". The WIDE and Dibbler DHCPv6 clients both fail when ppp0 losses carrier, rather than opening a new connection on the interface and trying again.

Errata: Router advertisement to interior network

[Whoops, left this out]

How does a host on the internal network automatically configure? It listens for advertisements from routers. Install the package radvd. Configure it to advertise the subnet's prefix and prefix length, the router's address, the MTU, and the DNS server details. radvd is fussy about the format of it's configuration file /etc/radvd.conf

interface eth0 {
  AdvSendAdvert on;
  IgnoreIfMissing on; 
  AdvLinkMTU 1500;
  AdvDefaultPreference high;
  prefix 2001:0db8:1234:abcd::/64 {
    AdvOnLink on;
    AdvAutonomous on;
  RDNSS 2001:0db8:1234:abcd::1 {
Tags: linux, raspberrypi

  • Blog moving to Dreamwidth

    Getting less and less happy with LiveJournal as a blogging platform: limited input formats, poor presentation, etc. But running your own blogging…

  • Activating IPv6 stable privacy addressing from RFC7217

    Understand stable privacy addressing In Three new things to know about deploying IPv6 I described the new IPv6 Interface Identifier creation…

  • Heatsink for RPi3

    I ordered a passive heatsink for system-on-chip of the Raspberry Pi 3 model B. Since it fits well I'll share the details: Order Fischer…

  • Post a new comment


    default userpic

    Your reply will be screened

    Your IP address will be recorded 

    When you submit the form an invisible reCAPTCHA check will be performed.
    You must follow the Privacy Policy and Google Terms of use.
Minor nitpick. When you set the following sysctls:


There is a slight race condition in between the two. "all" includes all existing interfaces, but not future interfaces. "default" includes all future interfaces, but no existing interfaces.

So if an interface (i.e. ppp0) is created in between the two statements, it will not have that applied. To do it race-free, just reverse the order:

Hi Glen, thanks for the great article I enjoyed playing with IPv6 the last couple days and getting it setup at home.

When using SLAAC you may want to enable some privacy as this is made up of the prefix and based on hardware mac address which can make it easy to be tracked.

In this case you may want to add to you sysctl.conf file

net.ipv6.conf.eth1.use_tempaddr = 2
net.ipv6.conf.all.use_tempaddr = 2
net.ipv6.conf.default.use_tempaddr = 2

which will randomize the part that is based on your hardware mac address
I choose not to use privacy addresses for these reasons:

1) My machines don't move. Therefore the network prefix does not change, so the activity can still be related to my subscription. If my machines moved around more often then there would be a gain in privacy.

2) Given (1) I'll take the convenience of being able to translate MAC address (often known) to IPv6 address. This makes the initial login to newly imaged devices very simple (no need to check DHCP servers, etc).

3) Privacy addresses are a poor choice for servers. Including ssh servers. Losing the ability to ssh to machines is more of a security concern to me than losing some minor (see (1)) privacy.

The default method of determining a IPv6 address will change to, essentially, prefix:random, where the random host portion is permanently maintained by the operating system after initial generation. In my view this has been woefully undertested (there will be a whole class of computers lacking sufficient randomness upon first-ever boot) and we'll pay the costs of that in future network design (eg, it won't be possible to have a large Link Layer of CPE devices).



June 1 2015, 02:04:49 UTC 5 years ago

Thanks for this write-up.

My setup is basically the same: ppp connection on eth1, local network on eth0. The ISP assigned me two networks: a /64 for the outside (ppp0) and a /56 for the inside (eth0). The outside address is assigned during the ppp connection, and I assigned an address from the /56 pool manually to eth0. I enabled forwarding and had the firewall wide open. Now I thought that I should be able to ping the internal address from another machine somewhere else. Didn't work. In Wireshark I saw nothing arriving on ppp0. Turns out that my ISP enables a route from the /64 to the /56 only when I request a /56 address from them with dhcp6c. On the router machine, I have to use dhcp6c just for that reason. Other machines in the local network are fine with manual configuration.

In /etc/ppp/peers/example I am using:

# enable IPv6
# local part of the IP address, lower 64 bit; this should give me a fixed IP
ipv6 ::1
# force pppd to create ppp0. I have another connection with unit 1 for ppp1
# in order to configure the fire wall, I need to know which one is which
unit 0
# no IPv4