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
This commit is contained in:
Tore Anderson
2025-02-14 11:13:26 +01:00
parent 1c4ca683b5
commit f564300918
3 changed files with 85 additions and 11 deletions

View File

@@ -20,8 +20,8 @@ install:
installdeps:
# .deb/apt-get based distros
if test -x "$(APT_GET)"; then $(APT_GET) -y install perl-base perl-modules libnet-ip-perl libnet-dns-perl iproute2 nftables tayga; fi
if test -x "$(APT_GET)"; then $(APT_GET) -y install perl-base perl-modules libnet-ip-perl libnet-dns-perl libjson-perl iproute2 nftables tayga; fi
# .rpm/DNF/YUM-based distros
if test -x "$(DNF_OR_YUM)"; then $(DNF_OR_YUM) -y install perl perl-IPC-Cmd perl-Net-IP perl-Net-DNS perl-File-Temp iproute nftables; fi
if test -x "$(DNF_OR_YUM)"; then $(DNF_OR_YUM) -y install perl perl-IPC-Cmd perl-Net-IP perl-Net-DNS perl-File-Temp perl-JSON iproute nftables; fi
# If necessary, try to install the TAYGA .rpm using dnf/yum. It is unfortunately not available in all .rpm based distros (in particular CentOS/RHEL).
if test -x "$(DNF_OR_YUM)" && test ! -x "$(TAYGA)"; then $(DNF_OR_YUM) -y install tayga || echo "ERROR: Failed to install TAYGA using dnf/yum, the package is probably not included in your distro. Try enabling the EPEL repo <URL: https://fedoraproject.org/wiki/EPEL> and try again, or install TAYGA <URL: http://www.litech.org/tayga> directly from source."; exit 1; fi

View File

@@ -241,17 +241,32 @@ with using B<clatd> as a SIIT-DC Edge Relay (I<RFC 7756>).
=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.
using the method described in I<RFC 7050>. By default, B<clatd> will first try
to determine if systemd-networkd is aware of a PLAT prefix (learned from the
PREF64 Router Advertisement option, cf. I<RFC 8781>), falling back on using
DNS64 discovery towards the system resolver if it isn't.
It might be useful to override this in case your network does not advertise the
PREF64 RA option, 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-networkctl=path> (default: assume in $PATH)
Path to the B<networkctl> binary from systemd-networkd. Required in order to
use any PLAT prefix discovered by systemd-networkd from the PREF64 Router
Advertisement option (see I<RFC 8781> and the I<UsePREF64> option in
I<systemd.network(5)> for more information). The first prefix returned is used,
any others are ignored.
To prevent PLAT prefix discovery via systemd-networkd from being attempted, set
this to an empty string.
=item B<cmd-nft=path> (default: assume in $PATH)
Path to the B<nft> binary from the nftables package available at
@@ -493,9 +508,9 @@ SOFTWARE.
=head1 SEE ALSO
ip(8), nft(8), tayga(8), tayga.conf(5)
ip(8), nft(8), systemd.network(5), tayga(8), tayga.conf(5)
RFC 6052, RFC 6145, RFC 6146, RFC 6877, RFC 7050, RFC 7335 RFC 7755, RFC 7756,
RFC 7757
RFC 7757, RFC 8781
=cut

63
clatd
View File

@@ -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-networkctl"} = "networkctl"; # assume in $PATH
$CFG{"cmd-nft"} = "nft"; # assume in $PATH
$CFG{"cmd-tayga"} = "tayga"; # assume in $PATH
$CFG{"cmd-ufw"} = "ufw"; # assume in $PATH
@@ -329,7 +330,7 @@ sub find_rfc7050_wka {
# 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 {
sub get_plat_prefix_from_dns64 {
p("Performing DNS64-based PLAT prefix discovery (cf. RFC 7050)");
require Net::DNS;
@@ -397,6 +398,62 @@ sub get_plat_prefix {
}
#
# This function attempts request a PLAT prefix from systemd-networkd, which
# systemd-networkd will know about if the Router Advertisements contain the
# PREF64 option defined in RFC 8781, and systemd-networkd is configured with
# UsePREF64=true (not the default). The first prefix seen is used, subsequent
# ones are ignored.
#
sub get_plat_prefix_from_networkd {
if(!can_run(cfg("cmd-networkctl"))) {
d(cfg("cmd-networkctl"), " is not installed or not exectutable, skipping");
return;
}
p("Attempting to query systemd-networkd for PLAT prefix (cf. RFC 8781)");
open(my $fd, '-|', cfg("cmd-networkctl"), qw(--json=short status))
or err(cfg("cmd-networkctl"), " failed to execute");
my @out = <$fd>;
if(!close($fd)) {
p("'networkctl status' failed, trying DNS64 PLAT prefix discovery");
return;
}
require JSON;
my $status = JSON::decode_json("@out");
return unless($status->{"Interfaces"});
for my $interface (@{$status->{"Interfaces"}}) {
next if(!$interface->{"NDisc"}->{"PREF64"});
d2($interface->{"Name"}, " PREF64 bytes: ",
join(" ", @{$interface->{"NDisc"}->{"PREF64"}->[0]->{"Prefix"}}),
" / ", $interface->{"NDisc"}->{"PREF64"}->[0]->{"PrefixLength"});
# PREF64 is an array of bytes, convert to IPv6 string representation
my @bytes = @{$interface->{"NDisc"}->{"PREF64"}->[0]->{"Prefix"}};
my $ipstr;
for (my $i = 0; $i < @bytes;) {
$ipstr .= sprintf("%02x%02x", $bytes[$i++], $bytes[$i++]);
$ipstr .= ":" if($i < @bytes);
}
$ipstr .= "/" . $interface->{"NDisc"}->{"PREF64"}->[0]->{"PrefixLength"};
d2("String representation: $ipstr");
# Run the result through Net::IP to ensure we have a valid IPv6 string
# and also to convert it to compact format.
my $ip = Net::IP->new($ipstr, 6)
or err("Failed to convert PREF64 bytes array to IPv6 string format");
my $prefix = $ip->short() . "/" . $ip->prefixlen();
d("Obtained PLAT prefix $prefix from systemd-networkd");
return $prefix;
}
}
#
# 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
@@ -770,7 +827,9 @@ 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();
$CFG{"plat-prefix"} ||= get_plat_prefix_from_networkd()
unless($CFG{"dns64-servers"});
$CFG{"plat-prefix"} ||= get_plat_prefix_from_dns64();
if(!$CFG{"plat-prefix"}) {
w("No PLAT prefix was discovered or specified; 464XLAT cannot work.");
exit 0;