Building a Debian GNU/Linux IPv6 home router
Introduction
This short post describes how i configured my own IPv6 home router using Debian GNU/Linux. I used a Dreamplug, but any form of device with at least 2 NICs should be usable. Allthough this guide describes the setup using Debian, it should be no problem using another distribution or one of the BSD variants.
I will not go into to many details about the configurations, for more information you should read the man pages and/or documentation.
Sources
This blog post from Phil Dibowitz was very helpful setting me on the right track and gives you more details about what is going on. Also I recommend reading Recommended Simple Security Capabilities in Customer Premises Equipment for Providing Residential IPv6 Internet Service.
DNS
At the minimum you want to have a recursive server for your clients. I also configured a zone and allowing dynamic updates so that dhcpd can add clients and also adding the routers own IP-addresses.
Installing bind
aptitude install bind9
Creating a key for dynamic updates
ddns-confgen -a hmac-md5 -r test -r /dev/urandom -q -k dhcp_updater > /etc/bind/dns-dhcp.key
chown root:bind /etc/bind/dns-dhcp.key
chown 640 /etc/bind/dns-dhcp.key
/etc/bind/named.conf.options
options {
directory "/var/cache/bind";
forwarders { };
//========================================================================
// If BIND logs error messages about the root key being expired,
// you will need to update your keys. See https://www.isc.org/bind-keys
//========================================================================
dnssec-validation yes;
auth-nxdomain no; # conform to RFC1035
listen-on-v6 { any; };
max-cache-size 44040192;
};
The empty forwarders statement will be filled in later by a post-up script. Set max-cache-size
to a suitable size or leave it out to use the defaults.
Setting up zones
I’m using bifrost.haavard.name
as my DNS zone for the network and 192.168.0.0/24
for the internal IPv4 network. My router is called heimdal
. Change accordingly for your needs or skip this if you don’t want an internal DNS zone.
mkdir /etc/bind/zones
chown root:bind /etc/bind/zones
chmod 2770 /etc/bind/zones
cat<<"EOD" > /etc/bind/zones/db.bifrost.haavard.name
$ORIGIN .
$TTL 3600 ; 1 hour
bifrost.haavard.name IN SOA heimdal.bifrost.haavard.name. post.haavard.name. (
1025 ; serial
604800 ; refresh (1 week)
86400 ; retry (1 day)
2419200 ; expire (4 weeks)
86400 ; minimum (1 day)
)
NS heimdal.bifrost.haavard.name.
heimdal A 192.168.0.1
EOD
cat<<"EOD" > /etc/bind/zones/db.192.168.0.0
$ORIGIN .
$TTL 3600 ; 1 hour
0.168.192.in-addr.arpa IN SOA heimdal.bifrost.haavard.name. post.haavard.name. (
6 ; serial
604800 ; refresh (1 week)
86400 ; retry (1 day)
2419200 ; expire (4 weeks)
86400 ; minimum (1 day)
)
NS heimdal.bifrost.haavard.name.
EOD
chown bind:bind /etc/bind/zones/*
/etc/bind/named.conf.local
include "/etc/bind/zones.rfc1918";
include "/etc/bind/bind.keys";
include "/etc/bind/dns-dhcp.key";
zone "bifrost.haavard.name" {
type master;
file "/etc/bind/zones/db.bifrost.haavard.name";
allow-update { key dhcp_updater; };
};
zone "0.168.192.in-addr.arpa" {
type master;
file "/etc/bind/zones/db.192.168.0";
allow-update { key dhcp_updater; };
};
/usr/local/bin/update-bind-forwarders
This script is used in the post up section of ifup to update the forwarders for bind.
#!/bin/bash
forwarders=$(egrep -v '127.0.0.1|::1' /etc/resolv.conf | awk '/^nameserver/ {print $2}' | tr '\n' ';')
sed -ri "s/forwarders[[:space:]]+\{[^}]+\};/forwarders { $forwarders };/" /etc/bind/named.conf.options
/etc/init.d/bind9 reload
DHCP server for IPv4
Installing dhcp server
aptitude install isc-dhcp-server
/etc/dhcpd.conf
ddns-update-style interim;
ddns-domainname "bifrost.haavard.name";
include "/etc/bind/dns-dhcp.key";
zone bifrost.haavard.name. {
primary 127.0.0.1;
key dhcp_updater;
}
zone 0.168.192.in-addr.arpa. {
primary 127.0.0.1;
key dhcp_updater;
}
option domain-name "bifrost.haavard.name haavard.name";
option domain-name-servers 192.168.0.1;
default-lease-time 86400;
max-lease-time 720000;
authoritative;
log-facility local7;
subnet 192.168.0.0 netmask 255.255.255.0 {
range 192.168.0.50 192.168.0.198;
option routers 192.168.0.1;
}
If you want to give some hosts static addresses, for instance for port forwarding, add to the config file
host XX {
hardware ethernet XX:XX:XX:XX:XX:XX;
fixed-address 192.168.0.X;
}
Router config
We’re going to run a post-script in dhcp which will configure addresses on the network interfaces and update DNS. To avoid hard coding this script, I use a config file which I put in /etc/router.conf
. If you do not want to use a local DNS zone, skip the dns_*
configuration parameters. NB! The dns_search
parameter is required if you want to use RDNSS.
/etc/router.conf
# the internet facing interface
external_nic = eth0
# space seperated list of internal nics
internal_nics = eth1
ipv4_prefix = 192.168.0.0/24
dns_update_server = ::1
dns_update_key = /etc/bind/dns-dhcp.key
dns_ttl = 3600
dns_zone = bifrost.haavard.name
dns_external_name = heimdal-ext
dns_eth1_name = heimdal
dns_search = bifrost.haavard.name haavard.name
radvd
I have choosen to use stateless autoconfig for IPv6. This is handled by the radvd daemon.
Installing radvd
aptitude install radvd
/etc/radvd.conf.tmpl
This is the template used by the dhcp post script to configure radvd. Modify as needed. If you do not want to use RDNSS remove the RDNSS
and DNSSL
lines.
interface __IFACE__ {
AdvSendAdvert on;
prefix __PREFIX__
{
AdvOnLink on;
AdvAutonomous on;
AdvRouterAddr on;
};
RDNSS __IP__ { };
DNSSL __SEARCH__ {};
};
Multicast routing daemon
You probably want to run a multicast routing daemon, I’ve choosen to use mrd6.
Installing mrd6
aptitude install mrd6
DHCP post script
This script will assign IP-addresses to the internal nics from the assigned address range, configure radvd as well as populate dns
The scripts in /etc/dhcp/dhclient-enter-hooks.d
are not executables, but shell scripts being sourced by dhclient-scripts
. That is why we need to create the actual script which is written in perl somewhere else and just call it from here.
Required packages
aptitude install libconfig-file-perl libnet-ip-perl
/etc/dhcp/dhclient-enter-hooks.d/router
/usr/local/bin/dhclient-router
/usr/local/bin/dhclient-router
#!/usr/bin/perl
use strict;
use Config::File;
use File::Temp;
use Net::IP;
my $config = Config::File::read_config_file('/etc/router.conf');
exit 0 unless $ENV{'interface'} = $config->{'external_nic'};
exit 0 unless $ENV{'reason'} =~ /^(BOUND|REBIND|RENEW)6?$/;
if(exists $ENV{'new_ip_address'} or exists $ENV{'new_ip6_address'}) {
my $recordtype;
my $ip;
if(exists $ENV{'new_ip_address'}) {
$recordtype = 'A';
$ip = $ENV{'new_ip_address'};
} else {
$recordtype = 'AAAA';
$ip = $ENV{'new_ip6_address'};
};
if(exists $config->{'dns_update_server'}
and exists $config->{'dns_update_key'}
and exists $config->{'dns_ttl'}
and exists $config->{'dns_zone'}
and exists $config->{'dns_external_name'}) {
open(my $nsupdate, "| nsupdate -k $config->{'dns_update_key'}");
print $nsupdate "server $config->{'dns_update_server'}\n";
print $nsupdate "zone $config->{'dns_zone'}\n";
print $nsupdate "update delete $config->{'dns_external_name'}.$config->{'dns_zone'}. IN $recordtype\n";
print $nsupdate "update add $config->{'dns_external_name'}.$config->{'dns_zone'}. $config->{'dns_ttl'} IN $recordtype $ip\n";
print $nsupdate "send\n";
print $nsupdate "quit\n";
close($nsupdate);
};
};
exit 0 unless exists $ENV{'new_ip6_prefix'};
my $tempconf = File::Temp->new;
$tempconf->unlink_on_destroy(0);
open(my $template, "/etc/radvd.conf.tmpl") or die "Unable to open /etc/radvd.conf.tmpl: $!";
my @template = <$template>;
close($template);
my @internal_nics = split(/\s+/, $config->{'internal_nics'});
my $num_internal_nets = scalar(@internal_nics);
my $prefix = Net::IP->new($ENV{'new_ip6_prefix'});
my @octets = split(/:/, $prefix->ip);
my $start_octet = hex($octets[3]);
my $last_octet = hex((split(/:/, $prefix->last_ip))[3]);
die "Not enough addresses for all internal nics" unless(($last_octet - $start_octet) >= $num_internal_nets);
if(exists $config->{'dns_update_server'}
and exists $config->{'dns_update_key'}
and exists $config->{'dns_ttl'}
and exists $config->{'dns_zone'}) {
open(my $nsupdate, "| nsupdate -k $config->{'dns_update_key'}");
print $nsupdate "server $config->{'dns_update_server'}\n";
print $nsupdate "zone $config->{'dns_zone'}\n";
print $nsupdate "update delete $config->{'dns_zone'}. IN APL\n";
my $apl_entry;
if(exists $config->{'ipv4_prefix'}) {
$apl_entry = "1:$config->{'ipv4_prefix'} 2:$ENV{'new_ip6_prefix'}";
} else {
$apl_entry = "2:$ENV{'new_ip6_prefix'}";
};
print $nsupdate "update add $config->{'dns_zone'}. $config->{'dns_ttl'} IN APL $apl_entry\n";
print $nsupdate "send\n";
print $nsupdate "quit\n";
close($nsupdate);
};
my $i = 0;
foreach my $internal_nic (@internal_nics) {
my $new_ip = Net::IP->new(sprintf("%s:%s:%s:%x::1", $octets[0], $octets[1], $octets[2], $start_octet + $i));
my $new_prefix = sprintf("%s:%s:%s:%x::/64", $octets[0], $octets[1], $octets[2], $start_octet + $i++);
my $current_ip = undef;
open(my $addr, "ip -6 addr show dev $internal_nic scope global |");
while(<$addr>) {
if(/inet6 ([0-9a-f:]+)\/64 scope global\s*$/) {
$current_ip = Net::IP->new($1);
};
};
my $ip = $new_ip->ip;
my $search = $config->{'dns_search'};
foreach my $line (@template) {
my $l = $line;
$l =~ s/__IFACE__/$internal_nic/;
$l =~ s/__PREFIX__/$new_prefix/;
$l =~ s/__IP__/$ip/;
$l =~ s/__SEARCH__/$search/;
print $tempconf $l;
};
if(defined $current_ip) {
next if $new_ip->overlaps($current_ip) == $IP_IDENTICAL; # address already set
my $ip = $current_ip->ip;
`ip -6 addr del $ip/64 dev $internal_nic`;
};
my $ip = $new_ip->ip;
`ip -6 addr add $ip/64 dev $internal_nic`;
if(exists $config->{'dns_update_server'}
and exists $config->{'dns_update_key'}
and exists $config->{'dns_zone'}
and exists $config->{'dns_ttl'}
and exists $config->{'dns_' . $internal_nic . '_name'}) {
open(my $nsupdate, "| nsupdate -k $config->{'dns_update_key'}");
print $nsupdate "server $config->{'dns_update_server'}\n";
print $nsupdate "zone $config->{'dns_zone'}\n";
my $entry = $config->{'dns_' . $internal_nic . '_name'} . '.' . $config->{'dns_zone'} . '.';
print $nsupdate "update delete $entry IN AAAA\n";
print $nsupdate "update add $entry $config->{'dns_ttl'} IN AAAA $ip\n";
print $nsupdate "send\n";
print $nsupdate "quit\n";
close($nsupdate);
};
};
my $tempconf_filename = $tempconf->filename;
close($tempconf);
unless(system("diff /etc/radvd.conf $tempconf_filename >/dev/null") == 0) {
rename($tempconf_filename, '/etc/radvd.conf');
chmod 0644, $tempconf_filename;
`/etc/init.d/radvd restart >/dev/null` ;
} else {
unlink($tempconf_filename);
};
Firewall
Especially for IPv6 there are som important firewall rules as discussed in Recommended Simple Security Capabilities in Customer Premises Equipment for Providing Residential IPv6 Internet Service. and for IPv4 you have to setup NAT. The rules here should address these issues. I have added some commented out examples for port forwarding and openings for specific ports, you will have to tailor the firewall configuration to suit your own needs. The IPv4 configuration is for running the 192.168.0.0/24 network on eth1. The iptables rules are for one external nic on eth0 and internal on eth1, you have to modify to fit your topology.
/etc/iptables
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [2:120]
# Port forward port 22 to 192.168.0.199. Also remember to rule in the FORWARD table
#-A PREROUTING -i eth0 -p tcp -m tcp --dport 22 -j DNAT --to-destination 192.168.0.199
-A POSTROUTING -o eth0 -j MASQUERADE
COMMIT
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:bad_tcp_packets - [0:0]
-A bad_tcp_packets -p tcp --tcp-flags SYN,ACK SYN,ACK -m conntrack --ctstate NEW -j REJECT --reject-with tcp-reset
-A bad_tcp_packets -p tcp ! --syn -m conntrack --ctstate NEW -j DROP
-A INPUT -p tcp -j bad_tcp_packets
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
# allow traffic from internal nic
-A INPUT -s 192.168.0.0/24 -i eth1 -j ACCEPT
# allow dhcp on external nic
-A INPUT -i eth0 -p udp -m udp --sport 67 --dport 68 -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -p icmp -j ACCEPT
# allow DNS and NTP
-A INPUT -p udp -m udp --sport 53 -j ACCEPT
-A INPUT -p udp -m udp --sport 123 -j ACCEPT
-A INPUT -m limit --limit 11/minute -j LOG --log-level info --log-prefix "IPv4-INPUT "
-A INPUT -j REJECT
-A FORWARD -p tcp -j bad_tcp_packets
# allow forwarding for internal nic
-A FORWARD -i eth1 -j ACCEPT
-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
# Port forward port 22 to 192.168.0.199
#-A FORWARD -d 192.168.0.199/32 -p tcp --dport 22 -j ACCEPT
-A FORWARD -m limit --limit 11/minute -j LOG --log-level info --log-prefix "IPv4-FORWARD "
-A FORWARD -j REJECT --reject-with icmp-host-unreachable
-A OUTPUT -p tcp -j bad_tcp_packets
COMMIT
/etc/ip6tables
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [27:2428]
:bad_tcp_packets - [0:0]
-A bad_tcp_packets -p tcp --tcp-flags SYN,ACK SYN,ACK -m conntrack --ctstate NEW -j REJECT --reject-with tcp-reset
#-A bad_tcp_packets -p tcp ! --syn -m conntrack --ctstate NEW -j LOG --log-prefix "New not syn:"
-A bad_tcp_packets -p tcp ! --syn -m conntrack --ctstate NEW -j DROP
-A INPUT -p tcp -j bad_tcp_packets
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
# allow dhcp
-A INPUT -i eth0 -p udp -m udp --dport dhcpv6-client -j ACCEPT
# allow icmp, essential for IPv6!
-A INPUT -p icmpv6 -j ACCEPT
# allow traffic from internal nic
-A INPUT -i eth1 -j ACCEPT
-A INPUT -i lo -j ACCEPT
# allow link local multicast
-A INPUT --dest ff02::/16 -j ACCEPT
# allow DNS and NTP
-A INPUT -p udp -m udp --sport 53 -j ACCEPT
-A INPUT -p udp -m udp --sport 123 -j ACCEPT
-A INPUT -m limit --limit 11/minute -j LOG --log-level info --log-prefix "IPv6-INPUT "
-A INPUT -j REJECT
-A FORWARD -p tcp -j bad_tcp_packets
# multicast, allow global multicast prefix, reject all else
-A FORWARD --source ff00::/8 -j REJECT --reject-with icmp6-adm-prohibited
-A FORWARD --dest ff0e::/16 -j ACCEPT
-A FORWARD --dest ff1e::/16 -j ACCEPT
-A FORWARD --dest ff2e::/16 -j ACCEPT
-A FORWARD --dest ff3e::/16 -j ACCEPT
-A FORWARD --dest ff4e::/16 -j ACCEPT
-A FORWARD --dest ff5e::/16 -j ACCEPT
-A FORWARD --dest ff6e::/16 -j ACCEPT
-A FORWARD --dest ff7e::/16 -j ACCEPT
-A FORWARD --dest ff00::/8 -j REJECT --reject-with icmp6-no-route
# link-local
-A FORWARD --dest fe80::/10 -j DROP
-A FORWARD --source fe80::/10 -j DROP
-A FORWARD --dest 3fa::/10 -j REJECT --reject-with icmp6-no-route
-A FORWARD --source 3fa::/10 -j REJECT --reject-with icmp6-no-route
# site local
-A FORWARD --dest 3fb::/10 -j REJECT --reject-with icmp6-no-route
-A FORWARD --source 3fb::/10 -j REJECT --reject-with icmp6-no-route
# ipv4-mapped
-A FORWARD --dest ::FFFF:0:0/96 -j REJECT --reject-with icmp6-no-route
-A FORWARD --source ::FFFF:0:0/96 -j REJECT --reject-with icmp6-no-route
# documentation prefix
-A FORWARD --dest 2001:db8::/32 -j REJECT --reject-with icmp6-no-route
-A FORWARD --source 2001:db8::/32 -j REJECT --reject-with icmp6-no-route
# orchid
-A FORWARD --dest 2001:10::/28 -j REJECT --reject-with icmp6-no-route
-A FORWARD --source 2001:10::/28 -j REJECT --reject-with icmp6-no-route
# routing header 0, deprecated
-A FORWARD -m rt --rt-type 0 -j REJECT --reject-with icmp6-adm-prohibited
# allow forwarding for internal nic
-A FORWARD -i eth1 -j ACCEPT
-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
# icmp
-A FORWARD -p icmpv6 -j ACCEPT
# ipsec
-A FORWARD -p udp --dport 500 -j ACCEPT
-A FORWARD -m ipv6header --soft --header esp -j ACCEPT
-A FORWARD -m ipv6header --soft --header auth -j ACCEPT
# Example if you want to allow ssh to your internal machines
#-A FORWARD -p tcp --dport 22 -j ACCEPT
-A FORWARD -m limit --limit 11/minute -j LOG --log-level info --log-prefix "IPv6-FORWARD "
-A FORWARD -j REJECT --reject-with icmp6-addr-unreachable
-A OUTPUT -p tcp -j bad_tcp_packets
# link-local
-A OUTPUT --dest 3fa::/10 -j REJECT --reject-with icmp6-no-route
-A OUTPUT --source 3fa::/10 -j REJECT --reject-with icmp6-no-route
# site local
-A OUTPUT --dest 3fb::/10 -j REJECT --reject-with icmp6-no-route
-A OUTPUT --source 3fb::/10 -j REJECT --reject-with icmp6-no-route
# ipv4-mapped
-A OUTPUT --dest ::FFFF:0:0/96 -j REJECT --reject-with icmp6-no-route
-A OUTPUT --source ::FFFF:0:0/96 -j REJECT --reject-with icmp6-no-route
# documentation prefix
-A OUTPUT --dest 2001:db8::/32 -j REJECT --reject-with icmp6-no-route
-A OUTPUT --source 2001:db8::/32 -j REJECT --reject-with icmp6-no-route
# orchid
-A OUTPUT --dest 2001:10::/28 -j REJECT --reject-with icmp6-no-route
-A OUTPUT --source 2001:10::/28 -j REJECT --reject-with icmp6-no-route
COMMIT
/etc/network/if-pre-up.d/iptables
#!/bin/bash
/sbin/iptables-restore < /etc/iptables
/sbin/ip6tables-restore < /etc/ip6tables
sysctl config
/etc/sysctl.conf
net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1
net.ipv4.tcp_ecn = 1
Network config
We have to manually run dhclient using post-up as there is per now no built in ifup method in Debian to run dhclient with -P
which is needed for prefix delegation.
/etc/network/interfaces
auto lo
iface lo inet loopback
auto eth0
iface eth0 inet dhcp
pre-up /etc/init.d/bind9 start
iface eth0 inet6 auto
pre-up /etc/init.d/bind9 start
post-up /sbin/sysctl net.ipv6.conf.eth0.accept_ra=2
post-up /sbin/dhclient -1 -6 -P -N -pf /run/dhclient.eth0-ipv6.pid -lf /var/lib/dhcp/dhclient.eth0-ipv6.leases eth0
post-up /usr/local/bin/update-bind-forwarders
pre-down /sbin/dhclient -v -r -pf /run/dhclient.eth0-ipv6.pid -lf /var/lib/dhcp/dhclient.eth0-ipv6.leases eth0
auto eth1
iface eth1 inet static
address 192.168.0.1
netmask 255.255.255.0
pre-up /etc/init.d/bind9 start
iface eth1 inet6 manual
Using CoDel queue algorithm
To avoid bufferbloat you can use the CoDel active queue management algorithm. This requires Linux kernel 3.5 or higher as well as iproute
20121001-1
or newer.
Installation
Download debloat.sh
from https://github.com/dtaht/deBloat and place it in /etc/network/if-up.d/debloat
. Remember to install ethtool
if it is not already installed.