diff --git a/checks/flake-module.nix b/checks/flake-module.nix index fe3b4576..3096fefa 100644 --- a/checks/flake-module.nix +++ b/checks/flake-module.nix @@ -18,6 +18,7 @@ deltachat = import ./deltachat nixosTestArgs; meshnamed = import ./meshnamed nixosTestArgs; borgbackup = import ./borgbackup nixosTestArgs; + syncthing = import ./syncthing nixosTestArgs; }; schemaTests = pkgs.callPackages ./schemas.nix { inherit self; diff --git a/checks/syncthing/default.nix b/checks/syncthing/default.nix new file mode 100644 index 00000000..cd9550e0 --- /dev/null +++ b/checks/syncthing/default.nix @@ -0,0 +1,108 @@ +(import ../lib/test-base.nix) ( + # Using nixos-test, because our own test system doesn't support the necessary + # features for systemd. + { lib, ... }: + { + name = "syncthing"; + + nodes.introducer = + { self, ... }: + { + imports = [ + self.clanModules.syncthing + self.nixosModules.clanCore + { + clanCore.machineName = "introducer"; + clanCore.clanDir = ./.; + environment.etc = { + "syncthing.pam".source = ./introducer/introducer_test_cert; + "syncthing.key".source = ./introducer/introducer_test_key; + "syncthing.api".source = ./introducer/introducer_test_api; + }; + clanCore.secrets.syncthing.secrets."syncthing.api".path = "/etc/syncthing.api"; + services.syncthing.cert = "/etc/syncthing.pam"; + services.syncthing.key = "/etc/syncthing.key"; + # Doesn't test zerotier! + services.syncthing.openDefaultPorts = true; + services.syncthing.settings.folders = { + "Shared" = { + enable = true; + path = "~/Shared"; + versioning = { + type = "trashcan"; + params = { + cleanoutDays = "30"; + }; + }; + }; + }; + clan.syncthing.autoAcceptDevices = true; + clan.syncthing.autoShares = [ "Shared" ]; + # For faster Tests + systemd.timers.syncthing-auto-accept.timerConfig = { + OnActiveSec = 1; + OnUnitActiveSec = 1; + }; + } + ]; + }; + nodes.peer1 = + { self, ... }: + { + imports = [ + self.clanModules.syncthing + self.nixosModules.clanCore + { + clanCore.machineName = "peer1"; + clanCore.clanDir = ./.; + clan.syncthing.introducer = lib.strings.removeSuffix "\n" ( + builtins.readFile ./introducer/introducer_device_id + ); + environment.etc = { + "syncthing.pam".source = ./peer_1/peer_1_test_cert; + "syncthing.key".source = ./peer_1/peer_1_test_key; + }; + services.syncthing.openDefaultPorts = true; + services.syncthing.cert = "/etc/syncthing.pam"; + services.syncthing.key = "/etc/syncthing.key"; + } + ]; + }; + nodes.peer2 = + { self, ... }: + { + imports = [ + self.clanModules.syncthing + self.nixosModules.clanCore + { + clanCore.machineName = "peer2"; + clanCore.clanDir = ./.; + clan.syncthing.introducer = lib.strings.removeSuffix "\n" ( + builtins.readFile ./introducer/introducer_device_id + ); + environment.etc = { + "syncthing.pam".source = ./peer_2/peer_2_test_cert; + "syncthing.key".source = ./peer_2/peer_2_test_key; + }; + services.syncthing.openDefaultPorts = true; + services.syncthing.cert = "/etc/syncthing.pam"; + services.syncthing.key = "/etc/syncthing.key"; + } + ]; + }; + testScript = '' + start_all() + introducer.wait_for_unit("syncthing") + peer1.wait_for_unit("syncthing") + peer2.wait_for_unit("syncthing") + peer1.wait_for_file("/home/user/Shared") + peer2.wait_for_file("/home/user/Shared") + introducer.shutdown() + peer1.execute("echo hello > /home/user/Shared/hello") + peer2.wait_for_file("/home/user/Shared/hello") + out = peer2.succeed("cat /home/user/Shared/hello") + print(out) + assert "hello" in out + ''; + } +) diff --git a/checks/syncthing/introducer/introducer_device_id b/checks/syncthing/introducer/introducer_device_id new file mode 100644 index 00000000..26f556c9 --- /dev/null +++ b/checks/syncthing/introducer/introducer_device_id @@ -0,0 +1 @@ +RN4ZZIJ-5AOJVWT-JD5IAAZ-SWVDTHU-B4RWCXE-AEM3SRG-QBM2KC5-JTGUNQT diff --git a/checks/syncthing/introducer/introducer_test_api b/checks/syncthing/introducer/introducer_test_api new file mode 100644 index 00000000..104f1827 --- /dev/null +++ b/checks/syncthing/introducer/introducer_test_api @@ -0,0 +1 @@ +fKwzSQK43LWMnjVK2TDjpTkziY364dvP diff --git a/checks/syncthing/introducer/introducer_test_cert b/checks/syncthing/introducer/introducer_test_cert new file mode 100644 index 00000000..2aec2c0b --- /dev/null +++ b/checks/syncthing/introducer/introducer_test_cert @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICHDCCAaOgAwIBAgIJAJDWPRNYN7/7MAoGCCqGSM49BAMCMEoxEjAQBgNVBAoT +CVN5bmN0aGluZzEgMB4GA1UECxMXQXV0b21hdGljYWxseSBHZW5lcmF0ZWQxEjAQ +BgNVBAMTCXN5bmN0aGluZzAeFw0yMzEyMDUwMDAwMDBaFw00MzExMzAwMDAwMDBa +MEoxEjAQBgNVBAoTCVN5bmN0aGluZzEgMB4GA1UECxMXQXV0b21hdGljYWxseSBH +ZW5lcmF0ZWQxEjAQBgNVBAMTCXN5bmN0aGluZzB2MBAGByqGSM49AgEGBSuBBAAi +A2IABEzIpSQGUVVlrSndNjiwkgZ045eH26agwT5RTN44bGRe8SJqBWC7HP3V7u1C +6ZQZALSDoDUG5Oi89wGrFnxU48mYFSJFlZAVzyZoqfxVMof3vnk3uFDPo47HA4ex +8fi6yaNVMFMwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggr +BgEFBQcDAjAMBgNVHRMBAf8EAjAAMBQGA1UdEQQNMAuCCXN5bmN0aGluZzAKBggq +hkjOPQQDAgNnADBkAjB+d84wmaQuv3c94ctxV0sMh23xeTR1cPNcE8wbPQYxHmbO +HbJ3IWo5HF3di63pVgECMBUfzpmFo8dshYR2/76Ovh573Svzk2+NKEMrqRyoNVFr +JNQFhCtHbFT1rYfqYWgJBQ== +-----END CERTIFICATE----- diff --git a/checks/syncthing/introducer/introducer_test_key b/checks/syncthing/introducer/introducer_test_key new file mode 100644 index 00000000..208a19b4 --- /dev/null +++ b/checks/syncthing/introducer/introducer_test_key @@ -0,0 +1,6 @@ +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDBvqJxL4s7JFy0y6Ulg7C9C0m3N9VZlW328uMJrwznGuCdRHa/VD4qY +IcjtwJisdaqgBwYFK4EEACKhZANiAARMyKUkBlFVZa0p3TY4sJIGdOOXh9umoME+ +UUzeOGxkXvEiagVguxz91e7tQumUGQC0g6A1BuTovPcBqxZ8VOPJmBUiRZWQFc8m +aKn8VTKH9755N7hQz6OOxwOHsfH4usk= +-----END EC PRIVATE KEY----- diff --git a/checks/syncthing/peer_1/peer_1_test_cert b/checks/syncthing/peer_1/peer_1_test_cert new file mode 100644 index 00000000..effa8126 --- /dev/null +++ b/checks/syncthing/peer_1/peer_1_test_cert @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICHTCCAaKgAwIBAgIIT2gZuvqVFP0wCgYIKoZIzj0EAwIwSjESMBAGA1UEChMJ +U3luY3RoaW5nMSAwHgYDVQQLExdBdXRvbWF0aWNhbGx5IEdlbmVyYXRlZDESMBAG +A1UEAxMJc3luY3RoaW5nMB4XDTIzMTIwNjAwMDAwMFoXDTQzMTIwMTAwMDAwMFow +SjESMBAGA1UEChMJU3luY3RoaW5nMSAwHgYDVQQLExdBdXRvbWF0aWNhbGx5IEdl +bmVyYXRlZDESMBAGA1UEAxMJc3luY3RoaW5nMHYwEAYHKoZIzj0CAQYFK4EEACID +YgAEBAr1CsciwCa0vi7eC6xxuSGijY3txbjtsyFanec/fge4oJBD3rVpaLKFETb3 +TvHHsuvblzElcP483MEVq6FMUoxwuL9CzTtpJrRhtwSmAs8AHLFu8irVn8sZjgkL +sXMho1UwUzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG +AQUFBwMCMAwGA1UdEwEB/wQCMAAwFAYDVR0RBA0wC4IJc3luY3RoaW5nMAoGCCqG +SM49BAMCA2kAMGYCMQDbrtLgfcyMMIkNQn+PJe9DHYAqj8C47LQcWuIY/nekhOu0 +aUfKctEAwyBtI60Y5zcCMQCEdgD/6CNBh7Qqq3z3CKPhlrpxHtCO5tNw17k0jfdH +haCwJInHZvZgclHk4EtFpTw= +-----END CERTIFICATE----- diff --git a/checks/syncthing/peer_1/peer_1_test_key b/checks/syncthing/peer_1/peer_1_test_key new file mode 100644 index 00000000..101f810c --- /dev/null +++ b/checks/syncthing/peer_1/peer_1_test_key @@ -0,0 +1,6 @@ +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDA14Nqo17Xs/xRLGH2KLuyzjKp4eW9iWFobVNM93RZZbECT++W3XcQc +cEc5WVtiPmWgBwYFK4EEACKhZANiAAQECvUKxyLAJrS+Lt4LrHG5IaKNje3FuO2z +IVqd5z9+B7igkEPetWlosoURNvdO8cey69uXMSVw/jzcwRWroUxSjHC4v0LNO2km +tGG3BKYCzwAcsW7yKtWfyxmOCQuxcyE= +-----END EC PRIVATE KEY----- diff --git a/checks/syncthing/peer_2/peer_2_test_cert b/checks/syncthing/peer_2/peer_2_test_cert new file mode 100644 index 00000000..b0830f0e --- /dev/null +++ b/checks/syncthing/peer_2/peer_2_test_cert @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICHjCCAaOgAwIBAgIJAKbMWefkf1rVMAoGCCqGSM49BAMCMEoxEjAQBgNVBAoT +CVN5bmN0aGluZzEgMB4GA1UECxMXQXV0b21hdGljYWxseSBHZW5lcmF0ZWQxEjAQ +BgNVBAMTCXN5bmN0aGluZzAeFw0yMzEyMDYwMDAwMDBaFw00MzEyMDEwMDAwMDBa +MEoxEjAQBgNVBAoTCVN5bmN0aGluZzEgMB4GA1UECxMXQXV0b21hdGljYWxseSBH +ZW5lcmF0ZWQxEjAQBgNVBAMTCXN5bmN0aGluZzB2MBAGByqGSM49AgEGBSuBBAAi +A2IABFZTMt4RfsfBue0va7QuNdjfXMI4HfZzJCEcG+b9MtV7FlDmwMKX5fgGykD9 +FBbC7yiza3+xCobdMb5bakz1qYJ7nUFCv1mwSDo2eNM+/XE+rJmlre8NwkwGmvzl +h1uhyqNVMFMwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggr +BgEFBQcDAjAMBgNVHRMBAf8EAjAAMBQGA1UdEQQNMAuCCXN5bmN0aGluZzAKBggq +hkjOPQQDAgNpADBmAjEAwzhsroN6R4/quWeXj6dO5gt5CfSTLkLee6vrcuIP5i1U +rZvJ3OKQVmmGG6IWYe7iAjEAyuq3X2wznaqiw2YK3IDI4qVeYWpCUap0fwRNq7/x +4dC4k+BOzHcuJOwNBIY/bEuK +-----END CERTIFICATE----- diff --git a/checks/syncthing/peer_2/peer_2_test_key b/checks/syncthing/peer_2/peer_2_test_key new file mode 100644 index 00000000..7b9b28a0 --- /dev/null +++ b/checks/syncthing/peer_2/peer_2_test_key @@ -0,0 +1,6 @@ +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDCXHGpvumKjjDRxB6SsjZOb7duw3w+rdlGQCJTIvRThLjD6zwjnyImi +7c3PD5nWtLqgBwYFK4EEACKhZANiAARWUzLeEX7HwbntL2u0LjXY31zCOB32cyQh +HBvm/TLVexZQ5sDCl+X4BspA/RQWwu8os2t/sQqG3TG+W2pM9amCe51BQr9ZsEg6 +NnjTPv1xPqyZpa3vDcJMBpr85Ydboco= +-----END EC PRIVATE KEY----- diff --git a/clanModules/flake-module.nix b/clanModules/flake-module.nix index 431cf319..e620178d 100644 --- a/clanModules/flake-module.nix +++ b/clanModules/flake-module.nix @@ -9,5 +9,6 @@ deltachat = ./deltachat.nix; xfce = ./xfce.nix; borgbackup = ./borgbackup.nix; + syncthing = ./syncthing.nix; }; } diff --git a/clanModules/syncthing.nix b/clanModules/syncthing.nix new file mode 100644 index 00000000..cc5eb7f9 --- /dev/null +++ b/clanModules/syncthing.nix @@ -0,0 +1,206 @@ +{ config +, pkgs +, lib +, ... +}: +{ + options.clan.syncthing = { + id = lib.mkOption { + type = lib.types.nullOr lib.types.str; + example = "BABNJY4-G2ICDLF-QQEG7DD-N3OBNGF-BCCOFK6-MV3K7QJ-2WUZHXS-7DTW4AS"; + default = config.clanCore.secrets.syncthing.facts."syncthing.pub".value or null; + }; + introducer = lib.mkOption { + description = '' + The introducer for the machine. + ''; + type = lib.types.nullOr lib.types.str; + default = null; + }; + autoAcceptDevices = lib.mkOption { + description = '' + Auto accept incoming device requests. + Should only be used on the introducer. + ''; + type = lib.types.bool; + default = false; + }; + autoShares = lib.mkOption { + description = '' + Auto share the following Folders by their ID's with introduced devices. + Should only be used on the introducer. + ''; + type = lib.types.listOf lib.types.str; + default = [ ]; + }; + }; + + imports = [ + { + # Syncthing ports: 8384 for remote access to GUI + # 22000 TCP and/or UDP for sync traffic + # 21027/UDP for discovery + # source: https://docs.syncthing.net/users/firewall.html + networking.firewall.interfaces."zt+".allowedTCPPorts = [ + 8384 + 22000 + ]; + # local ui TODO: mkDefault ? + networking.firewall.allowedTCPPorts = [ 8384 ]; + networking.firewall.interfaces."zt+".allowedUDPPorts = [ + 22000 + 21027 + ]; + + assertions = [ + { + assertion = + lib.all (attr: builtins.hasAttr attr config.services.syncthing.settings.folders) + config.clan.syncthing.autoShares; + message = '' + Syncthing: If you want to AutoShare a folder, you need to have it configured on the sharing device. + ''; + } + ]; + + services.syncthing = { + enable = true; + configDir = "/var/lib/syncthing"; + + overrideFolders = true; + overrideDevices = true; + + dataDir = lib.mkDefault "/home/user/"; + + key = + lib.mkDefault + config.clanCore.secrets.syncthing.secrets."syncthing.key".path or null; + cert = + lib.mkDefault + config.clanCore.secrets.syncthing.secrets."syncthing.cert".path or null; + + settings = { + options = { + urAccepted = -1; + # TODO: + # allowedNetworks = []; + }; + devices = + { } + // ( + if (config.clan.syncthing.introducer == null) then + { } + else + { + "${config.clan.syncthing.introducer}" = { + name = "introducer"; + id = config.clan.syncthing.introducer; + introducer = true; + autoAcceptFolders = true; + }; + } + ); + }; + }; + systemd.services.syncthing-auto-accept = + let + baseAddress = "127.0.0.1:8384"; + getPendingDevices = "/rest/cluster/pending/devices"; + postNewDevice = "/rest/config/devices"; + SharedFolderById = "/rest/config/folders/"; + apiKey = config.clanCore.secrets.syncthing.secrets."syncthing.api".path or null; + in + lib.mkIf config.clan.syncthing.autoAcceptDevices { + description = "Syncthing auto accept devices"; + requisite = [ "syncthing.service" ]; + after = [ "syncthing.service" ]; + wantedBy = [ "multi-user.target" ]; + + script = '' + set -x + # query pending deviceID's + APIKEY=$(cat ${apiKey}) + PENDING=$(${ + lib.getExe pkgs.curl + } -X GET -H "X-API-Key: $APIKEY" ${baseAddress}${getPendingDevices}) + PENDING=$(echo $PENDING | ${lib.getExe pkgs.jq} keys[]) + + # accept pending deviceID's + for ID in $PENDING;do + ${ + lib.getExe pkgs.curl + } -X POST -d "{\"deviceId\": $ID}" -H "Content-Type: application/json" -H "X-API-Key: $APIKEY" ${baseAddress}${postNewDevice} + + # get all shared folders by their ID + for folder in ${builtins.toString config.clan.syncthing.autoShares}; do + SHARED_IDS=$(${ + lib.getExe pkgs.curl + } -X GET -H "X-API-Key: $APIKEY" ${baseAddress}${SharedFolderById}"$folder" | ${ + lib.getExe pkgs.jq + } ."devices") + PATCHED_IDS=$(echo $SHARED_IDS | ${ + lib.getExe pkgs.jq + } ".+= [{\"deviceID\": $ID, \"introducedBy\": \"\", \"encryptionPassword\": \"\"}]") + ${ + lib.getExe pkgs.curl + } -X PATCH -d "{\"devices\": $PATCHED_IDS}" -H "X-API-Key: $APIKEY" ${baseAddress}${SharedFolderById}"$folder" + done + done + ''; + }; + + systemd.timers.syncthing-auto-accept = + lib.mkIf config.clan.syncthing.autoAcceptDevices + { + description = "Syncthing Auto Accept"; + + wantedBy = [ "syncthing-auto-accept.service" ]; + + timerConfig = { + OnActiveSec = lib.mkDefault 60; + OnUnitActiveSec = lib.mkDefault 60; + }; + }; + + systemd.services.syncthing-init-api-key = + let + apiKey = config.clanCore.secrets.syncthing.secrets."syncthing.api".path or null; + in + lib.mkIf config.clan.syncthing.autoAcceptDevices { + description = "Set the api key"; + after = [ "syncthing-init.service" ]; + wantedBy = [ "multi-user.target" ]; + script = '' + # set -x + set -efu pipefail + + APIKEY=$(cat ${apiKey}) + ${ + lib.getExe pkgs.gnused + } -i "s/.*<\/apikey>/$APIKEY<\/apikey>/" /var/lib/syncthing/config.xml + # sudo systemctl restart syncthing.service + systemctl restart syncthing.service + ''; + serviceConfig = { + WorkingDirectory = "/var/lib/syncthing"; + BindReadOnlyPaths = [ apiKey ]; + Type = "oneshot"; + }; + }; + + clanCore.secrets.syncthing = { + secrets."syncthing.key" = { }; + secrets."syncthing.cert" = { }; + secrets."syncthing.api" = { }; + facts."syncthing.pub" = { }; + generator.script = '' + ${pkgs.syncthing}/bin/syncthing generate --config "$secrets" + mv "$secrets"/key.pem "$secrets"/syncthing.key + mv "$secrets"/cert.pem "$secrets"/syncthing.cert + cat "$secrets"/config.xml | ${pkgs.gnugrep}/bin/grep -oP '(?<= "$facts"/syncthing.pub + cat "$secrets"/config.xml | ${pkgs.gnugrep}/bin/grep -oP '\K[^<]+' | uniq > "$secrets"/syncthing.api + ''; + }; + } + ]; +}