diff --git a/nixos/doc/manual/release-notes/rl-2505.section.md b/nixos/doc/manual/release-notes/rl-2505.section.md index 7a3982c95795..44e8e6b84364 100644 --- a/nixos/doc/manual/release-notes/rl-2505.section.md +++ b/nixos/doc/manual/release-notes/rl-2505.section.md @@ -88,6 +88,8 @@ Alongside many enhancements to NixOS modules and general system improvements, th - [Readeck](https://readeck.org/), a read-it later web-application. Available as [services.readeck](#opt-services.readeck.enable). +- [EasyTier](https://github.com/EasyTier/EasyTier), a decentralized VPN solution. Available as [services.easytier](#opt-services.easytier.enable). + - [Traccar](https://www.traccar.org/), a modern GPS Tracking Platform. Available as [services.traccar](#opt-services.traccar.enable). - [Schroot](https://codeberg.org/shelter/reschroot), a lightweight virtualisation tool. Securely enter a chroot and run a command or login shell. Available as [programs.schroot](#opt-programs.schroot.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 036490c9589a..953e11d26a4f 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1115,6 +1115,7 @@ ./services/networking/dnsproxy.nix ./services/networking/doh-proxy-rust.nix ./services/networking/doh-server.nix + ./services/networking/easytier.nix ./services/networking/ejabberd.nix ./services/networking/envoy.nix ./services/networking/epmd.nix diff --git a/nixos/modules/services/networking/easytier.nix b/nixos/modules/services/networking/easytier.nix new file mode 100644 index 000000000000..127c72063fac --- /dev/null +++ b/nixos/modules/services/networking/easytier.nix @@ -0,0 +1,292 @@ +{ + config, + lib, + pkgs, + ... +}: + +with lib; +let + cfg = config.services.easytier; + settingsFormat = pkgs.formats.toml { }; + + genFinalSettings = + inst: + attrsets.filterAttrsRecursive (_: v: v != { }) ( + attrsets.filterAttrsRecursive (_: v: v != null) ( + { + inherit (inst.settings) + instance_name + hostname + ipv4 + dhcp + listeners + ; + network_identity = { + inherit (inst.settings) network_name network_secret; + }; + peer = map (p: { uri = p; }) inst.settings.peers; + } + // inst.extraSettings + ) + ); + + genConfigFile = + name: inst: + if inst.configFile == null then + settingsFormat.generate "easytier-${name}.toml" (genFinalSettings inst) + else + inst.configFile; + + activeInsts = filterAttrs (_: inst: inst.enable) cfg.instances; + + settingsModule = name: { + options = { + instance_name = mkOption { + type = types.str; + default = name; + description = "Identify different instances on same host"; + }; + + hostname = mkOption { + type = with types; nullOr str; + default = null; + description = "Hostname shown in peer list and web console."; + }; + + network_name = mkOption { + type = with types; nullOr str; + default = null; + description = "EasyTier network name."; + }; + + network_secret = mkOption { + type = with types; nullOr str; + default = null; + description = '' + EasyTier network credential used for verification and + encryption. It can also be set in environmentFile. + ''; + }; + + ipv4 = mkOption { + type = with types; nullOr str; + default = null; + description = '' + IPv4 cidr address of this peer in the virtual network. If + empty, this peer will only forward packets and no TUN device + will be created. + ''; + example = "10.144.144.1/24"; + }; + + dhcp = mkOption { + type = types.bool; + default = false; + description = '' + Automatically determine the IPv4 address of this peer based on + existing peers on network. + ''; + }; + + listeners = mkOption { + type = with types; listOf str; + default = [ + "tcp://0.0.0.0:11010" + "udp://0.0.0.0:11010" + ]; + description = '' + Listener addresses to accept connections from other peers. + Valid format is: `://:`, where the protocol + can be `tcp`, `udp`, `ring`, `wg`, `ws`, `wss`. + ''; + }; + + peers = mkOption { + type = with types; listOf str; + default = [ ]; + description = '' + Peers to connect initially. Valid format is: `://:`. + ''; + example = [ + "tcp://example.com:11010" + ]; + }; + }; + }; + + instanceModule = + { name, ... }: + { + options = { + enable = mkOption { + type = types.bool; + default = true; + description = "Enable the instance."; + }; + + configServer = mkOption { + type = with types; nullOr str; + default = null; + description = '' + Configure the instance from config server. When this option + set, any other settings for configuring the instance manually + except `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"; + }; + + configFile = mkOption { + type = with types; nullOr path; + default = null; + description = '' + Path to easytier config file. Setting this option will + override `settings` and `extraSettings` of this instance. + ''; + }; + + environmentFiles = mkOption { + type = with types; listOf path; + default = [ ]; + description = '' + Environment files for this instance. All command-line args + have corresponding environment variables. + ''; + example = literalExpression '' + [ + /path/to/.env + /path/to/.env.secret + ] + ''; + }; + + settings = mkOption { + type = types.submodule (settingsModule name); + default = { }; + description = '' + Settings to generate {file}`easytier-${name}.toml` + ''; + }; + + extraSettings = mkOption { + type = settingsFormat.type; + default = { }; + description = '' + Extra settings to add to {file}`easytier-${name}.toml`. + ''; + }; + + extraArgs = mkOption { + type = with types; listOf str; + default = [ ]; + description = '' + Extra args append to the easytier command-line. + ''; + }; + }; + }; + +in +{ + options.services.easytier = { + enable = mkEnableOption "EasyTier daemon"; + + package = mkPackageOption pkgs "easytier" { }; + + allowSystemForward = mkEnableOption '' + Allow the system to forward packets from easytier. Useful when + `proxy_forward_by_system` enabled. + ''; + + instances = mkOption { + description = '' + EasyTier instances. + ''; + type = types.attrsOf (types.submodule instanceModule); + default = { }; + example = { + settings = { + network_name = "easytier"; + network_secret = "easytier"; + ipv4 = "10.144.144.1/24"; + peers = [ + "tcp://public.easytier.cn:11010" + "wss://example.com:443" + ]; + }; + extraSettings = { + flags.dev_name = "tun1"; + }; + }; + }; + }; + + config = mkIf cfg.enable { + environment.systemPackages = [ cfg.package ]; + + systemd.services = mapAttrs' ( + name: inst: + let + configFile = genConfigFile name inst; + in + nameValuePair "easytier-${name}" { + description = "EasyTier Daemon - ${name}"; + wants = [ + "network-online.target" + "nss-lookup.target" + ]; + after = [ + "network-online.target" + "nss-lookup.target" + ]; + wantedBy = [ "multi-user.target" ]; + path = with pkgs; [ + cfg.package + iproute2 + bash + ]; + restartTriggers = inst.environmentFiles ++ (optionals (inst.configServer == null) [ configFile ]); + serviceConfig = { + Type = "simple"; + Restart = "on-failure"; + EnvironmentFile = inst.environmentFiles; + StateDirectory = "easytier/easytier-${name}"; + StateDirectoryMode = "0700"; + WorkingDirectory = "/var/lib/easytier/easytier-${name}"; + ExecStart = escapeShellArgs ( + [ + "${cfg.package}/bin/easytier-core" + ] + ++ optionals (inst.configServer != null) ( + [ + "-w" + "${inst.configServer}" + ] + ++ (optionals (inst.settings.hostname != null) [ + "--hostname" + "${inst.settings.hostname}" + ]) + ) + ++ optionals (inst.configServer == null) [ + "-c" + "${configFile}" + ] + ++ inst.extraArgs + ); + }; + } + ) activeInsts; + + boot.kernel.sysctl = mkIf cfg.allowSystemForward { + "net.ipv4.conf.all.forwarding" = mkOverride 97 true; + "net.ipv6.conf.all.forwarding" = mkOverride 97 true; + }; + }; + + meta.maintainers = with maintainers; [ + ltrump + ]; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 60d118cba38f..9e66b5948d40 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -433,6 +433,7 @@ in fscrypt = runTest ./fscrypt.nix; fastnetmon-advanced = runTest ./fastnetmon-advanced.nix; lauti = runTest ./lauti.nix; + easytier = handleTest ./easytier.nix { }; ejabberd = runTest ./xmpp/ejabberd.nix; elk = handleTestOn [ "x86_64-linux" ] ./elk.nix { }; emacs-daemon = runTest ./emacs-daemon.nix; diff --git a/nixos/tests/easytier.nix b/nixos/tests/easytier.nix new file mode 100644 index 000000000000..e5366fb5ce98 --- /dev/null +++ b/nixos/tests/easytier.nix @@ -0,0 +1,121 @@ +import ./make-test-python.nix ( + { lib, ... }: + { + name = "easytier"; + meta.maintainers = with lib.maintainers; [ ltrump ]; + + nodes = + let + genPeer = + hostConfig: settings: + lib.mkMerge [ + { + services.easytier = { + enable = true; + instances.default = { + settings = { + network_name = "easytier_test"; + network_secret = "easytier_test_secret"; + } // settings; + }; + }; + + networking.useDHCP = false; + networking.firewall.allowedTCPPorts = [ + 11010 + 11011 + ]; + networking.firewall.allowedUDPPorts = [ + 11010 + 11011 + ]; + } + hostConfig + ]; + in + { + relay = + 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; + } + ]; + } + { + ipv4 = "10.144.144.1"; + listeners = [ + "tcp://0.0.0.0:11010" + "wss://0.0.0.0:11011" + ]; + }; + + peer1 = + genPeer + { + virtualisation.vlans = [ 1 ]; + } + { + ipv4 = "10.144.144.2"; + peers = [ "tcp://192.168.1.11:11010" ]; + }; + + peer2 = + genPeer + { + virtualisation.vlans = [ 2 ]; + } + { + ipv4 = "10.144.144.3"; + peers = [ "wss://192.168.2.11:11011" ]; + }; + }; + + testScript = '' + start_all() + + relay.wait_for_unit("easytier-default.service") + peer1.wait_for_unit("easytier-default.service") + peer2.wait_for_unit("easytier-default.service") + + # relay is accessible by the other hosts + peer1.succeed("ping -c5 192.168.1.11") + peer2.succeed("ping -c5 192.168.2.11") + + # The other hosts are in separate vlans + peer1.fail("ping -c5 192.168.2.11") + peer2.fail("ping -c5 192.168.1.11") + + # 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") + + # 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") + + # Relay can access the other hosts through EasyTier + relay.succeed("ping -c5 10.144.144.2") + relay.succeed("ping -c5 10.144.144.3") + + # 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") + ''; + } +) diff --git a/pkgs/by-name/ea/easytier/package.nix b/pkgs/by-name/ea/easytier/package.nix index 66f939a0d134..40c565d3d081 100644 --- a/pkgs/by-name/ea/easytier/package.nix +++ b/pkgs/by-name/ea/easytier/package.nix @@ -4,6 +4,7 @@ fetchFromGitHub, rustPlatform, protobuf, + nixosTests, nix-update-script, withQuic ? false, # with QUIC protocol support }: @@ -33,7 +34,10 @@ rustPlatform.buildRustPackage rec { doCheck = false; # tests failed due to heavy rely on network - passthru.updateScript = nix-update-script { }; + passthru = { + tests = { inherit (nixosTests) easytier; }; + updateScript = nix-update-script { }; + }; meta = { homepage = "https://github.com/EasyTier/EasyTier";