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