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 = "Authorized password to the opposite end of the tunnel.";
17 };
18 login = mkOption {
19 default = "";
20 type = types.str;
21 description = "(optional) name your peer has for you";
22 };
23 peerName = mkOption {
24 default = "";
25 type = types.str;
26 description = "(optional) human-readable name for peer";
27 };
28 publicKey = mkOption {
29 type = types.str;
30 description = "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 = "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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 Bind to this device for native ethernet operation.
175 `all` is a pseudo-name which will try to connect to all devices.
176 '';
177 };
178
179 beacon = mkOption {
180 type = types.int;
181 default = 2;
182 description = ''
183 Auto-connect to other cjdns nodes on the same network.
184 Options:
185 0: Disabled.
186 1: Accept beacons, this will cause cjdns to accept incoming
187 beacon messages and try connecting to the sender.
188 2: Accept and send beacons, this will cause cjdns to broadcast
189 messages on the local network which contain a randomly
190 generated per-session password, other nodes which have this
191 set to 1 or 2 will hear the beacon messages and connect
192 automatically.
193 '';
194 };
195
196 connectTo = mkOption {
197 type = types.attrsOf ( types.submodule ( connectToSubmodule ) );
198 default = { };
199 example = literalExpression ''
200 {
201 "01:02:03:04:05:06" = {
202 hostname = "homer.hype";
203 password = "5kG15EfpdcKNX3f2GSQ0H1HC7yIfxoCoImnO5FHM";
204 publicKey = "371zpkgs8ss387tmr81q04mp0hg1skb51hw34vk1cq644mjqhup0.k";
205 };
206 }
207 '';
208 description = ''
209 Credentials for connecting look similar to UDP credientials
210 except they begin with the mac address.
211 '';
212 };
213 };
214
215 addExtraHosts = mkOption {
216 type = types.bool;
217 default = false;
218 description = ''
219 Whether to add cjdns peers with an associated hostname to
220 {file}`/etc/hosts`. Beware that enabling this
221 incurs heavy eval-time costs.
222 '';
223 };
224
225 };
226
227 };
228
229 config = mkIf cfg.enable {
230
231 boot.kernelModules = [ "tun" ];
232
233 # networking.firewall.allowedUDPPorts = ...
234
235 systemd.services.cjdns = {
236 description = "cjdns: routing engine designed for security, scalability, speed and ease of use";
237 wantedBy = [ "multi-user.target" "sleep.target"];
238 after = [ "network-online.target" ];
239 bindsTo = [ "network-online.target" ];
240
241 preStart = optionalString (cfg.confFile == null) ''
242 [ -e /etc/cjdns.keys ] && source /etc/cjdns.keys
243
244 if [ -z "$CJDNS_PRIVATE_KEY" ]; then
245 shopt -s lastpipe
246 ${pkg}/bin/makekeys | { read private ipv6 public; }
247
248 install -m 600 <(echo "CJDNS_PRIVATE_KEY=$private") /etc/cjdns.keys
249 install -m 444 <(echo -e "CJDNS_IPV6=$ipv6\nCJDNS_PUBLIC_KEY=$public") /etc/cjdns.public
250 fi
251
252 if [ -z "$CJDNS_ADMIN_PASSWORD" ]; then
253 echo "CJDNS_ADMIN_PASSWORD=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 32)" \
254 >> /etc/cjdns.keys
255 fi
256 '';
257
258 script = (
259 if cfg.confFile != null then "${pkg}/bin/cjdroute < ${cfg.confFile}" else
260 ''
261 source /etc/cjdns.keys
262 (cat <<'EOF'
263 ${cjdrouteConf}
264 EOF
265 ) | sed \
266 -e "s/@CJDNS_ADMIN_PASSWORD@/$CJDNS_ADMIN_PASSWORD/g" \
267 -e "s/@CJDNS_PRIVATE_KEY@/$CJDNS_PRIVATE_KEY/g" \
268 | ${pkg}/bin/cjdroute
269 ''
270 );
271
272 startLimitIntervalSec = 0;
273 serviceConfig = {
274 Type = "forking";
275 Restart = "always";
276 RestartSec = 1;
277 CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_RAW CAP_SETUID";
278 ProtectSystem = true;
279 # Doesn't work on i686, causing service to fail
280 MemoryDenyWriteExecute = !pkgs.stdenv.isi686;
281 ProtectHome = true;
282 PrivateTmp = true;
283 };
284 };
285
286 networking.hostFiles = mkIf cfg.addExtraHosts [ cjdnsExtraHosts ];
287
288 assertions = [
289 { assertion = ( cfg.ETHInterface.bind != "" || cfg.UDPInterface.bind != "" || cfg.confFile != null );
290 message = "Neither cjdns.ETHInterface.bind nor cjdns.UDPInterface.bind defined.";
291 }
292 { assertion = config.networking.enableIPv6;
293 message = "networking.enableIPv6 must be enabled for CJDNS to work";
294 }
295 ];
296
297 };
298
299}