1# This test verifies that we can ping an IPv4-only server from an IPv6-only
2# client via a NAT64 router using CLAT on the client. The hosts and networks
3# are configured as follows:
4#
5# +------
6# Client | clat Address: 192.0.0.1/32 (configured via clatd)
7# | Route: default
8# |
9# | eth1 Address: Assigned via SLAAC within 2001:db8::/64
10# | | Route: default via IPv6LL address
11# +--|---
12# | VLAN 3
13# +--|---
14# | eth2 Address: 2001:db8::1/64
15# Router |
16# | nat64 Address: 64:ff9b::1/128
17# | Route: 64:ff9b::/96
18# | Address: 192.0.2.0/32
19# | Route: 192.0.2.0/24
20# |
21# | eth1 Address: 100.64.0.1/24
22# +--|---
23# | VLAN 2
24# +--|---
25# Server | eth1 Address: 100.64.0.2/24
26# | Route: 192.0.2.0/24 via 100.64.0.1
27# +------
28
29{ lib, ... }:
30
31{
32 name = "clatd";
33
34 meta.maintainers = with lib.maintainers; [
35 hax404
36 jmbaur
37 ];
38
39 nodes = {
40 # The server is configured with static IPv4 addresses. RFC 6052 Section 3.1
41 # disallows the mapping of non-global IPv4 addresses like RFC 1918 into the
42 # Well-Known Prefix 64:ff9b::/96. TAYGA also does not allow the mapping of
43 # documentation space (RFC 5737). To circumvent this, 100.64.0.2/24 from
44 # RFC 6589 (Carrier Grade NAT) is used here.
45 # To reach the IPv4 address pool of the NAT64 gateway, there is a static
46 # route configured. In normal cases, where the router would also source NAT
47 # the pool addresses to one IPv4 addresses, this would not be needed.
48 server = {
49 virtualisation.vlans = [
50 2 # towards router
51 ];
52 networking = {
53 useDHCP = false;
54 interfaces.eth1 = lib.mkForce { };
55 };
56 systemd.network = {
57 enable = true;
58 networks."vlan1" = {
59 matchConfig.Name = "eth1";
60 address = [
61 "100.64.0.2/24"
62 ];
63 routes = [
64 {
65 Destination = "192.0.2.0/24";
66 Gateway = "100.64.0.1";
67 }
68 ];
69 };
70 };
71 };
72
73 # The router is configured with static IPv4 addresses towards the server
74 # and IPv6 addresses towards the client. DNS64 is exposed towards the
75 # client so clatd is able to auto-discover the PLAT prefix. For NAT64, the
76 # Well-Known prefix 64:ff9b::/96 is used. NAT64 is done with TAYGA which
77 # provides the tun-interface nat64 and does the translation over it. The
78 # IPv6 packets are sent to this interfaces and received as IPv4 packets and
79 # vice versa. As TAYGA only translates IPv6 addresses to dedicated IPv4
80 # addresses, it needs a pool of IPv4 addresses which must be at least as
81 # big as the expected amount of clients. In this test, the packets from the
82 # pool are directly routed towards the client. In normal cases, there would
83 # be a second source NAT44 to map all clients behind one IPv4 address.
84 router = {
85 boot.kernel.sysctl = {
86 "net.ipv4.conf.all.forwarding" = 1;
87 "net.ipv6.conf.all.forwarding" = 1;
88 };
89
90 virtualisation.vlans = [
91 2 # towards server
92 3 # towards client
93 ];
94
95 networking = {
96 useDHCP = false;
97 useNetworkd = true;
98 firewall.enable = false;
99 interfaces.eth1 = lib.mkForce {
100 ipv4 = {
101 addresses = [
102 {
103 address = "100.64.0.1";
104 prefixLength = 24;
105 }
106 ];
107 };
108 };
109 interfaces.eth2 = lib.mkForce {
110 ipv6 = {
111 addresses = [
112 {
113 address = "2001:db8::1";
114 prefixLength = 64;
115 }
116 ];
117 };
118 };
119 };
120
121 systemd.network.networks."40-eth2" = {
122 networkConfig.IPv6SendRA = true;
123 ipv6Prefixes = [ { Prefix = "2001:db8::/64"; } ];
124 ipv6PREF64Prefixes = [ { Prefix = "64:ff9b::/96"; } ];
125 ipv6SendRAConfig = {
126 EmitDNS = true;
127 DNS = "_link_local";
128 };
129 };
130
131 services.resolved.extraConfig = ''
132 DNSStubListener=no
133 '';
134
135 networking.extraHosts = ''
136 192.0.0.171 ipv4only.arpa
137 192.0.0.170 ipv4only.arpa
138 '';
139
140 services.coredns = {
141 enable = true;
142 config = ''
143 .:53 {
144 bind ::
145 hosts /etc/hosts
146 dns64 64:ff9b::/96
147 }
148 '';
149 };
150
151 services.tayga = {
152 enable = true;
153 ipv4 = {
154 address = "192.0.2.0";
155 router = {
156 address = "192.0.2.1";
157 };
158 pool = {
159 address = "192.0.2.0";
160 prefixLength = 24;
161 };
162 };
163 ipv6 = {
164 address = "2001:db8::1";
165 router = {
166 address = "64:ff9b::1";
167 };
168 pool = {
169 address = "64:ff9b::";
170 prefixLength = 96;
171 };
172 };
173 };
174 };
175
176 # The client uses SLAAC to assign IPv6 addresses. To reach the IPv4-only
177 # server, the client starts the clat daemon which starts and configures the
178 # local IPv4 -> IPv6 translation via Tayga after discovering the PLAT
179 # prefix via DNS64.
180 client =
181 { pkgs, ... }:
182 {
183 virtualisation.vlans = [
184 3 # towards router
185 ];
186
187 networking = {
188 useDHCP = false;
189 interfaces.eth1 = lib.mkForce { };
190 };
191
192 systemd.network = {
193 enable = true;
194 networks."vlan1" = {
195 matchConfig.Name = "eth1";
196 ipv6AcceptRAConfig.UsePREF64 = true;
197 };
198 };
199
200 services.clatd.enable = true;
201
202 environment.systemPackages = [ pkgs.mtr ];
203 };
204 };
205
206 testScript = ''
207 import json
208
209 start_all()
210
211 # wait for all machines to start up
212 for machine in client, router, server:
213 machine.wait_for_unit("network.target")
214
215 with subtest("Wait for tayga and clatd"):
216 router.wait_for_unit("tayga.service")
217 client.wait_for_unit("clatd.service")
218 # clatd checks if this system has IPv4 connectivity for 10 seconds
219 client.wait_until_succeeds(
220 'journalctl -u clatd -e | grep -q "Starting up TAYGA, using config file"'
221 )
222
223 with subtest("networkd exports PREF64 prefix"):
224 assert json.loads(client.succeed("networkctl status eth1 --json=short"))[
225 "NDisc"
226 ]["PREF64"][0]["Prefix"] == [0x0, 0x64, 0xFF, 0x9B] + ([0] * 12)
227
228 with subtest("Test ICMP"):
229 client.wait_until_succeeds("ping -c3 100.64.0.2 >&2")
230
231 with subtest("Test ICMP and show a traceroute"):
232 client.wait_until_succeeds("mtr --show-ips --report-wide 100.64.0.2 >&2")
233
234 client.log(client.execute("systemd-analyze security clatd.service")[1])
235 '';
236}