diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 59e67971f0831..a5e80ed3e12dc 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -497,6 +497,7 @@ in early-mount-options = runTest ./early-mount-options.nix; earlyoom = runTestOn [ "x86_64-linux" ] ./earlyoom.nix; easytier = runTest ./easytier.nix; + easytier-modular = runTest ./easytier-modular.nix; ec2-config = (handleTestOn [ "x86_64-linux" ] ./ec2.nix { }).boot-ec2-config or { }; ec2-image = runTest ./ec2-image.nix; ec2-nixops = (handleTestOn [ "x86_64-linux" ] ./ec2.nix { }).boot-ec2-nixops or { }; diff --git a/nixos/tests/easytier-modular.nix b/nixos/tests/easytier-modular.nix new file mode 100644 index 0000000000000..c39dcaaf6709d --- /dev/null +++ b/nixos/tests/easytier-modular.nix @@ -0,0 +1,161 @@ +{ lib, ... }: +{ + _class = "nixosTest"; + + name = "easytier-modular"; + + nodes = + let + genPeer = + hostConfig: + { pkgs, ... }: + lib.mkMerge [ + { + networking.useDHCP = false; + networking.firewall.allowedTCPPorts = [ + 11010 + 11011 + ]; + networking.firewall.allowedUDPPorts = [ + 11010 + 11011 + ]; + + system.services."easytier-default" = { + imports = [ pkgs.easytier.services.default ]; + easytier.settings = { + instance_name = "default"; + dev_name = "et_def"; + rpc_portal = "0.0.0.0:11000"; + network_identity = { + network_name = "easytier_test"; + network_secret = "easytier_test_secret"; + }; + }; + }; + } + hostConfig + ]; + in + { + relay = + { pkgs, ... }@args: + lib.mkMerge [ + (genPeer { + virtualisation.vlans = [ + 1 + 2 + ]; + networking.interfaces.eth1.ipv4.addresses = [ + { + address = "192.168.1.11"; + prefixLength = 24; + } + ]; + networking.interfaces.eth2.ipv4.addresses = [ + { + address = "192.168.2.11"; + prefixLength = 24; + } + ]; + + system.services."easytier-default".easytier.settings = { + ipv4 = "10.144.144.1"; + listeners = [ + "tcp://0.0.0.0:11010" + "wss://0.0.0.0:11011" + ]; + }; + } args) + + { + networking.firewall.allowedTCPPorts = [ 11020 ]; + networking.firewall.allowedUDPPorts = [ 11020 ]; + + system.services."easytier-second" = { + imports = [ pkgs.easytier.services.default ]; + easytier = { + peers = [ + "tcp://192.168.1.11:11010" + "tcp://192.168.2.11:11010" + ]; + settings = { + instance_name = "second"; + ipv4 = "10.144.144.4"; + + rpc_portal = "0.0.0.0:11001"; + + network_identity = { + network_name = "easytier_test"; + network_secret = "easytier_test_secret"; + }; + + listeners = [ "tcp://0.0.0.0:11020" ]; + flags = { + bind_device = false; + no_tun = true; + }; + }; + }; + }; + } + ]; + + peer1 = genPeer { + virtualisation.vlans = [ 1 ]; + system.services."easytier-default".easytier = { + settings.ipv4 = "10.144.144.2"; + peers = [ "tcp://192.168.1.11:11010" ]; + }; + }; + + peer2 = genPeer { + virtualisation.vlans = [ 2 ]; + system.services."easytier-default".easytier = { + settings.ipv4 = "10.144.144.3"; + peers = [ "wss://192.168.2.11:11011" ]; + }; + }; + }; + + testScript = '' + start_all() + + with subtest("Waiting for all services..."): + relay.wait_for_unit("easytier-default.service") + relay.wait_for_unit("easytier-second.service") + peer1.wait_for_unit("easytier-default.service") + peer2.wait_for_unit("easytier-default.service") + + with subtest("relay is accessible by the other hosts"): + peer1.succeed("ping -c5 192.168.1.11") + peer2.succeed("ping -c5 192.168.2.11") + + with subtest("The other hosts are in separate vlans"): + peer1.fail("ping -c5 192.168.2.11") + peer2.fail("ping -c5 192.168.1.11") + + with subtest("Each host can ping themselves through EasyTier"): + relay.succeed("ping -c5 10.144.144.1") + peer1.succeed("ping -c5 10.144.144.2") + peer2.succeed("ping -c5 10.144.144.3") + + with subtest("Relay is accessible by the other hosts through EasyTier"): + peer1.succeed("ping -c5 10.144.144.1") + peer2.succeed("ping -c5 10.144.144.1") + + with subtest("Relay can access the other hosts through EasyTier"): + relay.succeed("ping -c5 10.144.144.2") + relay.succeed("ping -c5 10.144.144.3") + + with subtest("The other hosts in separate vlans can access each other through EasyTier"): + peer1.succeed("ping -c5 10.144.144.3") + peer2.succeed("ping -c5 10.144.144.2") + + with subtest("Relay Second is accessible through EasyTier"): + peer1.succeed("ping -c5 10.144.144.4") + peer2.succeed("ping -c5 10.144.144.4") + ''; + + meta.maintainers = with lib.maintainers; [ moraxyc ]; +} diff --git a/pkgs/by-name/ea/easytier/package.nix b/pkgs/by-name/ea/easytier/package.nix index 19040c2e7f7ad..564bc8f60152e 100644 --- a/pkgs/by-name/ea/easytier/package.nix +++ b/pkgs/by-name/ea/easytier/package.nix @@ -8,16 +8,20 @@ nix-update-script, installShellFiles, withQuic ? false, # with QUIC protocol support + + formats, + bash, + iproute2, }: -rustPlatform.buildRustPackage rec { +rustPlatform.buildRustPackage (finalAttrs: { pname = "easytier"; version = "2.5.0"; src = fetchFromGitHub { owner = "EasyTier"; repo = "EasyTier"; - tag = "v${version}"; + tag = "v${finalAttrs.version}"; hash = "sha256-XnEfxWDKUTQFWYKtqetI7sLbOmGqw2BqpU5by1ajZGA="; }; @@ -46,13 +50,20 @@ rustPlatform.buildRustPackage rec { doCheck = false; # tests failed due to heavy rely on network passthru = { - tests = { inherit (nixosTests) easytier; }; + tests = { inherit (nixosTests) easytier easytier-modular; }; updateScript = nix-update-script { }; }; + passthru.services.default = { + imports = [ + (lib.modules.importApply ./service.nix { inherit formats bash iproute2; }) + ]; + easytier.package = finalAttrs.finalPackage; + }; + meta = { homepage = "https://github.com/EasyTier/EasyTier"; - changelog = "https://github.com/EasyTier/EasyTier/releases/tag/v${version}"; + changelog = "https://github.com/EasyTier/EasyTier/releases/tag/v${finalAttrs.version}"; description = "Simple, decentralized mesh VPN with WireGuard support"; longDescription = '' EasyTier is a simple, safe and decentralized VPN networking solution implemented @@ -63,4 +74,4 @@ rustPlatform.buildRustPackage rec { platforms = with lib.platforms; unix ++ windows; maintainers = with lib.maintainers; [ ltrump ]; }; -} +}) diff --git a/pkgs/by-name/ea/easytier/service.nix b/pkgs/by-name/ea/easytier/service.nix new file mode 100644 index 0000000000000..e5cf1cfcbfc8a --- /dev/null +++ b/pkgs/by-name/ea/easytier/service.nix @@ -0,0 +1,201 @@ +# Non-module dependencies (`importApply`) +{ + formats, + iproute2, + bash, +}: + +# Service module +{ + lib, + config, + options, + ... +}: +let + cfg = config.easytier; + instanceName = cfg.settings.instance_name; + toml = formats.toml { }; +in +{ + _class = "service"; + + meta.maintainers = with lib.maintainers; [ moraxyc ]; + + options = { + easytier = { + package = lib.mkOption { + description = "Package to use for easytier"; + defaultText = "The package that provided this module."; + type = lib.types.package; + }; + + peers = lib.mkOption { + type = with lib.types; listOf str; + default = [ ]; + description = '' + Peers to connect initially. Valid format is: `://:`. + ''; + example = [ + "tcp://example.com:11010" + ]; + }; + + configServer = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = '' + Configure the instance from config server. When this option + set, any other settings for configuring the service manually + except {option}`easytier.settings.hostname` will be ignored. Valid formats are: + + - full uri for custom server: `udp://example.com:22020/` + - username only for official server: `` + ''; + example = "udp://example.com:22020/myusername"; + }; + + settings = lib.mkOption { + type = lib.types.submodule { + freeformType = toml.type; + options = { + hostname = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = "Hostname shown in peer list and web console."; + }; + instance_name = lib.mkOption { + type = lib.types.str; + description = "Identify different instances on same host"; + }; + network_identity = { + network_name = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = "EasyTier network name."; + }; + network_secret = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = '' + EasyTier network credential used for verification and encryption. + It is highly recommended to use {option}`easytier.environmentFiles` to + avoid leaking the secret into the world-readable Nix store. + ''; + }; + }; + }; + }; + description = "Settings to generate config file"; + }; + + configFile = lib.mkOption { + type = lib.types.path; + description = '' + EasyTier config file. + + When this option is set, it takes precedence over all other + {option}`easytier.settings.*` options. + ''; + }; + + environmentFiles = lib.mkOption { + type = with lib.types; listOf path; + default = [ ]; + description = '' + Files containing environment variables (like {env}`ET_NETWORK_SECRET`) + to be passed to the service. All command-line args + have corresponding environment variables + ''; + example = lib.literalExpression '' + [ + /path/to/.env + /path/to/.env.secret + ] + ''; + }; + + extraArgs = lib.mkOption { + description = "Extra arguments to pass to `easytier-core`"; + type = with lib.types; listOf str; + default = [ ]; + }; + }; + }; + + config = { + easytier = { + settings.peer = lib.mkIf (cfg.peers != [ ]) (map (p: { uri = p; }) cfg.peers); + configFile = lib.mkDefault ( + toml.generate "easytier-${instanceName}.toml" ( + lib.attrsets.filterAttrsRecursive (_: v: v != null) cfg.settings + ) + ); + }; + process = { + argv = [ + (lib.getExe' cfg.package "easytier-core") + ] + ++ lib.optional (cfg.settings.hostname != null) "--hostname=${cfg.settings.hostname}" + ++ lib.optional (cfg.configServer == null) "--config-file=${config.configData."config.toml".path}" + ++ lib.optional (cfg.configServer != null) "--config-server=${cfg.configServer}" + ++ cfg.extraArgs; + }; + configData."config.toml" = { + enable = lib.mkDefault (cfg.configServer == null); + source = cfg.configFile; + }; + } + # Refine the service for systemd + // lib.optionalAttrs (options ? systemd) { + systemd.mainExecStart = config.systemd.lib.escapeSystemdExecArgs config.process.argv; + + systemd.service = { + description = "EasyTier Daemon - ${instanceName}"; + wants = [ + "network-online.target" + "nss-lookup.target" + ]; + after = [ + "network-online.target" + "nss-lookup.target" + ]; + wantedBy = [ "multi-user.target" ]; + path = [ + cfg.package + iproute2 + bash + ]; + restartTriggers = [ cfg.configFile ]; + serviceConfig = { + Type = "simple"; + Restart = "on-failure"; + StateDirectory = "easytier/easytier-${instanceName}"; + StateDirectoryMode = "0700"; + WorkingDirectory = "%S/easytier/easytier-${instanceName}"; + EnvironmentFile = cfg.environmentFiles; + + # Hardening + DynamicUser = true; + CapabilityBoundingSet = [ + "CAP_NET_RAW" + "CAP_NET_ADMIN" + ]; + AmbientCapabilities = [ + "CAP_NET_RAW" + "CAP_NET_ADMIN" + ]; + DeviceAllow = "/dev/net/tun"; + MemoryDenyWriteExecute = true; + PrivateTmp = true; + ProtectHome = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RestrictRealtime = true; + UMask = "0077"; + }; + }; + }; +}