1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9
10let
11 cfg = config.services.ndppd;
12
13 render = s: f: concatStringsSep "\n" (mapAttrsToList f s);
14 prefer = a: b: if a != null then a else b;
15
16 ndppdConf = prefer cfg.configFile (
17 pkgs.writeText "ndppd.conf" ''
18 route-ttl ${toString cfg.routeTTL}
19 ${render cfg.proxies (
20 proxyInterfaceName: proxy: ''
21 proxy ${prefer proxy.interface proxyInterfaceName} {
22 router ${boolToString proxy.router}
23 timeout ${toString proxy.timeout}
24 ttl ${toString proxy.ttl}
25 ${render proxy.rules (
26 ruleNetworkName: rule: ''
27 rule ${prefer rule.network ruleNetworkName} {
28 ${rule.method}${optionalString (rule.method == "iface") " ${rule.interface}"}
29 }''
30 )}
31 }''
32 )}
33 ''
34 );
35
36 proxy = types.submodule {
37 options = {
38 interface = mkOption {
39 type = types.nullOr types.str;
40 description = ''
41 Listen for any Neighbor Solicitation messages on this interface,
42 and respond to them according to a set of rules.
43 Defaults to the name of the attrset.
44 '';
45 default = null;
46 };
47 router = mkOption {
48 type = types.bool;
49 description = ''
50 Turns on or off the router flag for Neighbor Advertisement Messages.
51 '';
52 default = true;
53 };
54 timeout = mkOption {
55 type = types.int;
56 description = ''
57 Controls how long to wait for a Neighbor Advertisement Message before
58 invalidating the entry, in milliseconds.
59 '';
60 default = 500;
61 };
62 ttl = mkOption {
63 type = types.int;
64 description = ''
65 Controls how long a valid or invalid entry remains in the cache, in
66 milliseconds.
67 '';
68 default = 30000;
69 };
70 rules = mkOption {
71 type = types.attrsOf rule;
72 description = ''
73 This is a rule that the target address is to match against. If no netmask
74 is provided, /128 is assumed. You may have several rule sections, and the
75 addresses may or may not overlap.
76 '';
77 default = { };
78 };
79 };
80 };
81
82 rule = types.submodule {
83 options = {
84 network = mkOption {
85 type = types.nullOr types.str;
86 description = ''
87 This is the target address is to match against. If no netmask
88 is provided, /128 is assumed. The addresses of several rules
89 may or may not overlap.
90 Defaults to the name of the attrset.
91 '';
92 default = null;
93 };
94 method = mkOption {
95 type = types.enum [
96 "static"
97 "iface"
98 "auto"
99 ];
100 description = ''
101 static: Immediately answer any Neighbor Solicitation Messages
102 (if they match the IP rule).
103 iface: Forward the Neighbor Solicitation Message through the specified
104 interface and only respond if a matching Neighbor Advertisement
105 Message is received.
106 auto: Same as iface, but instead of manually specifying the outgoing
107 interface, check for a matching route in /proc/net/ipv6_route.
108 '';
109 default = "auto";
110 };
111 interface = mkOption {
112 type = types.nullOr types.str;
113 description = "Interface to use when method is iface.";
114 default = null;
115 };
116 };
117 };
118
119in
120{
121 options.services.ndppd = {
122 enable = mkEnableOption "daemon that proxies NDP (Neighbor Discovery Protocol) messages between interfaces";
123 interface = mkOption {
124 type = types.nullOr types.str;
125 description = ''
126 Interface which is on link-level with router.
127 (Legacy option, use services.ndppd.proxies.\<interface\>.rules.\<network\> instead)
128 '';
129 default = null;
130 example = "eth0";
131 };
132 network = mkOption {
133 type = types.nullOr types.str;
134 description = ''
135 Network that we proxy.
136 (Legacy option, use services.ndppd.proxies.\<interface\>.rules.\<network\> instead)
137 '';
138 default = null;
139 example = "1111::/64";
140 };
141 configFile = mkOption {
142 type = types.nullOr types.path;
143 description = "Path to configuration file.";
144 default = null;
145 };
146 routeTTL = mkOption {
147 type = types.int;
148 description = ''
149 This tells 'ndppd' how often to reload the route file /proc/net/ipv6_route,
150 in milliseconds.
151 '';
152 default = 30000;
153 };
154 proxies = mkOption {
155 type = types.attrsOf proxy;
156 description = ''
157 This sets up a listener, that will listen for any Neighbor Solicitation
158 messages, and respond to them according to a set of rules.
159 '';
160 default = { };
161 example = literalExpression ''
162 {
163 eth0.rules."1111::/64" = {};
164 }
165 '';
166 };
167 };
168
169 config = mkIf cfg.enable {
170 warnings = mkIf (cfg.interface != null && cfg.network != null) [
171 ''
172 The options services.ndppd.interface and services.ndppd.network will probably be removed soon,
173 please use services.ndppd.proxies.<interface>.rules.<network> instead.
174 ''
175 ];
176
177 services.ndppd.proxies = mkIf (cfg.interface != null && cfg.network != null) {
178 ${cfg.interface}.rules.${cfg.network} = { };
179 };
180
181 systemd.services.ndppd = {
182 description = "NDP Proxy Daemon";
183 documentation = [
184 "man:ndppd(1)"
185 "man:ndppd.conf(5)"
186 ];
187 after = [ "network-pre.target" ];
188 wantedBy = [ "multi-user.target" ];
189 serviceConfig = {
190 ExecStart = "${pkgs.ndppd}/bin/ndppd -c ${ndppdConf}";
191
192 # Sandboxing
193 CapabilityBoundingSet = "CAP_NET_RAW CAP_NET_ADMIN";
194 ProtectSystem = "strict";
195 ProtectHome = true;
196 PrivateTmp = true;
197 PrivateDevices = true;
198 ProtectKernelTunables = true;
199 ProtectKernelModules = true;
200 ProtectControlGroups = true;
201 RestrictAddressFamilies = "AF_INET6 AF_PACKET AF_NETLINK";
202 RestrictNamespaces = true;
203 LockPersonality = true;
204 MemoryDenyWriteExecute = true;
205 RestrictRealtime = true;
206 RestrictSUIDSGID = true;
207 };
208 };
209 };
210}