diff --git a/.gitattributes b/.gitattributes index ca9c5d8..0fe79d9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ secrets/*.secret filter=git-crypt diff=git-crypt +secrets/wireguard.nix filter=git-crypt diff=git-crypt diff --git a/base/default.nix b/base/default.nix index 35ec4f2..03d0b3d 100644 --- a/base/default.nix +++ b/base/default.nix @@ -1,6 +1,7 @@ { ... }: { imports = [ + ./networking.nix ./nix.nix ./users.nix ]; diff --git a/base/networking.nix b/base/networking.nix new file mode 100644 index 0000000..c17ed76 --- /dev/null +++ b/base/networking.nix @@ -0,0 +1,12 @@ +{ lib, ... }: +{ + options.my.networking.externalInterface = with lib; mkOption { + type = types.nullOr types.str; + default = null; + example = "eth0"; + description = '' + Name of the network interface that egresses to the internet. Used for + e.g. NATing internal networks. + ''; + }; +} diff --git a/hosts/poseidon/default.nix b/hosts/poseidon/default.nix index d5ec7c7..1ff88f2 100644 --- a/hosts/poseidon/default.nix +++ b/hosts/poseidon/default.nix @@ -41,6 +41,7 @@ in "62.210.16.6" "62.210.16.7" ]; + my.networking.externalInterface = "eno1"; # List packages installed in system profile. To search, run: # $ nix search wget @@ -119,6 +120,23 @@ in username = "alarsyo"; password = secrets.transmission-password; }; + + wireguard = { + enable = true; + iface = "wg"; + port = 51820; + + net = { + v4 = { + subnet = "10.0.0"; + mask = 24; + }; + v6 = { + subnet = "fd42:42:42"; + mask = 64; + }; + }; + }; }; security.acme.acceptTerms = true; diff --git a/secrets/default.nix b/secrets/default.nix index 19bd7d6..7d0e393 100644 --- a/secrets/default.nix +++ b/secrets/default.nix @@ -1,4 +1,4 @@ -{ lib, config, ... }: +{ pkgs, lib, config, ... }: with lib; { options.my.secrets = mkOption { @@ -12,5 +12,7 @@ with lib; miniflux-admin-credentials = lib.fileContents ./miniflux-admin-credentials.secret; borg-backup-repo = lib.fileContents ./borg-backup-repo.secret; transmission-password = lib.fileContents ./transmission.secret; + + wireguard = pkgs.callPackage ./wireguard.nix { }; }; } diff --git a/secrets/wireguard.nix b/secrets/wireguard.nix new file mode 100644 index 0000000..e2bed79 Binary files /dev/null and b/secrets/wireguard.nix differ diff --git a/services/default.nix b/services/default.nix index 707a116..07c86fa 100644 --- a/services/default.nix +++ b/services/default.nix @@ -14,5 +14,6 @@ ./nginx.nix ./postgresql-backup.nix ./transmission.nix + ./wireguard.nix ]; } diff --git a/services/wireguard.nix b/services/wireguard.nix new file mode 100644 index 0000000..9d13b55 --- /dev/null +++ b/services/wireguard.nix @@ -0,0 +1,122 @@ +# Stolen from: +# +# https://gitea.belanyi.fr/ambroisie/nix-config/src/branch/main/services/wireguard.nix + +{ config, lib, pkgs, ... }: +let + cfg = config.my.services.wireguard; + hostName = config.networking.hostName; + + peers = config.my.secrets.wireguard.peers; + thisPeer = peers."${hostName}"; + otherPeers = lib.filterAttrs (name: _: name != hostName) peers; + + extIface = config.my.networking.externalInterface; +in +{ + options.my.services.wireguard = with lib; { + enable = mkEnableOption "Wireguard VPN service"; + + iface = mkOption { + type = types.str; + default = "wg"; + example = "wg0"; + description = "Name of the interface to configure"; + }; + + port = mkOption { + type = types.port; + default = 51820; + example = 55555; + description = "Port to configure for Wireguard"; + }; + + net = { + v4 = { + subnet = mkOption { + type = types.str; + default = "10.0.0"; + example = "10.100.0"; + description = "Which prefix to use for internal IPs"; + }; + mask = mkOption { + type = types.int; + default = 24; + example = 28; + description = "The CIDR mask to use on internal IPs"; + }; + }; + v6 = { + subnet = mkOption { + type = types.str; + default = "fd42:42:42"; + example = "fdc9:281f:04d7:9ee9"; + description = "Which prefix to use for internal IPs"; + }; + mask = mkOption { + type = types.int; + default = 64; + example = 68; + description = "The CIDR mask to use on internal IPs"; + }; + }; + }; + }; + + config.networking = lib.mkIf cfg.enable { + wg-quick.interfaces."${cfg.iface}" = { + listenPort = cfg.port; + address = with cfg.net; with lib; [ + "${v4.subnet}.${toString thisPeer.clientNum}/${toString v4.mask}" + "${v6.subnet}::${toString thisPeer.clientNum}/${toHexString v6.mask}" + ]; + privateKey = thisPeer.privateKey; + + peers = lib.mapAttrsToList + (name: peer: { + inherit (peer) publicKey; + } // lib.optionalAttrs (thisPeer ? externalIp) { + # Only forward from server to clients + allowedIPs = with cfg.net; [ + "${v4.subnet}.${toString peer.clientNum}/32" + "${v6.subnet}::${toString peer.clientNum}/128" + ]; + } // lib.optionalAttrs (peer ? externalIp) { + # Known addresses + endpoint = "${peer.externalIp}:${toString cfg.port}"; + } // lib.optionalAttrs (!(thisPeer ? externalIp)) { + # Forward all traffic to server + allowedIPs = with cfg.net; [ + "0.0.0.0/0" + "::/0" + ]; + # Roaming clients need to keep NAT-ing active + persistentKeepalive = 10; + }) + otherPeers; + } // lib.optionalAttrs (thisPeer ? externalIp) { + # Setup forwarding on server + postUp = with cfg.net; '' + ${pkgs.iptables}/bin/iptables -A FORWARD -i ${cfg.iface} -j ACCEPT + ${pkgs.iptables}/bin/iptables -t nat -A POSTROUTING -s ${v4.subnet}.1/${toString v4.mask} -o ${extIface} -j MASQUERADE + ${pkgs.iptables}/bin/ip6tables -A FORWARD -i ${cfg.iface} -j ACCEPT + ${pkgs.iptables}/bin/ip6tables -t nat -A POSTROUTING -s ${v6.subnet}::1/${toString v6.mask} -o ${extIface} -j MASQUERADE + ''; + preDown = with cfg.net; '' + ${pkgs.iptables}/bin/iptables -D FORWARD -i ${cfg.iface} -j ACCEPT + ${pkgs.iptables}/bin/iptables -t nat -D POSTROUTING -s ${v4.subnet}.1/${toString v4.mask} -o ${extIface} -j MASQUERADE + ${pkgs.iptables}/bin/ip6tables -D FORWARD -i ${cfg.iface} -j ACCEPT + ${pkgs.iptables}/bin/ip6tables -t nat -D POSTROUTING -s ${v6.subnet}::1/${toString v6.mask} -o ${extIface} -j MASQUERADE + ''; + + }; + + nat = lib.optionalAttrs (thisPeer ? externalIp) { + enable = true; + externalInterface = extIface; + internalInterfaces = [ cfg.iface ]; + }; + + firewall.allowedUDPPorts = lib.optional (thisPeer ? externalIp) cfg.port; + }; +}