From 0b520f54429c72549ddbf22a81429d297b4814c8 Mon Sep 17 00:00:00 2001 From: Tore Anderson Date: Tue, 11 Mar 2014 00:59:34 +0100 Subject: [PATCH] Initial commit (clatd v1.0) --- LICENCE | 5 + Makefile | 20 + README.pod | 327 +++++++++++++++ clatd | 792 +++++++++++++++++++++++++++++++++++ scripts/clatd.networkmanager | 24 ++ scripts/clatd.systemd | 21 + scripts/clatd.upstart | 16 + 7 files changed, 1205 insertions(+) create mode 100644 LICENCE create mode 100644 Makefile create mode 100644 README.pod create mode 100755 clatd create mode 100644 scripts/clatd.networkmanager create mode 100644 scripts/clatd.systemd create mode 100644 scripts/clatd.upstart diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..bed25d8 --- /dev/null +++ b/LICENCE @@ -0,0 +1,5 @@ +Copyright (c) 2014 Tore Anderson + + As long as you retain this notice, you may use this piece of software as + you wish. If you like it, and we happen to meet one day, you can buy me + a beer in return. If you really like it, make it an IPA. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..37d9e08 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +install: + # Install the main script to /usr/sbin + install -m0755 clatd /usr/sbin/clatd + # Install manual page if pod2man is installed + pod2man --name clatd --center "clatd - a CLAT implementation for Linux" --section 8 README.pod /usr/share/man/man8/clatd.8 && gzip -f9 /usr/share/man/man8/clatd.8 || echo "pod2man is required to generate manual page" + # Install systemd service file if applicable for this system + if test -x /usr/bin/systemctl && test -d "/etc/systemd/system"; then install -m0644 scripts/clatd.systemd /etc/systemd/system/clatd.service && systemctl daemon-reload; fi + if test -e "/etc/systemd/system/clatd.service" && test ! -e "/etc/systemd/system/multi-user.target.wants/clatd.service"; then systemctl enable clatd.service; fi + # Install upstart service file if applicable for this system + if test -x /sbin/initctl && test -d "/etc/init"; then install -m0644 scripts/clatd.upstart /etc/init/clatd.conf; fi + # Install NetworkManager dispatcher script if applicable + if test -d /etc/NetworkManager/dispatcher.d; then install -m0755 scripts/clatd.networkmanager /etc/NetworkManager/dispatcher.d/50-clatd; fi + +installdeps: + # .deb/apt-get based distros + if test -x /usr/bin/apt-get; then apt-get -y install perl-base perl-modules libnet-ip-perl libnet-dns-perl libio-socket-inet6-perl iproute iptables tayga; fi + # .rpm/YUM-based distros + if test -x /usr/bin/yum; then yum -y install perl perl-Net-IP perl-Net-DNS perl-IO-Socket-INET6 perl-File-Temp iproute iptables; fi + # to get TAYGA on .rpm/YUM-based distros, we unfortunately need to install from source + if test -x /usr/bin/yum && test ! -x /usr/sbin/tayga; then echo "TAYGA isn't packaged for YUM-based distros, will download and compile the source in 5 seconds (^C interrupts)" && sleep 5 && yum -y install gcc tar wget bzip2 && wget http://www.litech.org/tayga/tayga-0.9.2.tar.bz2 && bzcat tayga-0.9.2.tar.bz2 | tar x && cd tayga-0.9.2 && ./configure --prefix=/usr && make && make install && rm -rf ../tayga-0.9.2.tar.bz2 ../tayga-0.9.2; fi diff --git a/README.pod b/README.pod new file mode 100644 index 0000000..05bc549 --- /dev/null +++ b/README.pod @@ -0,0 +1,327 @@ +=head1 NAME + +B - a CLAT implementation for Linux + +=head1 DESCRIPTION + +B implements the CLAT component of the 464XLAT network architecture +specified in I. It allows an IPv6-only host to have IPv4 connectivity +that is translated to IPv6 before being routed to an upstream PLAT (which is +typically a Stateful NAT64 operated by the ISP) and there translated back to +IPv4 before being routed to the IPv4 internet. This is especially useful when +local applications on the host requires actual IPv4 connectivity or cannot +make use of DNS64 (for example because they use legacy AF_INET socket calls, +or if they are simply not using DNS64). + +It may also be used in combination with a stateless PLAT as defined by +I to give the otherwise IPv6-only host a public IPv4 +address with connectivity to the IPv4 internet. This may be useful in a +server environment that are using legacy IPv4-only applications as described +above. + +It relies on the software package TAYGA by Nathan Lutchansky for the actual +translation of packets between IPv4 and IPv6 (I) TAYGA may be +downloaded from its home page at L. + +=head1 SYNOPSIS + +B [options] + +=head1 OPTIONS + +=over + +=item -q + +Quiet mode; suppress normal output This is the same as setting B. +Warnings and errors are still outputted, to silence those too, repeat I<-q>. + +=item -d + +Enable debugging output. This is the same as setting B. Repeat for +even more debugging output, which is the +equivalent of setting B. + +=item -c conf-file + +Read configuration settings from B. See section B +below for more info. + +=item -h, --help + +Print a brief usage help and exit. + +=item key=value + +Set configuration B to I, overriding any setting found in the +configuration file. Refer to the section B below for more info. + +=back + +=head1 INVOCATION + +B is meant to be run under a daemonising control process such as +systemd, upstart, or similar. It is further meant to be (re)started whenever a +network interface goes up/down as this might mean a change in the PLAT +availability or which prefixes/addresses needs to be used for the CLAT to work. +It may also be run directly from the command line. It will run until killed +with SIGINT (^C) or SIGTERM, at which point it will clean up after itself and +exit gracefully. + +See the I directory in the source distribution for some examples on +how to invoke it it. + +=head1 INSTALLATION + +The following commands will quickly download and install the latest version +of B and its dependencies: + +=over + +=item git clone https://github.com/toreanderson/clatd + +=item sudo make -C clatd install installdeps + +=back + +This will install B to /usr/sbin, plus install systemd, upstart, and/or +NetworkManager scripts if your distribution appears to be using them, and +install all the dependencies. Note that TAYGA isn't available in RPM format, +so on RedHat/Fedora the installdeps target will install gcc and attempt to +compile TAYGA from source. + +=head1 CONFIGURATION + +B is designed to be able to run without any user-supplied configuration +in most cases. However, user-specified onfiguration settings may be added to +the configuration file, the path to which may be given on the command line +using the I<-c> option, or if it is not, the default location +I is used. Configuration settings may also be given directly +on the command line when starting B, which takes precedence over settings +in the configuration file. + +Settings are of the form B. A list of recogniced keys and their +possible values follow below: + +=over + +=item B (default: I<0>) + +Set this to 1 to suppress normal output from B. This is the same as +providing the command line option I<-q>. Set it to 2 to additionally +suppress warnings and errors. Note that this does not suppress debugging +output. + +=item B (default: I<0>) + +Set this to 1 to get debugging output from B, or 2 to get even more of +the stuff. These are the equivalent of providing the command line option I<-d> +the specified number of times. + +=item B (default: I) + +The name of the network device used by the CLAT. There should be no reason to +change the default, unless you plan on running multiple instances of B +simultaneously. + +=item B (default: I<192.0.0.1>) + +The IPv4 address that will be assigned to the CLAT device. Local applications +will bind to this address when communicating with external IPv4 destinations. +In a standard 464XLAT environment with a stateful NAT64 serving as the PLAT, +there should be no need to change the default, but if the PLAT is a stateless +translator (a la I-D.draft-anderson-siit-dc), you might want to set this to +the true external address used externally, so the the local applications can +correctly identify which public address they'll be using on the IPv4 internet. + +The default address is one from I. + +=item B (default: auto-generated) + +The IPv6 address of the CLAT. Traffic to/from the B will be +translated into this address. By default, B will attempt to figure out +which network device will be used for traffic towards the PLAT, see if there +is any SLAAC-configured addresses on it, and if so substitute the '0xfffe' +value in the middle of the Interface ID for '0xc1a7' to generate a new +address for the CLAT. If you're not using SLAAC you will have to set this +manually. + +=item B (default: use system resolver) + +Comma-separated list of DNS64 servers to use when discovering the PLAT prefix +using the method described in RFC 7050. By default, the system resolver is +used, but it might be useful to override this in case your ISP doesn't provide +you with a DNS64-enabled name server, and you want to test B using any of +the public DNS64/NAT64 instances on the internet. The first PLAT prefix +encountered will be used. + +=item B (default: assume in $PATH) + +Path to the B binary from the iproute2 package available at +L. Required. + +=item B (default: assume in $PATH) + +Path to the B binary from the netfilter package available at +L. Only required for adding ip6tables rules +(see the B configuration setting). + +=item B (default: assume in $PATH) + +Path to the B binary from the TAYGA package available at +L. Required. + +=item B (default: I) + +Controls whether or not B should enable IPv6 forwarding if necessary. IPv6 +forwarding is necessary for B to work correctly. It will also ensure that +the I sysctl is to '2' for all devices have it set to '1', in order +to prevent any connectivity loss as a result of enabling forwarding. + +All sysctls that are modified will be restored to their original values when +B is shutting down. + +=item B (default: see below) + +Controls whether or not B should insert ip6tables rules that permit the +forwarding of IPv6 traffic between the CLAT and PLAT devices. Such forwarding +must be permitted for B to work correctly. Any rules added will be removed +when B is shutting down. + +The default is I if the ip6tables_filter kernel module is loaded, I +if it is not. + +=item B (default: auto-detect) + +Which network device is facing the PLAT (NAT64). By default, this is +auto-detecting by performing a route table lookup towards the PLAT prefix. +This setting is used when setting up generating the CLAT IPv6 address, and +when setting up ip6tables rules and Proxy-ND entries. + +=item B (default: auto-detect) + +The IPv6 translation prefix into which the PLAT maps the IPv4 internet. See +I for a closer description. By default, this is auto-detected from +DNS64 answers using the method in I. + +=item B (default: I) + +Controls whether or not B should add a Proxy-ND entry for the CLAT IPv6 +address on the network device facing the PLAT. This is probably necessary +on Ethernet networks (otherwise the upstream IPv6 router won't know where to +send packets to the CLAT's IPv6 adderss), but likely not necessary on +point-to-point links like PPP or 3GPP mobile broadband, as in those cases +IPv6 ND isn't used. However it doesn't hurt to add Proxy-ND entries in that +case, either. + +Any entries added wil be removed when B is shutting down. + +=item B (default: use a temporary file) + +Where to write the TAYGA configuration file. By default, a temporary file will +be created (and also deleted when B is shutting down), but you may also +specify an explicit configuration file here, which will not be deleted on +shutdown. + +=item B (default: I<192.0.0.2>) + +The IPv4 address assigned to the TAYGA process. This is used for emitting +ICMPv4 errors back to the host (i.e., it will show up as the first hop when +tracerouting to IPv4 destinations), and you may also ping it to verify that +the TAYGA process is still alive and well. + +The default address is one from I. + +=item B (default: I) + +Whether or not to check if the system has IPv4 connectivity before starting +the CLAT. If it does, then B will simply exit without doing anything. +This is meant so that you can always enable B to the system startup +scripts or network-up event scripts (such as NetworkManager's dispatcher +scripts), but not have B interfering with native IPv4 connectivity when +this is present. + +If you want to always start the CLAT whenever possible, even though the +system has IPv4 connectivity, disable this setting. You may instead use the +B and B settings to prevent +B from interfering with native IPv4 connectivity. + +=item B (default: I<10>) + +When performing an IPv4 connectivity check, wait this number of seconds +before actually doing anything. This is to avoid a race condition where for +example IPv6 SLAAC finshes and triggers a network-up event script to start +B, while IPv4 DHCPv4 is still running in the background. This is at +least a likely scenario when using NetworkManager, as it will start the +dispatcher scripts as soon as either IPv4 or IPv6 has completed, and +IPv6 SLAAC is typically faster than IPv4 DHCPv4. + +Set it to 0 to perform the check immediately. + +=item B (default: I) + +Whether or not to add an IPv4 default route pointing to the CLAT. In a +typical 464XLAT environment, you want this. However when using B in +an environment where native IPv4 connectivity is also present, you might want +to disable this and instead control manually which IPv4 destinations is +reached through the CLAT and which are not. + +=item B (default: I<2048>) + +The metric of the IPv4 default route pointing to the CLAT. The default is +chosen because it is higher than that of a native IPv4 default route added by +NetworkManager, which makes it so that the native IPv4 connectivity is +preferred if present. + +=item B (default: I<1260>) + +The MTU of the default route pointing to the CLAT. The default is the default +IPv6 MTU used by TAYGA (1280, which in turn comes from I) minus 20 to +compensate for the difference in header size between IPv4 and IPv6. This +prevents outbound packets from having to be fragmented by TAYGA, and also +makes local applications advertise a TCP MSS to their remote peers that +prevent them from sending packets beck to us that would require fragmentation. + +If you know that the IPv6 Path MTU between the host and the PLAT is larger +than 1280, you may increase this, but then you should also recompile TAYGA +with a larger B setting in I. + +=back + +=head1 LIMITATIONS + +B will not be able to acquire an IPv6 address for the CLAT if SLAAC +isn't used. I suggests DHCPv6 IA_PD should be attempted in this +case, but this isn't currently implemented. + +B will not attempt to perform Duplicate Address Detection for the IPv6 +address it generates. This is a violation of I. + +B will not attempt to perform a connectivity check to a discovered PLAT +prefix before setting up the CLAT, as I suggest it should. + +=head1 BUGS + +If you are experiencing any bugs or have any feature requests, head over to +L and submit a new issue (if +someone else hasn't already done so). Please make sure to include logs with +full debugging output (using I<-d -d> on the command line or B in the +configuration file) when reporting a bug. + +=head1 LICENCE + +Copyright (c) 2014 Tore Anderson + +As long as you retain this notice, you may use this piece of software as +you wish. If you like it, and we happen to meet one day, you can buy me +a beer in return. If you really like it, make it an IPA. + +=head1 SEE ALSO + +ip(8), ip6tables(8), tayga(8), tayga.conf(5) + +RFC 6052, RFC 6145, RFC 6146, RFC 6877, RFC 7050 + +I-D.anderson-siit-dc, I-D.byrne-v6ops-clatip + +=cut diff --git a/clatd b/clatd new file mode 100755 index 0000000..0a4b762 --- /dev/null +++ b/clatd @@ -0,0 +1,792 @@ +#! /usr/bin/perl -w +# +# Copyright (c) 2014 Tore Anderson +# +# As long as you retain this notice, you may use this piece of software as +# you wish. If you like it, and we happen to meet one day, you can buy me +# a beer in return. If you really like it, make it an IPA. +# +# See the file 'README.pod' in the source distribution or the manual page +# clatd(8) for more information. +# +use strict; +use Net::IP; + +my $VERSION = "1.0"; + +# +# Populate the global config hash with the default values +# +my %CFG; +$CFG{"quiet"} = 0; # suppress normal output +$CFG{"debug"} = 0; # debugging output level +$CFG{"clat-dev"} = "clat"; # TUN interface name to use +$CFG{"clat-v4-addr"} = "192.0.0.1"; # from I-D.draft-byrne-v6ops-clatip +$CFG{"clat-v6-addr"} = undef; # derive from existing SLAAC addr +$CFG{"dns64-servers"} = undef; # use system resolver by default +$CFG{"cmd-ip"} = "ip"; # assume in $PATH +$CFG{"cmd-ip6tables"} = "ip6tables"; # assume in $PATH +$CFG{"cmd-tayga"} = "tayga"; # assume in $PATH +$CFG{"forwarding-enable"} = 1; # enable ipv6 forwarding? +$CFG{"ip6tables-enable"} = undef; # allow clat<->plat traffic? +$CFG{"plat-dev"} = undef; # PLAT-facing device, default detect +$CFG{"plat-prefix"} = undef; # detect using DNS64 by default +$CFG{"proxynd-enable"} = 1; # add proxy-nd entry for clat? +$CFG{"tayga-conffile"} = undef; # make a temporary one by default +$CFG{"tayga-v4-addr"} = "192.0.0.2"; # from I-D.draft-byrne-v6ops-clatip +$CFG{"v4-conncheck-enable"} = 1; # exit if there's already a defroute +$CFG{"v4-conncheck-delay"} = 10; # seconds before checking for v4 conn. +$CFG{"v4-defaultroute-enable"} = 1; # add a v4 defaultroute via the CLAT? +$CFG{"v4-defaultroute-metric"} = 2048; # metric for the IPv4 defaultroute +$CFG{"v4-defaultroute-mtu"} = 1260; # MTU for the IPv4 defaultroute + + +# +# helper functions for various modes of output and error handling +# +sub p { + print join("", @_), "\n" unless($CFG{"quiet"} >= 1); +} +sub d { + print join("", @_), "\n" if($CFG{"debug"} >= 1); +} +sub d2 { + print join("", @_), "\n" if($CFG{"debug"} >= 2); +} +sub w { + print " ", join("", @_), "\n" unless($CFG{"quiet"} >= 2); +} +sub err { + print " ", join("", @_), "\n" unless($CFG{"quiet"} >= 2); + cleanup_and_exit(1); +} + + +# +# Runs a command. First argument is what subroutine to call to a message if +# the command doesn't exit successfully, second is the command itself, and +# any more is the command line arguments. +# +sub cmd { + my $msgsub = shift; + my $command = shift; + my @cmdline = @_; + + d("cmd($command @cmdline)"); + + if(system($command, @cmdline)) { + if($? == -1) { + &{$msgsub}("cmd($command @cmdline) failed to execute"); + } elsif($? & 127) { + &{$msgsub}("cmd($command @cmdline) died with signal ", ($? & 127)); + } else { + &{$msgsub}("cmd($command @cmdline) returned ", ($? >> 127)); + } + } + return $?; +} + + +# +# Reads in key=value pairs from a configuration file, overwriting the default +# setting in the %CFG hash. The key must exist, or we +# +sub readconf { + d("readconf('@_')"); + open(my $fd, "@_") or err("readconf('@_') failed: $!"); + while(<$fd>) { + chomp; + next if m,^\s*(;|#|//|$),; # strip out comments and empty lines + if(m|^\s*([\w-]+)\s*=\s*(.*)\s*$|) { + if(!exists($CFG{$1})) { + w("Unknown key '$1' defined in config file ignored"); + } else { + $CFG{$1} = $2; + } + } else { + w("Unknown line '$_' in config file ignored"); + } + } + close($fd) or err($!); +} + + +# +# gets a boolean value from the config hash - fails if unset or syntactically +# invalid +# +sub cfgbool { + my ($key) = @_; + d2("cfgbool($key)"); + if(!exists($CFG{$key})) { + err("key '$key' doesn't exist in config hash"); + } + my $val = lc($CFG{$key}); + return 1 if($val eq "1" or $val eq "true" or $val eq "on" or $val eq "yes"); + return 0 if($val eq "0" or $val eq "false" or $val eq "off" or $val eq "no"); + err("$key: boolean value (1/0/true/false/on/off/yes/no) expected"); +} + + +# +# gets an integer value from the config hash - fails if unset or syntactically +# invalid +# +sub cfgint { + my ($key) = @_; + d2("cfgstr($key)"); + if(!exists($CFG{$key})) { + err("key '$key' doesn't exist in config hash"); + } + my $val = $CFG{$key}; + $val =~ m|^\d+$| or err("$key=$val - integer expected"); + return $val; +} + + +# +# gets a scalar value from the config hash - fails if unset +# +sub cfg { + my ($key) = @_; + d2("cfgstr($key)"); + if(!exists($CFG{$key})) { + err("key '$key' doesn't exist in config hash"); + } + return $CFG{$key}; +} + + +# +# read sysctl in the first argument, or set it to value in second argument +# if provided +# +sub sysctl { + my ($sysctl, $new_value) = @_; + + $sysctl =~ s|^/proc/sys/||; + + if(defined($new_value)) { + d("Setting sysctl /proc/sys/$sysctl=$new_value"); + my $fd; + open($fd, ">/proc/sys/$sysctl"); + if(!defined($fd)) { + w("Failed to open /proc/sys/$sysctl for writing: $!"); + return; + } + print $fd "$new_value\n"; + if(!close($fd)) { + w("Failed to close /proc/sys/$sysctl after writing: $!"); + return; + } + return $new_value; + } else { + d("Reading sysctl /proc/sys/$sysctl"); + my $fd; + open($fd, "/proc/sys/$sysctl"); + if(!defined($fd)) { + w("Failed to open /proc/sys/$sysctl for reading: $!"); + return; + } + my $value = <$fd>; + chomp($value); + if(!close($fd)) { + w("Failed to close /proc/sys/$sysctl after reading: $!"); + } + d("/proc/sys/$sysctl is set to '$value'"); + return $value; + } +} + + +# +# Look for either of the WKAs for ipv4only.arpa (192.0.0.170 and .171) in an +# IPv6 address at all of the locations RFC 6052 says it can occur. If it's +# present at any of those locations (but no more than once), return the +# inferred translation prefix. +# +sub find_rfc7050_wka { + my $AAAA = shift; + d("check_wka(): Testing to see if $AAAA was DNS64-synthesised"); + my $ip = Net::IP->new($AAAA, 6); + if(!$ip) { + w("Net::IP->new($AAAA, 6) failed: ", Net::IP::Error()); + return; + } + + my %rfc6052table; + $rfc6052table{"32"}{"mask"} = "0:0:ffff:ffff::"; + $rfc6052table{"32"}{"wkas"} = [qw(0:0:c000:aa:: 0:0:c000:ab::)]; + $rfc6052table{"40"}{"mask"} = "0:0:ff:ffff:ff::"; + $rfc6052table{"40"}{"wkas"} = [qw(0:0:c0:0:aa:: 0:0:c0:0:ab::)]; + $rfc6052table{"48"}{"mask"} = "::ffff:ff:ff00:0:0"; + $rfc6052table{"48"}{"wkas"} = [qw(::c000:0:aa00:0:0 ::c000:0:ab00:0:0)]; + $rfc6052table{"56"}{"mask"} = "::ff:ff:ffff:0:0"; + $rfc6052table{"56"}{"wkas"} = [qw(::c0:0:aa:0:0 ::c0:0:ab:0:0)]; + $rfc6052table{"64"}{"mask"} = "::ff:ffff:ff00:0"; + $rfc6052table{"64"}{"wkas"} = [qw(::c0:0:aa00:0 ::c0:0:ab00:0)]; + $rfc6052table{"96"}{"mask"} = "::ffff:ffff"; + $rfc6052table{"96"}{"wkas"} = [qw(::c000:aa ::c000:ab)]; + + my $discovered_pfx_len; + + for my $len (keys(%rfc6052table)) { + d2("Looking for Well-Known Addresses at prefix length /$len"); + my $maskedip = $ip->intip(); + my $mask = Net::IP->new($rfc6052table{"$len"}{"mask"}, 6); + if(!$mask) { + w('Net::IP->new(', $rfc6052table{"$len"}{"mask"}, ', 6) failed: ', + Net::IP::Error()); + return; + } + + $maskedip &= $mask->intip(); + + for my $wka (@{$rfc6052table{"$len"}{"wkas"}}) { + d2("Looking for WKA $wka"); + my $wkaint = Net::IP->new($wka, 6); + if(!$wkaint) { + w("Net::IP->new($wka, 6) failed: ", Net::IP::Error()); + next; + } + + if($maskedip == $wkaint->intip) { + if($discovered_pfx_len) { + w("Found WKA at two locations in ", $ip->sort, + "(/$discovered_pfx_len and /$len) - ignoring"); + return; + } + d2("Found it!"); + $discovered_pfx_len = $len; + } else { + d2("Didn't find it"); + } + } + } + + if(!$discovered_pfx_len) { + d2("Did not locate any WKAs in ", $ip->short); + return; + } + + # Yay, we have found a prefix! Zero the host bits manually, as Net::IP-new() + # unfortunately doesn't accept an address with a prefix length. That would + # have made the rest of this function so much easier... + $ip = $ip->intip; + $ip >>= (128-$discovered_pfx_len); + $ip <<= (128-$discovered_pfx_len); + + # Now convert that bigint back to an IPv6 address. Net::IP doesn't have + # a function to convert directly from a bigint to an IPv6 address (or + # to create a new instance directly from a bigint), so we'll have to take + # a detour via a binary string... + my $binip = Net::IP::ip_inttobin($ip, 6); + unless($binip) { + w("Failed to convert integer $ip to a binary string"); + return; + } + unless($ip = Net::IP::ip_bintoip($binip, 6)) { + w("Failed to convert binary string $binip to an IPv6 address"); + return; + } + + # Now make sure we have a valid prefix, and return it in pretty (compact) + # format + $ip = Net::IP->new("$ip/$discovered_pfx_len", 6); + if(!$ip) { + w("Net::IP->new($ip, 6) failed: ", Net::IP::Error()); + return; + } + + d("Inferred PLAT prefix ", $ip->short(), "/", $ip->prefixlen(), + " from AAAA record $AAAA"); + + return $ip->short() . "/" . $ip->prefixlen(); +} + + +# +# This function attempts to implement RFC 7050: Discovery of the IPv6 Prefix +# Used for IPv6 Address Synthesis. It tries to infer a PLAT prefix by looking +# up to see if the well-known hostname 'ipv4only.arpa' resolves to an IPv6 +# address, if so there is a high chance of DNS64 being used. +# +sub get_plat_prefix { + p("Performing DNS64-based PLAT prefix discovery (cf. RFC 7050)"); + + require IO::Socket::INET6; # needed by Net::DNS for querying IPv6 servers + require Net::DNS; + + my @dns64_servers = split(",", cfg("dns64-servers") || ""); + my @prefixes; + + while (1) { + my $dns64 = shift(@dns64_servers); + my $res; + if($dns64) { + d("Looking up 'ipv4only.arpa' using DNS64 server $dns64"); + $res = Net::DNS::Resolver->new(nameservers => [$dns64]); + } else { + d("Looking up 'ipv4only.arpa' using system resolver"); + $res = Net::DNS::Resolver->new(); + } + $res->dnssec(0); # RFC 7050 section 3 + my $pkt = $res->query('ipv4only.arpa', 'AAAA'); + if(!$pkt) { + d("No AAAA records was returned for 'ipv4only.arpa'"); + next; + } + for my $rr ($pkt->answer) { + if($rr->type ne "AAAA") { + w("Got an non-AAAA RR? That's unexpected... Type=", $rr->type); + next; + } + my $prefix = find_rfc7050_wka($rr->address); + if(grep { $_ eq "$prefix" } @prefixes) { + # we've seen this prefix already, ignore it (in most cases this will + # happen at least once, since ipv4only.arpa has two A records) + } else { + push(@prefixes, $prefix); + } + } + } continue { last unless @dns64_servers }; + + if(@prefixes > 1) { + # Cool! More than one prefix! Here we might at some point implement a + # connectivity check which tests that the prefixes actually work, and + # skips to the next one if so... + w("Multiple PLAT prefixes discovered (@prefixes), using the first seen"); + } + if(@prefixes) { + return $prefixes[0]; + } else { + p("No PLAT prefix could be discovered. Your ISP probably doesn't provide", + " NAT64/DNS64 PLAT service. Exiting."); + cleanup_and_exit(0); + } +} + + +# +# This function figures out which network interface on the system faces the +# PLAT/NAT64. We need this when generating an IPv6 address for the CLAT, when +# installing Proxy-ND entries, and when setting up ip6tables rules. +# +sub get_plat_dev { + d("get_plat_dev(): finding which network dev faces the PLAT"); + my $plat_dev; + my $plat_prefix = cfg("plat-prefix"); + if(!$plat_prefix) { + err("get_plat_dev(): No PLAT prefix to work with"); + } + open(my $fd, '-|', cfg("cmd-ip"), qw(-6 route get), $plat_prefix) + or err("get_plat_dev(): 'ip -6 route get $plat_prefix' failed to execute"); + while(<$fd>) { + if(/ dev (\S+) /) { + d("get_plat_dev(): Found PLAT-facing device: $1"); + $plat_dev = $1; + } + } + close($fd) or err("get_plat_dev(): 'ip -6 route get $plat_prefix' failed"); + return $plat_dev; +} + + +# +# Determines if an address is contructed using the Modified EUI-64 algorithm, +# by extension that it was configured using SLAAC (in which case we're at +# liberty to grab another address in that same /64 for the CLAT). +# +# This isn't a 100% foolproof check, as it is certainly possible to configure +# such an address statically, or to hand it out using DHCPv6 IA_NA, but as +# we can't easliy know with 100% certainty that SLAAC is being used, it'll +# have to do. The function checks three things which are known to be true for +# IPv6 addresses with Interface IDs based on Modified EUI-64: +# 1) bits 24 through 38 in the Interface ID are 1 +# 2) bit 39 in the Interface ID is 0 +# Return true if all of the above is the case, false otherwise. +# +sub is_modified_eui64 { + my $ip = shift; + $ip = Net::IP->new($ip) or return; + $ip = $ip->intip(); + + # Check 1) - return false if check fails + my $mask = Net::IP->new("::ff:fe00:0"); + $mask = $mask->intip(); + return unless ($ip & $mask) == $mask; + + # Check 2) and return + $mask = Net::IP->new("::100:0"); + $mask = $mask->intip(); + return ($ip & $mask) != $mask; +} + + +# +# This function considers any globally scoped /64 address on the PLAT-facing +# device, checks to see if it is base on Modified EUI-64, and generates a +# new address for the CLAT by substituting the "0xfffe" bits in the middle +# of the Interface ID with 0xc1a7 ("clat"). This keeps the last 24 bits +# unchanged, which has the added bonus of not requiring the host to join +# another Solicited-Node multicast group. +# +sub get_clat_v6_addr { + my $plat_dev = cfg("plat-dev"); + if(!$plat_dev) { + err("get_clat_v6_addr(): No PLAT device to work with"); + } + p("Attempting to derive a CLAT IPv6 address from a EUI-64 address on ", + "'$plat_dev'"); + open(my $fd, '-|', cfg("cmd-ip"), qw(-6 address list scope global dev), + $plat_dev) + or err("'ip -6 address list scope global dev $plat_dev' failed to execute"); + while(<$fd>) { + if(m| inet6 (\S+)/64 scope global |) { + my $candidate = $1; + next unless(is_modified_eui64($candidate)); + d2("Saw EUI-64 based address: $candidate"); + my $ip = Net::IP->new($candidate, 6) or next; + $ip = $ip->intip(); + + # First clear the middle 0xfffe bits of the interface ID + my $mask = Net::IP->new("ffff:ffff:ffff:ffff:ffff:ff00:00ff:ffff"); + $mask = $mask->intip(); + $ip &= $mask; + + # Next set them to the value 0xc1a7 and return + $mask = Net::IP->new("::c1:a700:0", 6) or next; + $mask = $mask->intip(); + $ip |= $mask; + + $ip = Net::IP->new(Net::IP::ip_bintoip(Net::IP::ip_inttobin($ip, 6), 6)); + return $ip->short() if $ip; + } + } + close($fd) + or err("'ip -6 address list scope global dev $plat_dev' failed"); + err("Failed to generate a CLAT IPv6 address (try setting 'clat-v6-addr')"); +} + +# +# This subroutine is called when we are exiting, for whatever reason. It +# tries to clean up any temporary changes we've made first. The variables +# below gets set as we go along, so that the cleanup subroutine can restore +# stuff if necessary. +# +my $cleanup_remove_clat_dev; # true if having created it +my $cleanup_delete_taygaconf; # true if having made a temp confile +my $cleanup_zero_forwarding_sysctl; # zero forwarding sysctl if set +my @cleanup_accept_ra_sysctls; # accept_ra sysctls to be reset to '1' +my $cleanup_zero_proxynd_sysctl; # zero proxy_ndp sysctl if set +my $cleanup_remove_proxynd_entry, # true if having added proxynd entry +my $cleanup_remove_ip6tables_rules; # true if having added ip6tables rules + +sub cleanup_and_exit { + my $exitcode = shift; + + if(defined($cleanup_remove_clat_dev)) { + d("Cleanup: Removing CLAT device"); + cmd(\&w, cfg("cmd-tayga"), "--config", cfg("tayga-conffile"), "--rmtun"); + } + if(defined($cleanup_delete_taygaconf)) { + d("Cleanup: Deleting TAYGA config file '", cfg("tayga-conffile"), "'"); + unlink(cfg("tayga-conffile")) + or w("unlink('", cfg("tayga-conffile"), "') failed"); + } + if(defined($cleanup_zero_forwarding_sysctl)) { + d("Cleanup: Resetting forwarding sysctl to 0"); + sysctl("net/ipv6/conf/all/forwarding", 0); + } + for my $sysctl (@cleanup_accept_ra_sysctls) { + d("Cleanup: Resetting $sysctl to 1"); + sysctl($sysctl, 1); + } + if(defined($cleanup_zero_proxynd_sysctl)) { + d("Cleanup: Resetting proxy_ndp sysctl to 0"); + sysctl("net/ipv6/conf/" . cfg("plat-dev") . "/proxy_ndp", 0); + } + if(defined($cleanup_remove_proxynd_entry)) { + d("Cleanup: Removing Proxy-ND entry for ", cfg("clat-v6-addr"), "on ", + cfg("plat-dev")); + cmd(\&w, cfg("cmd-ip"), qw(-6 neighbour delete proxy), cfg("clat-v6-addr"), + "dev", cfg("plat-dev")); + } + if(defined($cleanup_remove_ip6tables_rules)) { + d("Cleanup: Removing ip6tables rules allowing traffic between the CLAT ", + "and PLAT devices"); + cmd(\&w, cfg("cmd-ip6tables"), qw(-D FORWARD -i), cfg("clat-dev"), + "-o", cfg("plat-dev"), qw(-j ACCEPT)); + cmd(\&w, cfg("cmd-ip6tables"), qw(-D FORWARD -i), cfg("plat-dev"), + "-o", cfg("clat-dev"), qw(-j ACCEPT)); + } + + exit($exitcode); +} + + +# +# Ok, we're done defining helper functions, and are ready to start doing some +# real work here. First parse option arguments from command line, config +# overrides we do in a second pass below. We do it in two passes to ensure we +# have read in any config from the config file before possibly overriding with +# config supplied on the command line +# +# +for (my $i = 0; $i < @ARGV;) { + if($ARGV[$i] eq "-q") { + $CFG{"quiet"}++; + splice(@ARGV, $i, 1); + next; + } elsif($ARGV[$i] eq "-d") { + $CFG{"debug"}++; + splice(@ARGV, $i, 1); + next; + } elsif($ARGV[$i] eq "-c") { + if(!defined($ARGV[$i+1])) { + err("Command line option '-c' given without an argument"); + } + if(!defined(&readconf)) { + err("Command line option '-c' given more than once"); + } + readconf($ARGV[$i+1]); + undef(&readconf); + splice(@ARGV, $i, 2); + next; + } elsif($ARGV[$i] =~ /^(-h|--help)$/) { + print "clatd v$VERSION - a 464XLAT (RFC 6877) CLAT implementation for ", + "Linux\n"; + print "\n"; + print " Usage: clatd [-q] [-d [-d]] [-c config-file] ", + "[conf-key=val ...]\n"; + print " Author: Tore Anderson \n"; + print " Homepage: https://github.com/toreanderson/clatd\n"; + print "\n"; + print "For more documentation and information, see 'man 8 clatd'.\n"; + exit 0; + } elsif($ARGV[$i] =~ /^-/) { + err("Unrecognised command line option '$ARGV[$i]'"); + } + $i++; +} + + +# +# Read in config from default location if we haven't already due to +# '-c "somefile"' having been supplied on command line (if so, &readconf +# will have been undefined. However if it doesn't exit, that's OK - we'll +# just proceed with defaults + any command line overrides +# +if(defined(&readconf) && -e "/etc/clatd.conf") { + readconf("/etc/clatd.conf"); +} + + +# +# Finally, deal with config settings from command line. This is done last so +# that the command line takes precedence over all other sources of config +# +for (@ARGV) { + if(m|^([\w-]+)=(.*)$|) { + if(!exists($CFG{$1})) { + err("Unknown config key '$1' given on command line"); + } + $CFG{$1} = $2; + } else { + err("Unrecognised command line argument '$_'"); + } +} +d("Configuration successfully read, dumping it:"); +for my $key (sort(keys(%CFG))) { + d(" $key=", defined($CFG{$key}) ? $CFG{$key} : ""); +} + +p("Starting clatd v$VERSION by Tore Anderson "); + +# +# Step 1: Fill in any essential blanks in the configuration by auto-detecting +# any missing values. +$CFG{"plat-prefix"} ||= get_plat_prefix(); +if(!$CFG{"plat-prefix"}) { + w("No PLAT prefix was discovered or specified; 464XLAT cannot work."); + exit 0; +} else { + # Do some basic sanity checking on the PLAT prefix + my $ip = Net::IP->new($CFG{"plat-prefix"}, 6); + if(!$ip) { + d2("Net::IP::Error()=" . Net::IP::Error()) if(Net::IP::Error()); + err("PLAT prefix $CFG{'plat-prefix'} is not a valid IPv6 prefix"); + } + if($ip->prefixlen() != 96 and + $ip->prefixlen() != 64 and + $ip->prefixlen() != 56 and + $ip->prefixlen() != 48 and + $ip->prefixlen() != 32) { + err("PLAT prefix $CFG{'plat-prefix'} has an invalid prefix length ", + "(see RFC 6052 section 2.2)"); + } + p("Using PLAT (NAT64) prefix: $CFG{'plat-prefix'}"); +} +$CFG{"plat-dev"} ||= get_plat_dev(); +p("Device facing the PLAT: ", $CFG{"plat-dev"}); +$CFG{"clat-v6-addr"} ||= get_clat_v6_addr(); +p("Using CLAT IPv4 address: ", $CFG{"clat-v4-addr"}); +p("Using CLAT IPv6 address: ", $CFG{"clat-v6-addr"}); +if(!defined($CFG{"ip6tables-enable"})) { + $CFG{"ip6tables-enable"} = -e "/sys/module/ip6table_filter" ? 1 : 0; +} + +# +# Step 1: Detect if there is an IPv4 default route on the system from before. +# If so we have no need for 464XLAT, and we can just exit straight away +# +if(cfgbool("v4-conncheck-enable")) { + my $delay = cfgint("v4-conncheck-delay"); + p("Checking if this system already has IPv4 connectivity ", + $delay ? "in $delay sec(s)" : "now"); + sleep($delay); + open(my $fd, '-|', cfg("cmd-ip"), qw(-4 route list default)) + or err("'", cfg("cmd-ip"), " -4 route list default' failed to execute"); + while(<$fd>) { + if(/^default /) { + p("This system already has IPv4 connectivity; no need for a CLAT."); + exit_and_cleanup(0); + } + } + close($fd) or err("cmd(ip -4 route list default) failed"); +} else { + d("Skipping IPv4 connectivity check at user request"); +} + + + +# +# Write out the TAYGA config file, either to the user-specified location, +# or to a temporary file (which we'll delete later) +# +my $tayga_conffile = cfg("tayga-conffile"); +my $tayga_conffile_fh; +if(!$tayga_conffile) { + require File::Temp; + ($tayga_conffile_fh, $tayga_conffile) = File::Temp::tempfile(); + d2("Using temporary conffile for TAYGA: $tayga_conffile"); + $CFG{"tayga-conffile"} = $tayga_conffile; + $cleanup_delete_taygaconf = 1; +} else { + open($tayga_conffile_fh, ">$tayga_conffile") or + err("Could not open TAYGA config file '$tayga_conffile' for writing"); +} + +print $tayga_conffile_fh "# Ephemeral TAYGA config file written by $0\n"; +print $tayga_conffile_fh "# This file may be safely deleted at any time.\n"; +print $tayga_conffile_fh "tun-device ", cfg("clat-dev"), "\n"; +print $tayga_conffile_fh "prefix ", cfg("plat-prefix"), "\n"; +print $tayga_conffile_fh "ipv4-addr ", cfg("tayga-v4-addr"), "\n"; +print $tayga_conffile_fh "map ", cfg("clat-v4-addr"), " ", + cfg("clat-v6-addr"),"\n"; + +close($tayga_conffile_fh) or err("close($tayga_conffile_fh: $!"); + +# +# Enable IPv6 forwarding if necessary +# +if(cfgbool("forwarding-enable")) { + if(sysctl("net/ipv6/conf/all/forwarding") == 0) { + p("Enabling IPv6 forwarding"); + for my $ctl (glob("/proc/sys/net/ipv6/conf/*/accept_ra")) { + + # Don't touch the ctl for the "all" interface, as that will probably + # change interfaces that have accept_ra set to 0 also. + next if($ctl eq "/proc/sys/net/ipv6/conf/all/accept_ra"); + + if(sysctl($ctl) == 1) { + d("Changing $ctl from 1 to 2 to prevent connectivity loss after ", + "enabling IPv6 forwarding"); + sysctl($ctl, 2); + push(@cleanup_accept_ra_sysctls, $ctl); + } + } + sysctl("net/ipv6/conf/all/forwarding", 1); + $cleanup_zero_forwarding_sysctl = 0; + } +} + +# +# Add ip6tables rules permitting traffic between the PLAT and the CLAT +# +if(cfgbool("ip6tables-enable")) { + p("Adding ip6tables rules allowing traffic between the CLAT ", + "and PLAT devices"); + cmd(\&w, cfg("cmd-ip6tables"), qw(-I FORWARD -i), cfg("clat-dev"), + "-o", cfg("plat-dev"), qw(-j ACCEPT)); + cmd(\&w, cfg("cmd-ip6tables"), qw(-I FORWARD -i), cfg("plat-dev"), + "-o", cfg("clat-dev"), qw(-j ACCEPT)); + $cleanup_remove_ip6tables_rules = 1; +} + +# +# Enable ND proxy for the CLAT's IPv6 address on the interface facing the PLAT +# +if(cfgbool("proxynd-enable")) { + my $plat_dev = cfg("plat-dev"); + my $clat_v6_addr = cfg("clat-v6-addr"); + p("Enabling Proxy-ND for $clat_v6_addr on $plat_dev"); + if(sysctl("net/ipv6/conf/$plat_dev/proxy_ndp") == 0) { + sysctl("net/ipv6/conf/$plat_dev/proxy_ndp", 1); + $cleanup_zero_proxynd_sysctl = 1; + d("Enabled Proxy-ND sysctl for $plat_dev"); + } + cmd(\&w, cfg("cmd-ip"), qw(-6 neighbour add proxy), cfg("clat-v6-addr"), + "dev", cfg("plat-dev")); + + $cleanup_remove_proxynd_entry = 1; +} + +# +# Create the CLAT tun interface, add the IPv4 address to it as well as the +# route to the corresponding IPv6 address, and possibly an IPv4 default route +# +p("Creating and configuring up CLAT device '", cfg("clat-dev"), "'"); +cmd(\&err, cfg("cmd-tayga"), "--config", cfg("tayga-conffile"), "--mktun", + cfgint("debug") ? "-d" : ""); +$cleanup_remove_clat_dev = 1; +cmd(\&err, cfg("cmd-ip"), qw(link set up dev), cfg("clat-dev")); +cmd(\&err, cfg("cmd-ip"), qw(-4 address add), cfg("clat-v4-addr"), + "dev", cfg("clat-dev")); +cmd(\&err, cfg("cmd-ip"), qw(-6 route add), cfg("clat-v6-addr"), + "dev", cfg("clat-dev")); +if(cfgbool("v4-defaultroute-enable")) { + my @cmdline = (qw(-4 route add default dev), cfg("clat-dev")); + if(cfgint("v4-defaultroute-metric")) { + push(@cmdline, ("metric", cfgint("v4-defaultroute-metric"))) + } + if(cfgint("v4-defaultroute-mtu")) { + push(@cmdline, ("mtu", cfgint("v4-defaultroute-mtu"))) + } + p("Adding IPv4 default route via the CLAT"); + cmd(\&err, cfg("cmd-ip"), @cmdline); +} + +# +# All preparation done! We can now start TAYGA, which will handle the actual +# translation of IP packets. +# +p("Starting up TAYGA, using config file '$tayga_conffile'"); + +# We don't want systemd etc. to actually kill this script when stopping the +# service, just TAYGA (so that we can get around to cleaning up after +# ourselves) +$SIG{'INT'} = 'IGNORE'; +$SIG{'TERM'} = 'IGNORE'; + +cmd(\&err, cfg("cmd-tayga"), "--config", cfg("tayga-conffile"), "--nodetach", + cfgint("debug") ? "-d" : ""); +p("TAYGA terminated, cleaning up and exiting"); + +$SIG{'INT'} = 'DEFAULT'; +$SIG{'TERM'} = 'DEFAULT'; + +# +# TAYGA exited, probably because we're shutting down. Cleanup and exit. +# +cleanup_and_exit(0); diff --git a/scripts/clatd.networkmanager b/scripts/clatd.networkmanager new file mode 100644 index 0000000..50f39ef --- /dev/null +++ b/scripts/clatd.networkmanager @@ -0,0 +1,24 @@ +#!/bin/sh +# +# clatd dispatcher script for NetworkManager +# +# Install it to: /etc/NetworkManager/dispatcher.d/50-clatd +# +# Written by Tore Anderson +# + +# We simply restart clatd in all situations, as no matter if an interface +# goes up or down, it may mean that the PLAT devices changes, it may mean +# native IPv4 appearing or disappearing, or it may mean that DNS64 became +# available or unavailable...it's far easier to simply restart always and +# start from scratch than to figure out if a restart is truly necessary + +# systemd-based distros +if test -x /usr/bin/systemctl; then + /usr/bin/systemctl restart clatd.service +fi + +# upstart-based distros +if test -x /sbin/initctl; then + /sbin/initctl restart clatd +fi diff --git a/scripts/clatd.systemd b/scripts/clatd.systemd new file mode 100644 index 0000000..241edce --- /dev/null +++ b/scripts/clatd.systemd @@ -0,0 +1,21 @@ +# +# clatd service file for systemd +# +# Install it to: /etc/systemd/system/clatd.service +# Enable it with: systemctl enable clatd.service +# Start it with: systemctl start clatd.service +# +# Written by Tore Anderson +# + +[Unit] +Description=464XLAT CLAT daemon +Documentation=man:clatd(8) +After=network-online.target + +[Service] +Type=simple +ExecStart=/usr/sbin/clatd + +[Install] +WantedBy=multi-user.target diff --git a/scripts/clatd.upstart b/scripts/clatd.upstart new file mode 100644 index 0000000..761875f --- /dev/null +++ b/scripts/clatd.upstart @@ -0,0 +1,16 @@ +# +# clatd service file for upstart +# +# Install it to: /etc/init/clatd.conf +# Start it with: initctl start clatd +# +# Written by Tore Anderson +# + +description "464XLAT CLAT daemon" +author "Tore Anderson " + +start on net-device-up +stop on runlevel [!2345] + +exec /usr/sbin/clatd