1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9let
10 cfg = config.services.tayga;
11
12 # Converts an address set to a string
13 strAddr = addr: "${addr.address}/${toString addr.prefixLength}";
14
15 configFile = pkgs.writeText "tayga.conf" ''
16 tun-device ${cfg.tunDevice}
17
18 ipv4-addr ${cfg.ipv4.address}
19 ${optionalString (cfg.ipv6.address != null) "ipv6-addr ${cfg.ipv6.address}"}
20
21 prefix ${strAddr cfg.ipv6.pool}
22 dynamic-pool ${strAddr cfg.ipv4.pool}
23 data-dir ${cfg.dataDir}
24
25 ${concatStringsSep "\n" (mapAttrsToList (ipv4: ipv6: "map " + ipv4 + " " + ipv6) cfg.mappings)}
26
27 ${optionalString ((builtins.length cfg.log) > 0) ''
28 log ${concatStringsSep " " cfg.log}
29 ''}
30
31 wkpf-strict ${if cfg.wkpfStrict then "yes" else "no"}
32 '';
33
34 addrOpts =
35 v:
36 assert v == 4 || v == 6;
37 {
38 options = {
39 address = mkOption {
40 type = types.str;
41 description = "IPv${toString v} address.";
42 };
43
44 prefixLength = mkOption {
45 type = types.ints.between 0 (if v == 4 then 32 else 128);
46 description = ''
47 Subnet mask of the interface, specified as the number of
48 bits in the prefix ("${if v == 4 then "24" else "64"}").
49 '';
50 };
51 };
52 };
53
54 versionOpts = v: {
55 options = {
56 router = {
57 address = mkOption {
58 type = types.str;
59 description = "The IPv${toString v} address of the router.";
60 };
61 };
62
63 address = mkOption {
64 type = types.nullOr types.str;
65 default = null;
66 description = "The source IPv${toString v} address of the TAYGA server.";
67 };
68
69 pool = mkOption {
70 type = with types; nullOr (submodule (addrOpts v));
71 description = "The pool of IPv${toString v} addresses which are used for translation.";
72 };
73 };
74 };
75in
76{
77 options = {
78 services.tayga = {
79 enable = mkEnableOption "Tayga";
80
81 package = mkPackageOption pkgs "tayga" { };
82
83 ipv4 = mkOption {
84 type = types.submodule (versionOpts 4);
85 description = "IPv4-specific configuration.";
86 example = literalExpression ''
87 {
88 address = "192.0.2.0";
89 router = {
90 address = "192.0.2.1";
91 };
92 pool = {
93 address = "192.0.2.1";
94 prefixLength = 24;
95 };
96 }
97 '';
98 };
99
100 ipv6 = mkOption {
101 type = types.submodule (versionOpts 6);
102 description = "IPv6-specific configuration.";
103 example = literalExpression ''
104 {
105 address = "2001:db8::1";
106 router = {
107 address = "64:ff9b::1";
108 };
109 pool = {
110 address = "64:ff9b::";
111 prefixLength = 96;
112 };
113 }
114 '';
115 };
116
117 dataDir = mkOption {
118 type = types.path;
119 default = "/var/lib/tayga";
120 description = "Directory for persistent data.";
121 };
122
123 tunDevice = mkOption {
124 type = types.str;
125 default = "nat64";
126 description = "Name of the nat64 tun device.";
127 };
128
129 mappings = mkOption {
130 type = types.attrsOf types.str;
131 default = { };
132 description = "Static IPv4 -> IPv6 host mappings.";
133 example = literalExpression ''
134 {
135 "192.168.5.42" = "2001:db8:1:4444::1";
136 "192.168.5.43" = "2001:db8:1:4444::2";
137 "192.168.255.2" = "2001:db8:1:569::143";
138 }
139 '';
140 };
141
142 log = mkOption {
143 type = types.listOf types.str;
144 default = [ ];
145 description = "Packet errors to log (drop, reject, icmp, self)";
146 example = literalExpression ''
147 [ "drop" "reject" "icmp" "self" ]
148 '';
149 };
150
151 wkpfStrict = mkOption {
152 type = types.bool;
153 default = true;
154 description = "Enable restrictions on the use of the well-known prefix (64:ff9b::/96) - prevents translation of non-global IPv4 ranges when using the well-known prefix. Must be enabled for RFC 6052 compatibility.";
155 };
156 };
157 };
158
159 config = mkIf cfg.enable {
160 assertions = [
161 {
162 assertion = allUnique (attrValues cfg.mappings);
163 message = "Neither the IPv4 nor the IPv6 addresses must be entered twice in the mappings.";
164 }
165 ];
166
167 networking.interfaces."${cfg.tunDevice}" = {
168 virtual = true;
169 virtualType = "tun";
170 virtualOwner = mkIf config.networking.useNetworkd "";
171 ipv4 = {
172 addresses = [
173 {
174 address = cfg.ipv4.router.address;
175 prefixLength = 32;
176 }
177 ];
178 routes = [
179 cfg.ipv4.pool
180 ];
181 };
182 ipv6 = {
183 addresses = [
184 {
185 address = cfg.ipv6.router.address;
186 prefixLength = 128;
187 }
188 ];
189 routes = [
190 cfg.ipv6.pool
191 ];
192 };
193 };
194
195 environment.etc."tayga.conf".source = configFile;
196
197 systemd.services.tayga = {
198 description = "Stateless NAT64 implementation";
199 wantedBy = [ "multi-user.target" ];
200 after = [ "network.target" ];
201
202 reloadTriggers = [ configFile ];
203 serviceConfig = {
204 ExecStart = "${cfg.package}/bin/tayga -d --nodetach --config /etc/tayga.conf";
205 ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID";
206 Restart = "always";
207
208 # Hardening Score:
209 # - nixos-scripts: 2.1
210 # - systemd-networkd: 1.6
211 ProtectHome = true;
212 SystemCallFilter = [
213 "@network-io"
214 "@system-service"
215 "~@privileged"
216 "~@resources"
217 ];
218 ProtectKernelLogs = true;
219 AmbientCapabilities = [
220 "CAP_NET_ADMIN"
221 ];
222 CapabilityBoundingSet = "";
223 RestrictAddressFamilies = [
224 "AF_INET"
225 "AF_INET6"
226 "AF_NETLINK"
227 ];
228 StateDirectory = "tayga";
229 DynamicUser = mkIf config.networking.useNetworkd true;
230 MemoryDenyWriteExecute = true;
231 RestrictRealtime = true;
232 RestrictSUIDSGID = true;
233 ProtectHostname = true;
234 ProtectKernelModules = true;
235 ProtectKernelTunables = true;
236 RestrictNamespaces = true;
237 NoNewPrivileges = true;
238 ProtectControlGroups = true;
239 SystemCallArchitectures = "native";
240 PrivateTmp = true;
241 LockPersonality = true;
242 ProtectSystem = true;
243 PrivateUsers = true;
244 ProtectProc = "invisible";
245 };
246 };
247 };
248}