This repository has no description
5.8 kB
199 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
133 krebs.iptables.tables.filter.FORWARD.rules = [
134 # IPv6 forwarding for containers
135 {
136 v4 = false;
137 v6 = true;
138 predicate = "-i ctr0";
139 target = "ACCEPT";
140 }
141 {
142 v4 = false;
143 v6 = true;
144 predicate = "-o ctr0 -m conntrack --ctstate RELATED,ESTABLISHED";
145 target = "ACCEPT";
146 }
147 # IPv4 forwarding for NAT64 translated packets
148 {
149 v4 = true;
150 v6 = false;
151 predicate = "-m conntrack --ctstate RELATED,ESTABLISHED";
152 target = "ACCEPT";
153 }
154 {
155 v4 = true;
156 v6 = false;
157 predicate = "";
158 target = "ACCEPT";
159 }
160 ];
161
162 # NAT66 - masquerade container IPv6 to reach public IPv6 (when host has IPv6)
163 # Note: No IPv4 MASQUERADE needed - jool does SNAT with pool4 address for NAT64 traffic
164 krebs.iptables.tables.nat.POSTROUTING.rules = [
165 {
166 v4 = false;
167 v6 = true;
168 predicate = "-s fd00:c700::/64 ! -d fd00:c700::/64";
169 target = "MASQUERADE";
170 }
171 ];
172
173 # Allow containers to reach host's DNS64 resolver (IPv6 only)
174 krebs.iptables.tables.filter.INPUT.rules = [
175 {
176 v4 = false;
177 v6 = true;
178 predicate = "-s fd00:c700::/64 -p udp --dport 53";
179 target = "ACCEPT";
180 }
181 {
182 v4 = false;
183 v6 = true;
184 predicate = "-s fd00:c700::/64 -p tcp --dport 53";
185 target = "ACCEPT";
186 }
187 ];
188
189 # Send only container traffic to Jool NAT64 (--iptables mode)
190 # Host traffic to 64:ff9b::/96 passes through to upstream NAT64 (e.g., Android hotspot)
191 krebs.iptables.tables.mangle.PREROUTING.rules = [
192 {
193 v4 = false;
194 v6 = true;
195 predicate = "-i ctr0 -d 64:ff9b::/96";
196 target = "JOOL";
197 }
198 ];
199}