Initial commit (clatd v1.0)

This commit is contained in:
Tore Anderson
2014-03-11 00:59:34 +01:00
commit 0b520f5442
7 changed files with 1205 additions and 0 deletions

5
LICENCE Normal file
View File

@@ -0,0 +1,5 @@
Copyright (c) 2014 Tore Anderson <tore@fud.no>
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.

20
Makefile Normal file
View File

@@ -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

327
README.pod Normal file
View File

@@ -0,0 +1,327 @@
=head1 NAME
B<clatd> - a CLAT implementation for Linux
=head1 DESCRIPTION
B<clatd> implements the CLAT component of the 464XLAT network architecture
specified in I<RFC 6877>. 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<I-D.anderson-siit-dc> 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<RFC 6145>) TAYGA may be
downloaded from its home page at L<http://www.litech.org/tayga/>.
=head1 SYNOPSIS
B<clatd> [options]
=head1 OPTIONS
=over
=item -q
Quiet mode; suppress normal output This is the same as setting B<quiet=1>.
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<debug=1>. Repeat for
even more debugging output, which is the
equivalent of setting B<debug=2>.
=item -c conf-file
Read configuration settings from B<conf-file>. See section B<CONFIGURATION>
below for more info.
=item -h, --help
Print a brief usage help and exit.
=item key=value
Set configuration B<key> to I<value>, overriding any setting found in the
configuration file. Refer to the section B<CONFIGURATION> below for more info.
=back
=head1 INVOCATION
B<clatd> 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<scripts/> 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<clatd> and its dependencies:
=over
=item git clone https://github.com/toreanderson/clatd
=item sudo make -C clatd install installdeps
=back
This will install B<clatd> 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<clatd> 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</etc/clatd.conf> is used. Configuration settings may also be given directly
on the command line when starting B<clatd>, which takes precedence over settings
in the configuration file.
Settings are of the form B<key=value>. A list of recogniced keys and their
possible values follow below:
=over
=item B<quiet=integer> (default: I<0>)
Set this to 1 to suppress normal output from B<clatd>. 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<debug=integer> (default: I<0>)
Set this to 1 to get debugging output from B<clatd>, 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<clat-dev=string> (default: I<clat>)
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<clatd>
simultaneously.
=item B<clat-v4-addr=ipv4-address> (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<I-D.draft-byrne-v6ops-clatip>.
=item B<clat-v6-addr=ipv6-address> (default: auto-generated)
The IPv6 address of the CLAT. Traffic to/from the B<clat-v4-addr> will be
translated into this address. By default, B<clatd> 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<dns64-servers=srv1,[srv2,..]> (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<clatd> using any of
the public DNS64/NAT64 instances on the internet. The first PLAT prefix
encountered will be used.
=item B<cmd-ip=path> (default: assume in $PATH)
Path to the B<ip> binary from the iproute2 package available at
L<https://www.kernel.org/pub/linux/utils/net/iproute2>. Required.
=item B<cmd-ip6tables=path> (default: assume in $PATH)
Path to the B<ip6tables> binary from the netfilter package available at
L<http://netfilter.org>. Only required for adding ip6tables rules
(see the B<ip6tables-enable> configuration setting).
=item B<cmd-tayga=path> (default: assume in $PATH)
Path to the B<tayga> binary from the TAYGA package available at
L<http://www.litech.org/tayga>. Required.
=item B<forwarding-enable=bool> (default: I<yes>)
Controls whether or not B<clatd> should enable IPv6 forwarding if necessary. IPv6
forwarding is necessary for B<clatd> to work correctly. It will also ensure that
the I<accept_ra> 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<clatd> is shutting down.
=item B<ip6tables-enable=bool> (default: see below)
Controls whether or not B<clatd> should insert ip6tables rules that permit the
forwarding of IPv6 traffic between the CLAT and PLAT devices. Such forwarding
must be permitted for B<clatd> to work correctly. Any rules added will be removed
when B<clatd> is shutting down.
The default is I<yes> if the ip6tables_filter kernel module is loaded, I<no>
if it is not.
=item B<plat-dev> (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<plat-prefix> (default: auto-detect)
The IPv6 translation prefix into which the PLAT maps the IPv4 internet. See
I<RFC 6052> for a closer description. By default, this is auto-detected from
DNS64 answers using the method in I<RFC 7050>.
=item B<proxynd-enable> (default: I<yes>)
Controls whether or not B<clatd> 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<clatd> is shutting down.
=item B<tayga-conffile> (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<clatd> is shutting down), but you may also
specify an explicit configuration file here, which will not be deleted on
shutdown.
=item B<tayga-v4-addr> (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<I-D.draft-byrne-v6ops-clatip>.
=item B<v4-conncheck-enable=bool> (default: I<yes>)
Whether or not to check if the system has IPv4 connectivity before starting
the CLAT. If it does, then B<clatd> will simply exit without doing anything.
This is meant so that you can always enable B<clatd> to the system startup
scripts or network-up event scripts (such as NetworkManager's dispatcher
scripts), but not have B<clatd> 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<v4-defaultroute-enable> and B<v4-defaultroute-metric> settings to prevent
B<clatd> from interfering with native IPv4 connectivity.
=item B<v4-conncheck-delay=seconds> (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<clatd>, 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<v4-defaultroute-enable=bool> (default: I<yes>)
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<clatd> 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<v4-defaultroute-metric=integer> (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<v4-defaultroute-mtu=integer> (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<RFC 6145>) 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<ipv6_offlink_mtu> setting in I<conffile.c>.
=back
=head1 LIMITATIONS
B<clatd> will not be able to acquire an IPv6 address for the CLAT if SLAAC
isn't used. I<RFC 6877> suggests DHCPv6 IA_PD should be attempted in this
case, but this isn't currently implemented.
B<clatd> will not attempt to perform Duplicate Address Detection for the IPv6
address it generates. This is a violation of I<RFC 6877>.
B<clatd> will not attempt to perform a connectivity check to a discovered PLAT
prefix before setting up the CLAT, as I<RFC 7050> suggest it should.
=head1 BUGS
If you are experiencing any bugs or have any feature requests, head over to
L<https://github.com/toreanderson/clatd/issues> 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<debug=2> in the
configuration file) when reporting a bug.
=head1 LICENCE
Copyright (c) 2014 Tore Anderson <tore@fud.no>
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

792
clatd Executable file
View File

@@ -0,0 +1,792 @@
#! /usr/bin/perl -w
#
# Copyright (c) 2014 Tore Anderson <tore@fud.no>
#
# 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 "<warn> ", join("", @_), "\n" unless($CFG{"quiet"} >= 2);
}
sub err {
print "<error> ", 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 <tore\@fud.no>\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} : "<undefined>");
}
p("Starting clatd v$VERSION by Tore Anderson <tore\@fud.no>");
#
# 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);

View File

@@ -0,0 +1,24 @@
#!/bin/sh
#
# clatd dispatcher script for NetworkManager
#
# Install it to: /etc/NetworkManager/dispatcher.d/50-clatd
#
# Written by Tore Anderson <tore@fud.no>
#
# 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

21
scripts/clatd.systemd Normal file
View File

@@ -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 <tore@fud.no>
#
[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

16
scripts/clatd.upstart Normal file
View File

@@ -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 <tore@fud.no>
#
description "464XLAT CLAT daemon"
author "Tore Anderson <tore@fud.no>"
start on net-device-up
stop on runlevel [!2345]
exec /usr/sbin/clatd