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