11 Commits

Author SHA1 Message Date
Tore Anderson
009c3f74a9 ipxlat: support all RFC 6052 prefix lengths
Also fixes a bug that caused /40 prefixes to be rejected (regardless of
translation engine in use).
2026-04-11 23:20:34 +02:00
Tore Anderson
4e1c7fa71f Add support for new ipxlat kernel driver (WIP)
Needs the kernel module from
https://codeberg.org/IPv6-Monostack/ipxlat-net-next. This can be built
and loaded out-of-tree like so (assuming you've got all the necessary
kernel development packages (make, gcc, kernel-headers etc.) installed:

```
git clone --depth 1 https://codeberg.org/IPv6-Monostack/ipxlat-net-next
cd ipxlat-net-next/drivers/net/ipxlat
make -f /lib/modules/$(uname -r)/build/Makefile M=$PWD CONFIG_IPXLAT=m CFLAGS_MODULE=-I$PWD/../../../include modules
sudo make -f /lib/modules/$(uname -r)/build/Makefile M=$PWD CONFIG_IPXLAT=m CFLAGS_MODULE=-I$PWD/../../../include modules_install
sudo modprobe ipxlat
```

You'll also need to install the 'ipxlat-ctl' utility from
https://codeberg.org/IPv6-Monostack/ipxlat somewhere in your $PATH (or
point clatd's 'cmd-ipxlat-ctl' config option at its location).

(Note: I was not able to make the above 'ipxlat-ctl' utility work, so I
have made an alternative 'ipxlat-ctl' script located in the root of the
clatd repo. You probably need to edit that script so that it can find
the pyynl cli utility included in the kernel sources.)

For now, only /96 RFC6052 prefixes are supported. Support for the other
prefix lengths will be added later.
2026-04-11 13:00:46 +02:00
Tore Anderson
d9f274bbe7 Merge pull request #51 from UnderEu/patch-1
Update service status to better reflect 'no PLAT' state
2025-03-23 11:30:06 +01:00
Anderson Silva
ae8da007c6 Update service status to better reflect 'no PLAT' state
When checking the service status of clatd in systemctl, if no NAT64 prefix is identified, clatd prints _"No PLAT prefix could be discovered. Your ISP probably doesn't provide NAT64/DNS64 PLAT service. Exiting."_.
I understand the point of it but there are cases that a maintainer (i.e. yours truly) deploys a local PLAT on LAN for an IPv6-mostly network despite the ISP who this network upstreams traffic to the Internet has it or not. So, my intention is to reflect a better status, stating that the current connection, regardless of the medium (Ethernet, Wi-Fi, WWAN, mobilt tethering a.k.a. hotspot), has no PLAT available instead of blaming the ISP only.
2025-03-22 22:10:44 -03:00
Tore Anderson
5e085f1b17 Release clatd v2.1.0
New feature:

* Support for RFC 8781 PLAT prefix discovery through the PREF64 Router
  Advertisement option. Relies on systemd-networkd's support for the
  same being enabled. (See issue #32.)

Bugfixes:

* Create any missing leading directories in the Makefile's install
  target. (See pull request #47, thanks @DanielG!)
* Apply metadata mark earlier in the nftables pipeline. This makes clatd
  interoperate with the IPv6 reverse path filtering as implemented by
  NixOS. (See pull request #49, thanks @jmbaur!)
2025-03-21 08:46:54 +01:00
Tore Anderson
06c567b9cb Merge pull request #49 from jmbaur/chain-priority
Decrease priority number for prerouting chain. This makes clatd work out of the box on NixOS, which does IPv6 reverse path filtering at priority `mangle + 10`.
2025-03-21 08:19:36 +01:00
Jared Baur
f86f1cabb8 Decrease priority number for prerouting chain
This allows for clatd to work OOTB on distros shipping firewalls that do
reverse-path filtering based on conntrack marks in the "mangle" priority
(i.e. -150).
2025-03-20 15:20:14 -07:00
Tore Anderson
f812070f60 Separate DNS64 servers with spaces in debug output 2025-03-20 22:26:58 +01:00
Tore Anderson
f91d96b991 Get PLAT prefix from systemd-networkd, if possible
Makes clatd check if systemd-networkd is aware of any PLAT prefix (which
it may have learned from the PREF64 Router Advertisement option, cf. RFC
8781).

If a prefix is obtained from systemd-network, DNS64-based PLAT prefix
discovery is skipped, as mandated by
https://datatracker.ietf.org/doc/draft-ietf-v6ops-prefer8781/.

However, if the dns64-servers config option is set, clatd will use
DNS64-based PLAT prefix discovery towards the specified servers, and it
will not query systemd-networkd at all.

Closes #32
2025-02-22 10:00:45 +01:00
Tore Anderson
b93a5526a5 Merge pull request #47 from DanielG/fix-makefile
Fix Makefile for distros
2025-02-21 13:07:50 +01:00
Daniel Gröber
6f98967f0e Fix Makefile for distros
The first declared target (previously 'install') is use when make is called
without a target argument. This breaks the assumptions of distros.

Further install needs -D to creat the necessary target directories or it
behaves as cp and the target dirs need to exist already.
2025-02-21 12:49:05 +01:00
3 changed files with 109 additions and 21 deletions

View File

@@ -7,9 +7,11 @@ DNF_OR_YUM:=$(shell which dnf || which yum)
SYSTEMCTL:=$(shell which systemctl)
TAYGA:=$(shell which tayga)
all:
install:
# Install the main script
install -m0755 clatd $(DESTDIR)$(PREFIX)/sbin/clatd
install -D -m0755 clatd $(DESTDIR)$(PREFIX)/sbin/clatd
# Install manual page if pod2man is installed
pod2man --name clatd --center "clatd - a CLAT implementation for Linux" --section 8 README.pod $(DESTDIR)$(PREFIX)/share/man/man8/clatd.8 && gzip -f9 $(DESTDIR)$(PREFIX)/share/man/man8/clatd.8 || echo "pod2man is required to generate manual page"
# Install systemd service file if applicable for this system

108
clatd
View File

@@ -27,7 +27,7 @@ use strict;
use IPC::Cmd qw(can_run);
use Net::IP;
my $VERSION = "2.0.0";
my $VERSION = "2.1.0";
#
# Populate the global config hash with the default values
@@ -42,6 +42,7 @@ $CFG{"clat-v4-addr"} = "192.0.0.1"; # from RFC 7335
$CFG{"clat-v6-addr"} = "shared"; # re-use primary address from host OS
$CFG{"dns64-servers"} = undef; # use system resolver by default
$CFG{"cmd-ip"} = "ip"; # assume in $PATH
$CFG{"cmd-ipxlat-ctl"} = "ipxlat-ctl"; # assume in $PATH
$CFG{"cmd-networkctl"} = "networkctl"; # assume in $PATH
$CFG{"cmd-nft"} = "nft"; # assume in $PATH
$CFG{"cmd-tayga"} = "tayga"; # assume in $PATH
@@ -62,6 +63,7 @@ $CFG{"v4-defaultroute-replace"} = 0; # replace existing v4 defaultroute?
$CFG{"v4-defaultroute-metric"} = 2048; # metric for the IPv4 defaultroute
$CFG{"v4-defaultroute-mtu"} = 1260; # MTU for the IPv4 defaultroute
$CFG{"v4-defaultroute-advmss"} = 0; # TCP MSS for the IPv4 defaultroute
$CFG{"xlat-engine"} = undef; # which translation engine to use
#
@@ -357,7 +359,8 @@ sub get_plat_prefix_from_dns64 {
$res->nameservers(map {
Net::IP->new($_)->version() == 4 ? "::ffff:$_" : $_;
} $res->nameservers);
d2("Nameservers after Net::DNS bug workaround: ", $res->nameservers);
d2("Nameservers after Net::DNS bug workaround: ",
join(" ", $res->nameservers));
my $pkt = $res->query('ipv4only.arpa', 'AAAA');
if(!$pkt) {
@@ -391,7 +394,7 @@ sub get_plat_prefix_from_dns64 {
p("No PLAT prefix could be discovered, using fallback");
return $CFG{"plat-fallback-prefix"};
} else {
p("No PLAT prefix could be discovered. Your ISP probably doesn't provide",
p("No PLAT prefix could be discovered. Your connection probably doesn't provide",
" NAT64/DNS64 PLAT service. Exiting.");
cleanup_and_exit(0);
}
@@ -665,6 +668,7 @@ sub get_clat_v6_addr {
#
my $cleanup_remove_tayga_clat_dev; # true if having created it
my $cleanup_remove_nat46_clat_dev; # true if having created it
my $cleanup_remove_ipxlat_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'
@@ -696,6 +700,9 @@ sub cleanup_and_exit {
print $nat46_control_fh "del ", cfg("clat-dev"), "\n";
close($nat46_control_fh) or err("close($nat46_control_fh: $!");
}
if(defined($cleanup_remove_ipxlat_clat_dev)) {
cmd(\&w, cfg("cmd-ip"), qw(link delete dev), cfg("clat-dev"));
}
if(defined($cleanup_zero_forwarding_sysctl)) {
d("Cleanup: Resetting forwarding sysctl to 0");
sysctl("net/ipv6/conf/all/forwarding", 0);
@@ -733,11 +740,11 @@ sub cleanup_and_exit {
}
if(defined($cleanup_remove_ufw_rules)) {
cmd(\&w, cfg("cmd-ufw"), qw(route delete allow in on), cfg("clat-dev"),
"from", cfg("clat-v6-addr"), qw(out on), cfg("plat-dev"), "to",
"from", cfg("internal-clat-v6-addr"), qw(out on), cfg("plat-dev"), "to",
cfg("plat-prefix"));
cmd(\&w, cfg("cmd-ufw"), qw(route delete allow in on), cfg("plat-dev"),
"from", cfg("plat-prefix"), qw(out on), cfg("clat-dev"), "to",
cfg("clat-v6-addr"));
cfg("internal-clat-v6-addr"));
}
exit($exitcode);
@@ -844,6 +851,7 @@ if(!$CFG{"plat-prefix"}) {
$ip->prefixlen() != 64 and
$ip->prefixlen() != 56 and
$ip->prefixlen() != 48 and
$ip->prefixlen() != 40 and
$ip->prefixlen() != 32) {
err("PLAT prefix $CFG{'plat-prefix'} has an invalid prefix length ",
"(see RFC 6052 section 2.2)");
@@ -870,6 +878,16 @@ if($CFG{"clat-v6-addr"} eq "shared") {
$CFG{"clat-v6-addr"} = get_clat_v6_addr();
}
# Since ipxlat does not support EAM, we'll need to do SNAT66 from the RFC 6052
# representaion of the CLAT IPv4 address used internally. It is sometimes
# necessary to refer to this address instead of the CLAT IPv6 address, so
# create a separate entry in the config hash for that. The other engines (tayga
# and nat46) supports using EAM to map directly between the CLAT IPv4 address
# and CLAT IPv6 address without any going via any intermediate RFC6052 address,
# so default to having the two addresses identical (if using ipxlat, the
# internal one will be overridden later).
$CFG{"internal-clat-v6-addr"} = $CFG{"clat-v6-addr"};
# Fill defaults for proxynd-enable and ctmark for clat-v6-addr!=shared
if(!defined($CFG{"proxynd-enable"})) {
$CFG{"proxynd-enable"} = 1;
@@ -906,17 +924,49 @@ if(cfgbool("v4-conncheck-enable") and !cfgbool("v4-defaultroute-replace")) {
d("Skipping IPv4 connectivity check at user request");
}
# Let's figure out if there's nat46 kernel module loaded
my $nat46_controlfile = "/proc/net/nat46/control";
my $use_nat46 = (-e $nat46_controlfile);
my $nat46_controlfile = "/proc/net/nat46/control";
#
# Auto-detect which translation engine to use if not specified in config
if(!cfg("xlat-engine") and (-e $nat46_controlfile)) {
p("Using translation engine: nat46");
$CFG{"xlat-engine"} = "nat46";
} elsif(!cfg("xlat-engine") and can_run(cfg("cmd-ipxlat-ctl"))) {
p("Using translation engine: ipxlat");
$CFG{"xlat-engine"} = "ipxlat";
my $pfx = Net::IP->new($CFG{"plat-prefix"});
my $fmt = $pfx->ip;
if($pfx->prefixlen == 96) {
$fmt =~ s/0000:0000$/%02x%02x:%02x%02x/;
} elsif($pfx->prefixlen == 64) {
$fmt =~ s/00:0000:0000:0000$/%02x:%02x%02x:%02x00:0000/;
} elsif($pfx->prefixlen == 56) {
$fmt =~ s/00:0000:0000:0000:0000$/%02x:00%02x:%02x%02x:0000:0000/;
} elsif($pfx->prefixlen == 48) {
$fmt =~ s/0000:0000:0000:0000:0000$/%02x%02x:00%02x:%02x00:0000:0000/;
} elsif($pfx->prefixlen == 40) {
$fmt =~ s/00:0000:0000:0000:0000:0000$/%02x:%02x%02x:00%02x:0000:0000:0000/;
} elsif($pfx->prefixlen == 32) {
$fmt =~ s/0000:0000:0000:0000:0000:0000$/%02x%02x:%02x%02x:0000:0000:0000:0000/;
}
my $ip = sprintf($fmt, split(/\./, $CFG{"clat-v4-addr"}));
$CFG{"internal-clat-v6-addr"} = Net::IP->new($ip)->short;
p("Using internal CLAT IPv6 address: ", $CFG{"internal-clat-v6-addr"});
} elsif(!cfg("xlat-engine") and can_run(cfg("cmd-tayga"))) {
p("Using translation engine: TAYGA");
$CFG{"xlat-engine"} = "tayga";
} elsif(!cfg("xlat-engine")) {
err("No supported translation engine available. Please install TAYGA or ensure ",
"either the nat46 or ipxlat kernel modules is loaded.");
}
#
# Write out the TAYGA config file, either to the user-specified location,
# or to a temporary file (which we'll delete later)
#
unless($use_nat46) {
if(cfg("xlat-engine") eq "tayga") {
my $tayga_conffile = cfg("tayga-conffile");
my $tayga_conffile_fh;
if(!$tayga_conffile) {
@@ -975,11 +1025,11 @@ if(can_run(cfg("cmd-ufw"))) {
if(/^Status: active$/) {
p("UFW local firewall framework active, allowing CLAT-PLAT traffic");
cmd(\&err, cfg("cmd-ufw"), qw(route allow in on), cfg("clat-dev"),
"from", cfg("clat-v6-addr"), qw(out on), cfg("plat-dev"), "to",
"from", cfg("internal-clat-v6-addr"), qw(out on), cfg("plat-dev"), "to",
cfg("plat-prefix"));
cmd(\&err, cfg("cmd-ufw"), qw(route allow in on), cfg("plat-dev"),
"from", cfg("plat-prefix"), qw(out on), cfg("clat-dev"), "to",
cfg("clat-v6-addr"));
cfg("internal-clat-v6-addr"));
$cleanup_remove_ufw_rules = 1;
}
}
@@ -1026,14 +1076,17 @@ close($fd) or err("'ip -6 rule show prio 0 table local' failed");
# route to the corresponding IPv6 address, and possibly an IPv4 default route
#
p("Creating and configuring up CLAT device '", cfg("clat-dev"), "'");
if($use_nat46) {
if(cfg("xlat-engine") eq "nat46") {
my $nat46_control_fh;
open($nat46_control_fh, ">$nat46_controlfile") or
err("Could not open nat46 control socket for writing");
print $nat46_control_fh "add ", cfg("clat-dev"), "\n";
close($nat46_control_fh) or err("close($nat46_control_fh: $!");
$cleanup_remove_nat46_clat_dev = 1;
} else {
} elsif(cfg("xlat-engine") eq "ipxlat") {
cmd(\&err, cfg("cmd-ip"), qw(link add name), cfg("clat-dev"), qw(type ipxlat));
$cleanup_remove_ipxlat_clat_dev = 1;
} elsif(cfg("xlat-engine") eq "tayga") {
cmd(\&err, cfg("cmd-tayga"), "--config", cfg("tayga-conffile"), "--mktun",
cfgint("debug") ? "-d" : "");
$cleanup_remove_tayga_clat_dev = 1;
@@ -1041,10 +1094,10 @@ if($use_nat46) {
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"),
cmd(\&err, cfg("cmd-ip"), qw(-6 route add), cfg("internal-clat-v6-addr"),
"dev", cfg("clat-dev"), "table", cfgint("route-table"));
cmd(\&err, cfg("cmd-ip"), qw(-6 rule add prio 0 from),
cfg("plat-prefix"), "to", cfg("clat-v6-addr"),
cfg("plat-prefix"), "to", cfg("internal-clat-v6-addr"),
cfgint("ctmark") ? ("fwmark", cfgint("ctmark")) : (),
"table", cfg("route-table"));
$cleanup_remove_clat_iprule = 1;
@@ -1055,13 +1108,13 @@ if(cfgint("ctmark")) {
or err("'nft -f-' failed to execute");
print $fd "add table ip6 clatd\n";
print $fd "add chain ip6 clatd prerouting ",
"{ type filter hook prerouting priority 0; }\n";
"{ type filter hook prerouting priority mangle; }\n";
print $fd "add rule ip6 clatd prerouting",
" iif ", cfg("clat-dev"),
" ip6 saddr ", cfg("clat-v6-addr"),
" ip6 saddr ", cfg("internal-clat-v6-addr"),
" ip6 daddr ", cfg("plat-prefix"),
" ct mark set ", cfgint("ctmark"),
# set meta mark as well, to placate firewalld's IPv6_rpfilter
# set meta mark as well, to placate firewalld's IPv6_rpfilter and NixOS' rpfilter rules
" meta mark set ", cfgint("ctmark"), " counter\n";
print $fd "add rule ip6 clatd prerouting",
" iif ", cfg("plat-dev"),
@@ -1069,6 +1122,15 @@ if(cfgint("ctmark")) {
" ip6 daddr ", cfg("clat-v6-addr"),
" ct mark ", cfgint("ctmark"),
" meta mark set ct mark counter\n";
if(cfg("clat-v6-addr") ne cfg("internal-clat-v6-addr")) {
print $fd "add chain ip6 clatd postrouting ",
"{ type nat hook postrouting priority srcnat; }\n";
print $fd "add rule ip6 clatd postrouting",
" iif ", cfg("clat-dev"),
" ip6 saddr ", cfg("internal-clat-v6-addr"),
" ip6 daddr ", cfg("plat-prefix"),
" snat to ", cfg("clat-v6-addr");
}
close($fd) or err("'nft -f-' failed");
$cleanup_remove_nftable = 1;
}
@@ -1115,7 +1177,7 @@ if(cfg("script-up")) {
# All preparation done! We can now start nat46 or TAYGA, which will handle the actual
# translation of IP packets.
#
if($use_nat46){
if(cfg("xlat-engine") eq "nat46") {
p("Setting up nat46 kernel module");
my $nat46_control_fh;
open($nat46_control_fh, ">$nat46_controlfile") or
@@ -1131,7 +1193,13 @@ if($use_nat46){
$SIG{'INT'} = \&cleanup_handler;
$SIG{'TERM'} = \&cleanup_handler;
sleep();
} else {
} elsif(cfg("xlat-engine") eq "ipxlat") {
cmd(\&err, cfg("cmd-ipxlat-ctl"), cfg("clat-dev"), "pool6", cfg("plat-prefix"));
# Nothing more to do here, we just set up a cleanup handler and sleep forever.
$SIG{'INT'} = \&cleanup_handler;
$SIG{'TERM'} = \&cleanup_handler;
sleep();
} elsif(cfg("xlat-engine") eq "tayga") {
my $tayga_conffile = cfg("tayga-conffile");
p("Starting up TAYGA, using config file '$tayga_conffile'");

18
ipxlat-ctl Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/bash -e
# This is a wrapper around the python ynl cli included with the kernel that
# mimics the documented behaviour of the ipxlat-ctl tool available at
# https://codeberg.org/IPv6-Monostack/ipxlat, but which I haven't been able to
# make work. You'll need to ensure the tool is somewhere in $PATH.
PATH=/usr/src/kernels/ipxlat-net-next/tools/net/ynl/pyynl:$PATH
dev="$1"
prefix="$3"
IID=$(< /sys/class/net/$dev/ifindex)
ADDR_HEX=$(python3 -c 'import ipaddress,sys; print(ipaddress.IPv6Address(sys.argv[1]).packed.hex())' ${prefix%/*})
PREFIXLEN=${prefix#*/}
JSON='{"ifindex": '"$IID"', "config": {"xlat-prefix6": { "prefix": "'$ADDR_HEX'", "prefix-len": '$PREFIXLEN'}}}'
cli.py --family ipxlat --do dev-set --json "$JSON"