{ config, lib, pkgs, ... }: with lib; let pkg = pkgs.cjdns; cfg = config.services.cjdns; connectToSubmodule = { options, ... }: { options = { password = mkOption { type = types.str; description = "Authorized password to the opposite end of the tunnel."; }; publicKey = mkOption { type = types.str; description = "Public key at the opposite end of the tunnel."; }; hostname = mkOption { default = ""; example = "foobar.hype"; type = types.str; description = "Optional hostname to add to /etc/hosts; prevents reverse lookup failures."; }; }; }; # Additional /etc/hosts entries for peers with an associated hostname cjdnsExtraHosts = import (pkgs.runCommand "cjdns-hosts" {} # Generate a builder that produces an output usable as a Nix string value '' exec >$out echo \'\' ${concatStringsSep "\n" (mapAttrsToList (k: v: optionalString (v.hostname != "") "echo $(${pkgs.cjdns}/bin/publictoip6 ${v.publicKey}) ${v.hostname}") (cfg.ETHInterface.connectTo // cfg.UDPInterface.connectTo))} echo \'\' ''); parseModules = x: x // { connectTo = mapAttrs (name: value: { inherit (value) password publicKey; }) x.connectTo; }; # would be nice to merge 'cfg' with a //, # but the json nesting is wacky. cjdrouteConf = builtins.toJSON ( { admin = { bind = cfg.admin.bind; password = "@CJDNS_ADMIN_PASSWORD@"; }; authorizedPasswords = map (p: { password = p; }) cfg.authorizedPasswords; interfaces = { ETHInterface = if (cfg.ETHInterface.bind != "") then [ (parseModules cfg.ETHInterface) ] else [ ]; UDPInterface = if (cfg.UDPInterface.bind != "") then [ (parseModules cfg.UDPInterface) ] else [ ]; }; privateKey = "@CJDNS_PRIVATE_KEY@"; resetAfterInactivitySeconds = 100; router = { interface = { type = "TUNInterface"; }; ipTunnel = { allowedConnections = []; outgoingConnections = []; }; }; security = [ { exemptAngel = 1; setuser = "nobody"; } ]; }); in { options = { services.cjdns = { enable = mkOption { type = types.bool; default = false; description = '' Whether to enable the cjdns network encryption and routing engine. A file at /etc/cjdns.keys will be created if it does not exist to contain a random secret key that your IPv6 address will be derived from. ''; }; confFile = mkOption { type = types.nullOr types.path; default = null; example = "/etc/cjdroute.conf"; description = '' Ignore all other cjdns options and load configuration from this file. ''; }; authorizedPasswords = mkOption { type = types.listOf types.str; default = [ ]; example = [ "snyrfgkqsc98qh1y4s5hbu0j57xw5s0" "z9md3t4p45mfrjzdjurxn4wuj0d8swv" "49275fut6tmzu354pq70sr5b95qq0vj" ]; description = '' Any remote cjdns nodes that offer these passwords on connection will be allowed to route through this node. ''; }; admin = { bind = mkOption { type = types.str; default = "127.0.0.1:11234"; description = '' Bind the administration port to this address and port. ''; }; }; UDPInterface = { bind = mkOption { type = types.str; default = ""; example = "192.168.1.32:43211"; description = '' Address and port to bind UDP tunnels to. ''; }; connectTo = mkOption { type = types.attrsOf ( types.submodule ( connectToSubmodule ) ); default = { }; example = { "192.168.1.1:27313" = { hostname = "homer.hype"; password = "5kG15EfpdcKNX3f2GSQ0H1HC7yIfxoCoImnO5FHM"; publicKey = "371zpkgs8ss387tmr81q04mp0hg1skb51hw34vk1cq644mjqhup0.k"; }; }; description = '' Credentials for making UDP tunnels. ''; }; }; ETHInterface = { bind = mkOption { type = types.str; default = ""; example = "eth0"; description = '' Bind to this device for native ethernet operation. all is a pseudo-name which will try to connect to all devices. ''; }; beacon = mkOption { type = types.int; default = 2; description = '' Auto-connect to other cjdns nodes on the same network. Options: 0: Disabled. 1: Accept beacons, this will cause cjdns to accept incoming beacon messages and try connecting to the sender. 2: Accept and send beacons, this will cause cjdns to broadcast messages on the local network which contain a randomly generated per-session password, other nodes which have this set to 1 or 2 will hear the beacon messages and connect automatically. ''; }; connectTo = mkOption { type = types.attrsOf ( types.submodule ( connectToSubmodule ) ); default = { }; example = { "01:02:03:04:05:06" = { hostname = "homer.hype"; password = "5kG15EfpdcKNX3f2GSQ0H1HC7yIfxoCoImnO5FHM"; publicKey = "371zpkgs8ss387tmr81q04mp0hg1skb51hw34vk1cq644mjqhup0.k"; }; }; description = '' Credentials for connecting look similar to UDP credientials except they begin with the mac address. ''; }; }; addExtraHosts = mkOption { type = types.bool; default = false; description = '' Whether to add cjdns peers with an associated hostname to /etc/hosts. Beware that enabling this incurs heavy eval-time costs. ''; }; }; }; config = mkIf cfg.enable { boot.kernelModules = [ "tun" ]; # networking.firewall.allowedUDPPorts = ... systemd.services.cjdns = { description = "cjdns: routing engine designed for security, scalability, speed and ease of use"; wantedBy = [ "multi-user.target" "sleep.target"]; after = [ "network-online.target" ]; bindsTo = [ "network-online.target" ]; preStart = if cfg.confFile != null then "" else '' [ -e /etc/cjdns.keys ] && source /etc/cjdns.keys if [ -z "$CJDNS_PRIVATE_KEY" ]; then shopt -s lastpipe ${pkg}/bin/makekeys | { read private ipv6 public; } umask 0077 echo "CJDNS_PRIVATE_KEY=$private" >> /etc/cjdns.keys echo -e "CJDNS_IPV6=$ipv6\nCJDNS_PUBLIC_KEY=$public" > /etc/cjdns.public chmod 600 /etc/cjdns.keys chmod 444 /etc/cjdns.public fi if [ -z "$CJDNS_ADMIN_PASSWORD" ]; then echo "CJDNS_ADMIN_PASSWORD=$(tr -dc A-Za-z0-9 > /etc/cjdns.keys fi ''; script = ( if cfg.confFile != null then "${pkg}/bin/cjdroute < ${cfg.confFile}" else '' source /etc/cjdns.keys echo '${cjdrouteConf}' | sed \ -e "s/@CJDNS_ADMIN_PASSWORD@/$CJDNS_ADMIN_PASSWORD/g" \ -e "s/@CJDNS_PRIVATE_KEY@/$CJDNS_PRIVATE_KEY/g" \ | ${pkg}/bin/cjdroute '' ); serviceConfig = { Type = "forking"; Restart = "always"; StartLimitInterval = 0; RestartSec = 1; CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_RAW CAP_SETUID"; ProtectSystem = true; # Doesn't work on i686, causing service to fail MemoryDenyWriteExecute = !pkgs.stdenv.isi686; ProtectHome = true; PrivateTmp = true; }; }; networking.extraHosts = mkIf cfg.addExtraHosts cjdnsExtraHosts; assertions = [ { assertion = ( cfg.ETHInterface.bind != "" || cfg.UDPInterface.bind != "" || cfg.confFile != null ); message = "Neither cjdns.ETHInterface.bind nor cjdns.UDPInterface.bind defined."; } { assertion = config.networking.enableIPv6; message = "networking.enableIPv6 must be enabled for CJDNS to work"; } ]; }; }