summaryrefslogtreecommitdiff
path: root/modules/yggdrasil-wg/default.nix
blob: 67bb34f578a1ad8793f61cd56ff1057403bc7145 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
{ config, hostName, lib, pkgs, ... }:

with lib;

let
  listenPort = 51820;
  udp2rawPort = 51821;
  subnet = "2a03:4000:52:ada:1";
  subnetLength = 80;
  hostLength = subnetLength + 16;

  links = [
    { from = "vidhar";
      to = "surtr";
      endpointHost = "202.61.241.61";
      persistentKeepalive = 25;
      dynamicEndpointRefreshSeconds = 86400;
    }
    { from = "sif";
      to = "surtr";
      endpointHost = "202.61.241.61";
      persistentKeepalive = 25;
      dynamicEndpointRefreshSeconds = 86400;
    }
  ];
  routes = [
    { from = "sif";
      to = "vidhar";
      via = "surtr";
    }
    { from = "vidhar";
      to = "sif";
      via = "surtr";
    }
  ];
  hostIPs = {
    surtr = ["${subnet}::/${toString hostLength}"];
    vidhar = ["${subnet}:1::/${toString hostLength}"];
    sif = ["${subnet}:2::/${toString hostLength}"];
  };

  mkPublicKeyPath = host: ./hosts + "/${host}.pub";
  mkPrivateKeyPath = host: ./hosts + "/${host}.priv";
  
  publicKeyPath = mkPublicKeyPath hostName;
  privateKeyPath = mkPrivateKeyPath hostName;
  inNetwork = pathExists privateKeyPath && pathExists publicKeyPath;
  hostLinks = filter ({ from, to, ... }: from == hostName || to == hostName) links;
  hostRoutes = filter ({ from, to, ... }: from == hostName || to == hostName) routes;
  isRouter = inNetwork && any ({via, ...}: via == hostName) routes;
  linkToPeer = ix: opts@{from, to, ...}:
    let
      other = if from == hostName then to else from;
    in {
      allowedIPs = hostIPs.${other} ++ concatMap (rArgs: if rArgs.from != hostName || rArgs.via != to then [] else hostIPs.${rArgs.to}) routes;
      publicKey = trim (readFile (mkPublicKeyPath other));
    } // (optionalAttrs (from == hostName) (filterAttrs (n: _v: !(elem n ["from" "to" "endpointHost"])) opts // optionalAttrs (opts ? "endpointHost") { endpoint = "127.0.0.1:${toString (udp2rawPort + ix)}"; }));

  trim = str: if hasSuffix "\n" str then trim (removeSuffix "\n" str) else str;
  stripSubnet = addr: let matchRes = builtins.match "^(.*)/[0-9]+$" addr; in if matchRes == null then addr else elemAt matchRes 0;
in {
  config = {
    assertions = [
      { assertion = inNetwork || !(pathExists privateKeyPath || pathExists publicKeyPath);
        message = "yggdrasil-wg: Either both public and private keys must exist or neither.";
      }
      { assertion = !inNetwork || (hostIPs ? "${hostName}");
        message = "yggdrasil-wg: Entry in hostIPs must exist.";
      }
    ] ++ map ({from, to, ...}: let other = if from == hostName then to else from; in { assertion = pathExists (mkPublicKeyPath other); message = "yggdrasil-wg: This host (${hostName}) has a link with ‘${other}’, but no public key is available for ‘${other}’."; }) hostLinks;

    networking.wireguard.interfaces = mkIf inNetwork {
      yggdrasil = {
        allowedIPsAsRoutes = false;
        inherit listenPort;
        ips = hostIPs.${hostName};
        peers = filter (value: value != null) (imap0 (ix: opts@{to, from, ...}: if from == hostName || to == hostName then linkToPeer ix opts else null) links);
        privateKeyFile = config.sops.secrets."yggdrasil-wg.priv".path;
        postSetup = ''
          ${concatMapStringsSep "\n" (linkArgs: let other = if linkArgs.from == hostName then linkArgs.to else linkArgs.from; in concatMapStringsSep "\n" (otherIP: "ip route replace \"${otherIP}\" dev \"yggdrasil\" table \"main\"") hostIPs.${other}) hostLinks}
          ${concatMapStringsSep "\n" (routeArgs: let other = if routeArgs.from == hostName then routeArgs.to else routeArgs.from; in concatMapStringsSep "\n" (otherIP: concatMapStringsSep "\n" (viaIP: "ip route replace \"${otherIP}\" via \"${viaIP}\" dev \"yggdrasil\" table \"main\"") (map stripSubnet hostIPs.${routeArgs.via})) hostIPs.${other}) hostRoutes}
        '';
      };
    };

    systemd.services = listToAttrs (filter ({ value, ...}: value != null) (imap0 (ix: opts@{to, from, ...}: let other = if from == hostName then to else from; in nameValuePair "yggdrasil-udp2raw@${other}" (if opts ? "endpointHost" && (from == hostName || to == hostName) then {
      path = with pkgs; [iptables];
      serviceConfig = {
        RuntimeDirectory = ["udp2raw-config-${other}"];
        RuntimeDirectoryMode = "0700";
        ExecStartPre = pkgs.writeShellScript "udp2raw-mkconfig-${other}.sh" ''
          umask 0077
          secret=$(cat ${config.sops.secrets."yggdrasil-udp2raw-secret".path})
          cat >''${RUNTIME_DIRECTORY}/udp2raw.conf <<EOF
          ${if from == hostName then ''
            -c
            -l 127.0.0.1:${toString (udp2rawPort + ix)}
            -r ${opts.endpointHost}:${toString (udp2rawPort + ix)}
          '' else ''
            -s
            -l 0.0.0.0:${toString (udp2rawPort + ix)}
            -r 127.0.0.1:${toString listenPort}
          ''}
          -k $secret
          --auth-mode hmac_sha1
          --raw-mode faketcp
          -a
          --retry-on-error
          EOF
        '';
        ExecStart = "${pkgs.udp2raw}/bin/udp2raw --conf-file \${RUNTIME_DIRECTORY}/udp2raw.conf";
        Restart = "always";
      };
    } else null)) links)) // {
      "wireguard-yggdrasil" = {
        bindsTo = filter (value: value != null) (map (opts@{to, from, ...}: let other = if from == hostName then to else from; in if opts ? "endpointHost" then "yggdrasil-udp2raw@${other}.service" else null) hostLinks);
        after = filter (value: value != null) (map (opts@{to, from, ...}: let other = if from == hostName then to else from; in if opts ? "endpointHost" then "yggdrasil-udp2raw@${other}.service" else null) hostLinks);
      };
      firewall.path = optionals isRouter [pkgs.procps];
    };

    sops.secrets = {
      "yggdrasil-wg.priv" = mkIf (pathExists privateKeyPath) {
        format = "binary";
        sopsFile = privateKeyPath;
      };
      "yggdrasil-udp2raw-secret" = mkIf (any (opts@{to, from, ...}: (to == hostName || from == hostName) && opts ? "endpointHost") links) {
        format = "binary";
        sopsFile = ./udp2raw-secret;
      };
    };

    networking.hosts = mkIf inNetwork (listToAttrs (concatMap ({name, value}: map (ip: nameValuePair (stripSubnet ip) ["${name}.yggdrasil"]) value) (mapAttrsToList nameValuePair hostIPs)));

    networking.firewall = mkIf isRouter {
      extraCommands = ''
        ip6tables -A FORWARD -i yggdrasil -o yggdrasil -j nixos-fw-accept
        ip46tables -A FORWARD -j nixos-fw-log-refuse
        sysctl net.ipv6.conf.all.forwarding=1
      '';
      extraStopCommands = ''
        sysctl net.ipv6.conf.all.forwarding=0
        ip46tables -D FORWARD -j nixos-fw-log-refuse || true
        ip6tables -D FORWARD -i yggdrasil -o yggdrasil -j nixos-fw-accept || true
      '';
    };
  };
}