This repository has no description
5.9 kB
181 lines
1{ config, pkgs, ... }:
2{
3 # Local DNS64 resolver for containers
4 # Synthesizes AAAA records with 64:ff9b::/96 prefix for IPv4-only hosts
5 services.unbound = {
6 enable = true;
7 settings = {
8 server = {
9 interface = [
10 "fd00:c700::1"
11 "127.0.0.1"
12 ];
13 access-control = [
14 "fd00:c700::/64 allow"
15 "127.0.0.0/8 allow"
16 ];
17 # DNS64: synthesize AAAA with NAT64 prefix
18 module-config = ''"dns64 validator iterator"'';
19 dns64-prefix = "64:ff9b::/96";
20 # Always synthesize, even if real AAAA exists (needed when host has no IPv6)
21 dns64-synthall = "yes";
22 # Handle reverse DNS for NAT64 prefix locally (return NXDOMAIN immediately)
23 # Without this, PTR lookups for 64:ff9b::* timeout waiting for upstream
24 local-zone = ''"0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.b.9.f.f.4.6.0.0.ip6.arpa." static'';
25 };
26 forward-zone = [
27 {
28 name = ".";
29 forward-addr = [
30 "1.1.1.1" # Cloudflare (IPv4 - always works)
31 "8.8.8.8" # Google (IPv4)
32 "2606:4700:4700::1111" # Cloudflare (IPv6 - when available)
33 ];
34 }
35 ];
36 };
37 };
38
39 # Bridge interface for containers
40 systemd.network.netdevs."10-ctr0" = {
41 netdevConfig = {
42 Kind = "bridge";
43 Name = "ctr0";
44 };
45 };
46
47 systemd.network.networks."10-ctr0" = {
48 matchConfig.Name = "ctr0";
49 address = [ "fd00:c700::1/64" ];
50 networkConfig = {
51 ConfigureWithoutCarrier = true;
52 IPv6SendRA = true; # Enable Router Advertisements
53 };
54
55 # Router Advertisement configuration
56 ipv6SendRAConfig = {
57 Managed = false; # No DHCPv6 for addresses (use SLAAC)
58 OtherInformation = false; # No DHCPv6 for DNS (use RDNSS)
59 RouterLifetimeSec = 1800;
60 DNS = [ "fd00:c700::1" ]; # RDNSS: advertise DNS64 resolver
61 };
62 ipv6Prefixes = [
63 { Prefix = "fd00:c700::/64"; }
64 ];
65 # Advertise NAT64 prefix via PREF64 option (RFC 8781)
66 ipv6PREF64Prefixes = [
67 { Prefix = "64:ff9b::/96"; }
68 ];
69 };
70
71 # Exclude from NetworkManager
72 networking.networkmanager.unmanaged = [ "ctr0" ];
73
74 # IP forwarding
75 boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = 1;
76 boot.kernel.sysctl."net.ipv4.ip_forward" = 1;
77
78 # Jool NAT64 kernel module
79 boot.extraModulePackages = [ config.boot.kernelPackages.jool ];
80 boot.kernelModules = [ "jool" ];
81
82 systemd.services.jool = {
83 description = "Jool NAT64";
84 after = [ "network.target" ];
85 wantedBy = [ "multi-user.target" ];
86 serviceConfig = {
87 Type = "oneshot";
88 RemainAfterExit = true;
89 # Use --iptables mode so we can control which packets Jool processes
90 # (--netfilter mode intercepts ALL traffic to pool6, breaking upstream NAT64)
91 ExecStart = "${pkgs.jool-cli}/bin/jool instance add --iptables --pool6 64:ff9b::/96";
92 ExecStop = "${pkgs.jool-cli}/bin/jool instance remove";
93 };
94 };
95
96 # Update Jool pool4 with current IPv4 address (needed for NAT64 on dynamic IPs)
97 systemd.services.jool-pool4-update = {
98 description = "Update Jool NAT64 pool4 with current IPv4";
99 after = [
100 "jool.service"
101 "network-online.target"
102 ];
103 wants = [ "network-online.target" ];
104 wantedBy = [ "multi-user.target" ];
105 path = [
106 pkgs.iproute2
107 pkgs.gawk
108 pkgs.jool-cli
109 ];
110 serviceConfig = {
111 Type = "oneshot";
112 RemainAfterExit = true;
113 };
114 script = ''
115 # Get default route's source IP
116 IP=$(ip -4 route get 1.1.1.1 2>/dev/null | awk '/src/ {print $7}')
117 if [ -n "$IP" ]; then
118 # Flush old pool and add new
119 jool pool4 flush --tcp 2>/dev/null || true
120 jool pool4 flush --udp 2>/dev/null || true
121 jool pool4 flush --icmp 2>/dev/null || true
122 jool pool4 add "$IP" 1-65535 --tcp
123 jool pool4 add "$IP" 1-65535 --udp
124 jool pool4 add "$IP" 1-65535 --icmp
125 echo "Jool pool4 updated to $IP"
126 else
127 echo "No IPv4 address found, skipping pool4 update"
128 fi
129 '';
130 };
131
132 # Firewall rules for container bridge: IPv6 from the containers, and the
133 # IPv4 side of NAT64 (packets Jool translated).
134 networking.firewall.extraForwardRules = ''
135 iifname "ctr0" meta nfproto ipv6 accept
136 meta nfproto ipv4 accept
137 '';
138
139 # Allow containers to reach host's DNS64 resolver (IPv6 only)
140 networking.firewall.extraInputRules = ''
141 ip6 saddr fd00:c700::/64 meta l4proto { tcp, udp } th dport 53 accept
142 '';
143
144 # NAT66 - masquerade container IPv6 to reach public IPv6 (when host has IPv6)
145 # Note: No IPv4 MASQUERADE needed - jool does SNAT with pool4 address for NAT64 traffic
146 networking.nftables.tables.container-bridge = {
147 family = "ip6";
148 content = ''
149 chain postrouting {
150 type nat hook postrouting priority srcnat; policy accept;
151 ip6 saddr fd00:c700::/64 ip6 daddr != fd00:c700::/64 masquerade
152 }
153 '';
154 };
155
156 # Send only container traffic to Jool NAT64 (--iptables mode)
157 # Host traffic to 64:ff9b::/96 passes through to upstream NAT64 (e.g., Android hotspot)
158 # JOOL is an xtables target, nftables has no equivalent; ip6tables (the
159 # nft backend) loads it through nft_compat into the ip6 mangle table.
160 systemd.services.jool-iptables =
161 let
162 rule = "PREROUTING -i ctr0 -d 64:ff9b::/96 -j JOOL";
163 in
164 {
165 description = "Send container traffic to Jool NAT64";
166 after = [
167 "jool.service"
168 "nftables.service"
169 ];
170 requires = [ "jool.service" ];
171 wantedBy = [ "multi-user.target" ];
172 path = [ pkgs.iptables ];
173 environment.XTABLES_LIBDIR = "${pkgs.jool-cli}/lib/xtables";
174 serviceConfig = {
175 Type = "oneshot";
176 RemainAfterExit = true;
177 };
178 script = "ip6tables -t mangle -C ${rule} 2>/dev/null || ip6tables -t mangle -A ${rule}";
179 preStop = "ip6tables -t mangle -D ${rule} || true";
180 };
181}