1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6
7 pkg = pkgs.cjdns;
8
9 cfg = config.services.cjdns;
10
11 connectToSubmodule =
12 { ... }:
13 { options =
14 { password = mkOption {
15 type = types.str;
16 description = lib.mdDoc "Authorized password to the opposite end of the tunnel.";
17 };
18 login = mkOption {
19 default = "";
20 type = types.str;
21 description = lib.mdDoc "(optional) name your peer has for you";
22 };
23 peerName = mkOption {
24 default = "";
25 type = types.str;
26 description = lib.mdDoc "(optional) human-readable name for peer";
27 };
28 publicKey = mkOption {
29 type = types.str;
30 description = lib.mdDoc "Public key at the opposite end of the tunnel.";
31 };
32 hostname = mkOption {
33 default = "";
34 example = "foobar.hype";
35 type = types.str;
36 description = lib.mdDoc "Optional hostname to add to /etc/hosts; prevents reverse lookup failures.";
37 };
38 };
39 };
40
41 # Additional /etc/hosts entries for peers with an associated hostname
42 cjdnsExtraHosts = pkgs.runCommand "cjdns-hosts" {} ''
43 exec >$out
44 ${concatStringsSep "\n" (mapAttrsToList (k: v:
45 optionalString (v.hostname != "")
46 "echo $(${pkgs.cjdns}/bin/publictoip6 ${v.publicKey}) ${v.hostname}")
47 (cfg.ETHInterface.connectTo // cfg.UDPInterface.connectTo))}
48 '';
49
50 parseModules = x:
51 x // { connectTo = mapAttrs (name: value: { inherit (value) password publicKey; }) x.connectTo; };
52
53 cjdrouteConf = builtins.toJSON ( recursiveUpdate {
54 admin = {
55 bind = cfg.admin.bind;
56 password = "@CJDNS_ADMIN_PASSWORD@";
57 };
58 authorizedPasswords = map (p: { password = p; }) cfg.authorizedPasswords;
59 interfaces = {
60 ETHInterface = if (cfg.ETHInterface.bind != "") then [ (parseModules cfg.ETHInterface) ] else [ ];
61 UDPInterface = if (cfg.UDPInterface.bind != "") then [ (parseModules cfg.UDPInterface) ] else [ ];
62 };
63
64 privateKey = "@CJDNS_PRIVATE_KEY@";
65
66 resetAfterInactivitySeconds = 100;
67
68 router = {
69 interface = { type = "TUNInterface"; };
70 ipTunnel = {
71 allowedConnections = [];
72 outgoingConnections = [];
73 };
74 };
75
76 security = [ { exemptAngel = 1; setuser = "nobody"; } ];
77
78 } cfg.extraConfig);
79
80in
81
82{
83 options = {
84
85 services.cjdns = {
86
87 enable = mkOption {
88 type = types.bool;
89 default = false;
90 description = lib.mdDoc ''
91 Whether to enable the cjdns network encryption
92 and routing engine. A file at /etc/cjdns.keys will
93 be created if it does not exist to contain a random
94 secret key that your IPv6 address will be derived from.
95 '';
96 };
97
98 extraConfig = mkOption {
99 type = types.attrs;
100 default = {};
101 example = { router.interface.tunDevice = "tun10"; };
102 description = lib.mdDoc ''
103 Extra configuration, given as attrs, that will be merged recursively
104 with the rest of the JSON generated by this module, at the root node.
105 '';
106 };
107
108 confFile = mkOption {
109 type = types.nullOr types.path;
110 default = null;
111 example = "/etc/cjdroute.conf";
112 description = lib.mdDoc ''
113 Ignore all other cjdns options and load configuration from this file.
114 '';
115 };
116
117 authorizedPasswords = mkOption {
118 type = types.listOf types.str;
119 default = [ ];
120 example = [
121 "snyrfgkqsc98qh1y4s5hbu0j57xw5s0"
122 "z9md3t4p45mfrjzdjurxn4wuj0d8swv"
123 "49275fut6tmzu354pq70sr5b95qq0vj"
124 ];
125 description = lib.mdDoc ''
126 Any remote cjdns nodes that offer these passwords on
127 connection will be allowed to route through this node.
128 '';
129 };
130
131 admin = {
132 bind = mkOption {
133 type = types.str;
134 default = "127.0.0.1:11234";
135 description = lib.mdDoc ''
136 Bind the administration port to this address and port.
137 '';
138 };
139 };
140
141 UDPInterface = {
142 bind = mkOption {
143 type = types.str;
144 default = "";
145 example = "192.168.1.32:43211";
146 description = lib.mdDoc ''
147 Address and port to bind UDP tunnels to.
148 '';
149 };
150 connectTo = mkOption {
151 type = types.attrsOf ( types.submodule ( connectToSubmodule ) );
152 default = { };
153 example = literalExpression ''
154 {
155 "192.168.1.1:27313" = {
156 hostname = "homer.hype";
157 password = "5kG15EfpdcKNX3f2GSQ0H1HC7yIfxoCoImnO5FHM";
158 publicKey = "371zpkgs8ss387tmr81q04mp0hg1skb51hw34vk1cq644mjqhup0.k";
159 };
160 }
161 '';
162 description = lib.mdDoc ''
163 Credentials for making UDP tunnels.
164 '';
165 };
166 };
167
168 ETHInterface = {
169 bind = mkOption {
170 type = types.str;
171 default = "";
172 example = "eth0";
173 description =
174 lib.mdDoc ''
175 Bind to this device for native ethernet operation.
176 `all` is a pseudo-name which will try to connect to all devices.
177 '';
178 };
179
180 beacon = mkOption {
181 type = types.int;
182 default = 2;
183 description = lib.mdDoc ''
184 Auto-connect to other cjdns nodes on the same network.
185 Options:
186 0: Disabled.
187 1: Accept beacons, this will cause cjdns to accept incoming
188 beacon messages and try connecting to the sender.
189 2: Accept and send beacons, this will cause cjdns to broadcast
190 messages on the local network which contain a randomly
191 generated per-session password, other nodes which have this
192 set to 1 or 2 will hear the beacon messages and connect
193 automatically.
194 '';
195 };
196
197 connectTo = mkOption {
198 type = types.attrsOf ( types.submodule ( connectToSubmodule ) );
199 default = { };
200 example = literalExpression ''
201 {
202 "01:02:03:04:05:06" = {
203 hostname = "homer.hype";
204 password = "5kG15EfpdcKNX3f2GSQ0H1HC7yIfxoCoImnO5FHM";
205 publicKey = "371zpkgs8ss387tmr81q04mp0hg1skb51hw34vk1cq644mjqhup0.k";
206 };
207 }
208 '';
209 description = lib.mdDoc ''
210 Credentials for connecting look similar to UDP credientials
211 except they begin with the mac address.
212 '';
213 };
214 };
215
216 addExtraHosts = mkOption {
217 type = types.bool;
218 default = false;
219 description = lib.mdDoc ''
220 Whether to add cjdns peers with an associated hostname to
221 {file}`/etc/hosts`. Beware that enabling this
222 incurs heavy eval-time costs.
223 '';
224 };
225
226 };
227
228 };
229
230 config = mkIf cfg.enable {
231
232 boot.kernelModules = [ "tun" ];
233
234 # networking.firewall.allowedUDPPorts = ...
235
236 systemd.services.cjdns = {
237 description = "cjdns: routing engine designed for security, scalability, speed and ease of use";
238 wantedBy = [ "multi-user.target" "sleep.target"];
239 after = [ "network-online.target" ];
240 bindsTo = [ "network-online.target" ];
241
242 preStart = if cfg.confFile != null then "" else ''
243 [ -e /etc/cjdns.keys ] && source /etc/cjdns.keys
244
245 if [ -z "$CJDNS_PRIVATE_KEY" ]; then
246 shopt -s lastpipe
247 ${pkg}/bin/makekeys | { read private ipv6 public; }
248
249 umask 0077
250 echo "CJDNS_PRIVATE_KEY=$private" >> /etc/cjdns.keys
251 echo -e "CJDNS_IPV6=$ipv6\nCJDNS_PUBLIC_KEY=$public" > /etc/cjdns.public
252
253 chmod 600 /etc/cjdns.keys
254 chmod 444 /etc/cjdns.public
255 fi
256
257 if [ -z "$CJDNS_ADMIN_PASSWORD" ]; then
258 echo "CJDNS_ADMIN_PASSWORD=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 32)" \
259 >> /etc/cjdns.keys
260 fi
261 '';
262
263 script = (
264 if cfg.confFile != null then "${pkg}/bin/cjdroute < ${cfg.confFile}" else
265 ''
266 source /etc/cjdns.keys
267 (cat <<'EOF'
268 ${cjdrouteConf}
269 EOF
270 ) | sed \
271 -e "s/@CJDNS_ADMIN_PASSWORD@/$CJDNS_ADMIN_PASSWORD/g" \
272 -e "s/@CJDNS_PRIVATE_KEY@/$CJDNS_PRIVATE_KEY/g" \
273 | ${pkg}/bin/cjdroute
274 ''
275 );
276
277 startLimitIntervalSec = 0;
278 serviceConfig = {
279 Type = "forking";
280 Restart = "always";
281 RestartSec = 1;
282 CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_RAW CAP_SETUID";
283 ProtectSystem = true;
284 # Doesn't work on i686, causing service to fail
285 MemoryDenyWriteExecute = !pkgs.stdenv.isi686;
286 ProtectHome = true;
287 PrivateTmp = true;
288 };
289 };
290
291 networking.hostFiles = mkIf cfg.addExtraHosts [ cjdnsExtraHosts ];
292
293 assertions = [
294 { assertion = ( cfg.ETHInterface.bind != "" || cfg.UDPInterface.bind != "" || cfg.confFile != null );
295 message = "Neither cjdns.ETHInterface.bind nor cjdns.UDPInterface.bind defined.";
296 }
297 { assertion = config.networking.enableIPv6;
298 message = "networking.enableIPv6 must be enabled for CJDNS to work";
299 }
300 ];
301
302 };
303
304}