Use routing policy and ctmark for routing to CLAT

This moves the route for IPv6 traffic from the PLAT to the CLAT into a
separate routing table, and uses the Linux kernel's routing policy
framework to redirect traffic into this routing table.

This makes it possible to set `clat-v6-addr` to an address also used by
the main host OS, removing the requirement for having a dedicated
secondary address assigned to the CLAT.

Additionally, support using nftables to set a connection tracking mark
on outbound connections from the CLAT, and ensuring only return traffic
matching that mark is returned back to the CLAT. This makes it possible
for the CLAT to share an IPv6 address with the main host OS without
breaking connectivity to DNS64-synthesised IPv6 addresses.

The trade-off of using a connection tracking mark is that the CLAT can
not receive unsolicited traffic from the IPv4 Internet via the PLAT.
However in the common case, where the PLAT is Stateful NAT64, that is
the case no matter what.

Closes #25.
This commit is contained in:
Tore Anderson
2025-02-02 10:27:01 +01:00
parent 05728771ca
commit f0d7c09adf
3 changed files with 104 additions and 7 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 libio-socket-ip-perl iproute2 tayga; fi
if test -x "$(APT_GET)"; then $(APT_GET) -y install perl-base perl-modules libnet-ip-perl libnet-dns-perl libio-socket-ip-perl iproute2 nftables tayga; fi
# .rpm/DNF/YUM-based distros
if test -x "$(DNF_OR_YUM)"; then $(DNF_OR_YUM) -y install perl perl-Net-IP perl-Net-DNS perl-IO-Socket-IP perl-File-Temp iproute; fi
if test -x "$(DNF_OR_YUM)"; then $(DNF_OR_YUM) -y install perl perl-Net-IP perl-Net-DNS perl-IO-Socket-IP perl-File-Temp 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

@@ -218,6 +218,17 @@ If multiple addresses are found in either category, the one that shares the
longest common prefix with the PLAT prefix will be preferred when deriving
the CLAT IPv6 address according to the algorithm described above.
=item B<ctmark> (default: 0)
If set to a non-zero integer, nftables will be used to mark outgoing
connections through the CLAT with this connection tracking mark, and the Linux
kernel routing policy is set up so that only response traffic from the PLAT
that matches this connection tracking mark is routed back to the CLAT.
This means that connections initiated from the external IPv4 network via the
PLAT will not reach the CLAT, therefore the use of this feature is incompatible
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
@@ -232,6 +243,11 @@ encountered will be used.
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-nft=path> (default: assume in $PATH)
Path to the B<nft> binary from the nftables package available at
L<https://nftables.org/projects/nftables/>. Required if I<ctmark> is set.
=item B<cmd-tayga=path> (default: assume in $PATH)
Path to the B<tayga> binary from the TAYGA package available at
@@ -252,7 +268,8 @@ B<clatd> is shutting down.
Which network device is facing the PLAT (NAT64). By default, this is
auto-detected by performing a route table lookup towards the PLAT prefix. This
setting is used when generating the CLAT IPv6 address and Proxy-ND entries.
setting is used when generating the CLAT IPv6 address, adding Proxy-ND entries,
and nftables rules.
=item B<plat-prefix> (default: auto-detect)
@@ -277,6 +294,13 @@ case, either.
Any entries added wil be removed when B<clatd> is shutting down.
=item B<route-table> (default: I<0xc1a7>)
The Linux kernel routing table used to hold the route that directs IPv6 packets
from the PLAT to the CLAT. B<clatd> will add a custom routing policy entry
(using B<ip -6 rule add>) so that this routing table is used instead of the
default one.
=item B<tayga-conffile> (default: use a temporary file)
Where to write the TAYGA configuration file. By default, a temporary file will
@@ -399,6 +423,15 @@ If the upstream network is using DHCPv6, B<clatd> will not be able to generate
a CLAT IPv6 address at all, due to the fact that DHCPv6-assigned addresses do
not carry a prefix length.
If B<clat-v6-addr> is set to an address assigned to a local interface and
B<ctmark> is not set, the host OS will not be able to communicate
bi-directionally with IPv4 destinations directly through the PLAT (e.g.,
I<ping6 64:ff9b::192.0.2.1>). This is because the response traffic will be
routed back to the CLAT, and ultimately return to the Linux kernel as an IPv4
packet, which does not match the outgoing IPv6 socket. Such direct
communication is normal when using DNS64 synthesis for all queries (as opposed
to just I<ipv4only.arpa>).
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.
@@ -434,7 +467,7 @@ SOFTWARE.
=head1 SEE ALSO
ip(8), tayga(8), tayga.conf(5)
ip(8), nft(8), tayga(8), tayga.conf(5)
RFC 6052, RFC 6145, RFC 6146, RFC 6877, RFC 7050, RFC 7335 RFC 7755, RFC 7756,
RFC 7757

70
clatd
View File

@@ -41,12 +41,15 @@ $CFG{"clat-v4-addr"} = "192.0.0.1"; # from RFC 7335
$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-nft"} = "nft"; # assume in $PATH
$CFG{"cmd-tayga"} = "tayga"; # assume in $PATH
$CFG{"ctmark"} = 0; # match ctmark for routing pkts to CLAT
$CFG{"forwarding-enable"} = 1; # enable ipv6 forwarding?
$CFG{"plat-dev"} = undef; # PLAT-facing device, default detect
$CFG{"plat-prefix"} = undef; # detect using DNS64 by default
$CFG{"plat-fallback-prefix"} = undef; # fallback prefix if no prefix is found
$CFG{"proxynd-enable"} = 1; # add proxy-nd entry for clat?
$CFG{"route-table"} = 0xc1a7; # add route to CLAT in this table
$CFG{"tayga-conffile"} = undef; # make a temporary one by default
$CFG{"tayga-v4-addr"} = "192.0.0.2"; # from RFC 7335
$CFG{"v4-conncheck-enable"} = 1; # exit if there's already a defroute
@@ -385,8 +388,8 @@ sub get_plat_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 and
# when installing Proxy-ND entries.
# PLAT/NAT64. We need this when generating an IPv6 address for the CLAT, when
# installing Proxy-ND entries, and when setting up nft rules.
#
sub get_plat_dev {
d("get_plat_dev(): finding which network dev faces the PLAT");
@@ -594,6 +597,9 @@ 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_nftable; # true if having added an nftable
my $cleanup_remove_clat_iprule; # true if having added clat iprule
my $cleanup_restore_local_iprule_prio; # true if having reordered local iprule
my @cleanup_restore_v4_defaultroutes; # temporarily replaced defaultroutes
sub cleanup_and_exit {
@@ -634,10 +640,23 @@ sub cleanup_and_exit {
cmd(\&w, cfg("cmd-ip"), qw(-6 neighbour delete proxy), cfg("clat-v6-addr"),
"dev", cfg("plat-dev"));
}
if(defined($cleanup_remove_nftable)) {
d("Cleanup: Removing clatd netfilter table");
cmd(\&w, cfg("cmd-nft"), "delete table ip6 clatd");
}
for my $rt (@cleanup_restore_v4_defaultroutes) {
d("Cleanup: Restoring temporarily replaced IPv4 default route");
cmd(\&w, cfg("cmd-ip"), qw(-4 route add), @{$rt});
}
if(defined($cleanup_restore_local_iprule_prio)) {
d("Cleanup: Restoring local ip rule priority to 0");
cmd(\&w, cfg("cmd-ip"), qw(-6 rule add prio 0 table local));
cmd(\&w, cfg("cmd-ip"), qw(-6 rule del prio 1 table local));
}
if(defined($cleanup_remove_clat_iprule)) {
d("Cleanup: Removing ip rule for redirecting inbound traffic to CLAT");
cmd(\&w, cfg("cmd-ip"), qw(-6 rule del prio 0 table), cfg("route-table"));
}
exit($exitcode);
}
@@ -854,6 +873,21 @@ if(cfgbool("proxynd-enable")) {
$cleanup_remove_proxynd_entry = 1;
}
# Move the default ip rule for local traffic to a higher priority, so we can
# override it. This is necessary if re-using the primary IPv6 address on the
# PLAT-facing device for the CLAT function.
open(my $fd, '-|', cfg("cmd-ip"), qw(-6 rule show prio 0 table local))
or err("'ip -6 rule show prio 0 table local' failed to execute");
while(<$fd>) {
d("Increasing prio of default local ip rule to 1");
cmd(\&err, cfg("cmd-ip"), qw(-6 rule add prio 1 table local));
cmd(\&err, cfg("cmd-ip"), qw(-6 rule del prio 0 table local));
$cleanup_restore_local_iprule_prio = 1;
last;
}
close($fd) or err("'ip -6 rule show prio 0 table local' failed");
#
# 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
@@ -875,7 +909,37 @@ 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"));
"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"),
cfgint("ctmark") ? ("fwmark", cfgint("ctmark")) : (),
"table", cfg("route-table"));
$cleanup_remove_clat_iprule = 1;
if(cfgint("ctmark")) {
p("Adding clatd nftable, using conntrack mark ", cfgint("ctmark"));
open(my $fd, '|-', cfg("cmd-nft"), "-f-")
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";
print $fd "add rule ip6 clatd prerouting",
" iif ", cfg("clat-dev"),
" ip6 saddr ", cfg("clat-v6-addr"),
" ip6 daddr ", cfg("plat-prefix"),
" ct mark set ", cfgint("ctmark"),
# set meta mark as well, to placate firewalld's IPv6_rpfilter
" meta mark set ", cfgint("ctmark"), " counter\n";
print $fd "add rule ip6 clatd prerouting",
" iif ", cfg("plat-dev"),
" ip6 saddr ", cfg("plat-prefix"),
" ip6 daddr ", cfg("clat-v6-addr"),
" ct mark ", cfgint("ctmark"),
" meta mark set ct mark counter\n";
close($fd) or err("'nft -f-' failed");
$cleanup_remove_nftable = 1;
}
if(cfgbool("v4-defaultroute-replace")) {
open(my $fd, '-|', cfg("cmd-ip"), qw(-4 route show default))
or err("'ip -4 route show default' failed to execute");