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