From d53e3a1b33c4694fb0308a2897f67cc289e5cdfc Mon Sep 17 00:00:00 2001 From: "Shahar \"Dawn\" Or" Date: Mon, 10 Jun 2024 14:47:49 +0700 Subject: [PATCH] vula: re-init at unstable-2024-05-17 Co-authored-by: Adrien Faure Co-authored-by: Ali Jamadi Co-authored-by: GetPsyched Co-authored-by: Robert James Hernandez Co-authored-by: Shahar "Dawn" Or Co-authored-by: Yifei Sun Co-authored-by: yakampe --- pkgs/by-name/highctidh/package.nix | 42 +++--- pkgs/by-name/vula/package.nix | 63 ++++----- projects/Vula/dbus.conf.nix | 60 +++++++++ projects/Vula/default.nix | 18 +-- projects/Vula/example-simple.nix | 4 + projects/Vula/nss-altfiles.nix | 32 +++++ projects/Vula/service.nix | 197 ++++++++++++++++++++++++----- projects/Vula/test.nix | 86 +++++++++++-- 8 files changed, 393 insertions(+), 109 deletions(-) create mode 100644 projects/Vula/dbus.conf.nix create mode 100644 projects/Vula/example-simple.nix create mode 100644 projects/Vula/nss-altfiles.nix diff --git a/pkgs/by-name/highctidh/package.nix b/pkgs/by-name/highctidh/package.nix index 60e47dae..95f6adaf 100644 --- a/pkgs/by-name/highctidh/package.nix +++ b/pkgs/by-name/highctidh/package.nix @@ -2,39 +2,33 @@ lib, python3, fetchgit, -}: -with builtins; let - python = python3; +}: let + inherit (lib) licenses maintainers; + + version = "1.0.2024060500"; + src = fetchgit { + url = "https://codeberg.org/vula/highctidh"; + rev = "v${version}"; + hash = "sha256-TyD5KzUz89RBxsSZeJYOkIzD29DF0BjizpMnsTpFOHI="; + }; in - python.pkgs.buildPythonApplication rec { + python3.pkgs.buildPythonPackage { pname = "highctidh"; - version = "1.0.2023121800"; - format = "pyproject"; - - src = fetchgit { - url = "https://codeberg.org/vula/highctidh"; - rev = "v${version}"; - hash = "sha256-83zTz5iBF/ApJV2hnsT2DfN/T36f73MrXmhLDJa5Z8I="; - }; + inherit version src; + pyproject = true; - postPatch = '' - patchShebangs test.sh - mkdir -p build/tmp - ''; + sourceRoot = "${src.name}/src"; - propagatedBuildInputs = with python.pkgs; [ + nativeBuildInputs = with python3.pkgs; [ setuptools - build ]; - nativeBuildInputs = propagatedBuildInputs; - - doCheck = true; + nativeCheckInputs = with python3.pkgs; [pytestCheckHook]; - meta = with lib; { - description = "Fork of high-ctidh as as a portable shared library with Python bindings."; + meta = { + description = "Fork of high-ctidh as as a portable shared library with Python bindings"; homepage = "https://codeberg.org/vula/highctidh"; license = licenses.publicDomain; - maintainers = with maintainers; [lorenzleutgeb]; + maintainers = with maintainers; [lorenzleutgeb mightyiam]; }; } diff --git a/pkgs/by-name/vula/package.nix b/pkgs/by-name/vula/package.nix index b3fe26a0..a68d06e2 100644 --- a/pkgs/by-name/vula/package.nix +++ b/pkgs/by-name/vula/package.nix @@ -3,64 +3,55 @@ python3, fetchgit, highctidh, - coreutils, -}: -with builtins; let - python = python3; + wrapGAppsHook, +}: let + inherit + (lib) + licenses + maintainers + ; in - python.pkgs.buildPythonApplication rec { + python3.pkgs.buildPythonApplication { pname = "vula"; - version = "0.2.2023112801"; - format = "pyproject"; + version = "unstable-2024-05-17"; src = fetchgit { url = "https://codeberg.org/vula/vula"; - rev = "v${version}"; - hash = "sha256-hBB6jKCLwgfPsgINuvGuLgihrr9zhG46V6/G0SXdCSc="; + rev = "b82933c2d45496afb91727e7ce3dff61ae262473"; + hash = "sha256-DVjEg28GFmA3fOgXZ8MQ7rwfZtt6WkK1qHnyTnYbKcY="; }; + # without removing `pyproject.toml` we don't end up with an executable. postPatch = '' - substituteInPlace configs/systemd/* \ - --replace 'ExecStart=vula' "ExecStart=$out/bin/vula" - - substituteInPlace configs/dbus/* \ - --replace 'Exec=/bin/false' "Exec=${coreutils}/bin/false" + rm pyproject.toml ''; - propagatedBuildInputs = with python.pkgs; - [ - setuptools - pyaudio - pyroute2 - hkdf - pynacl + propagatedBuildInputs = + (with python3.pkgs; [ click + cryptography + hkdf packaging - pyyaml - pystray - qrcode pillow pydbus - zeroconf + pynacl + pyroute2 + pyyaml + qrcode schema - cryptography tkinter - ] + zeroconf + ]) ++ [highctidh]; - postInstall = '' - mkdir -p $out/{lib/systemd/system,/share/dbus-1/system-services} - cp configs/systemd/* $out/lib/systemd/system/ - cp configs/dbus/* $out/share/dbus-1/system-services/ - ''; - - doCheck = true; + nativeBuildInputs = [wrapGAppsHook]; + nativeCheckInputs = with python3.pkgs; [pytestCheckHook]; - meta = with lib; { + meta = { description = "Automatic local network encryption"; homepage = "https://vula.link/"; license = licenses.gpl3Only; - maintainers = with maintainers; [lorenzleutgeb]; + maintainers = with maintainers; [lorenzleutgeb mightyiam stepbrobd]; mainProgram = "vula"; }; } diff --git a/projects/Vula/dbus.conf.nix b/projects/Vula/dbus.conf.nix new file mode 100644 index 00000000..1f31f4d0 --- /dev/null +++ b/projects/Vula/dbus.conf.nix @@ -0,0 +1,60 @@ +{ + userPrefix, + operatorsGroup, +}: '' + + + + system + + + + + + + + + + + + + + + + + + + + + + + + + + +'' diff --git a/projects/Vula/default.nix b/projects/Vula/default.nix index 00dc6f7d..5c32887f 100644 --- a/projects/Vula/default.nix +++ b/projects/Vula/default.nix @@ -1,11 +1,13 @@ -{ - pkgs, - lib, - sources, -} @ args: { +{pkgs, ...} @ args: { packages = {inherit (pkgs) vula;}; - nixos = { - modules.services.vula = ./service.nix; - tests.vula = import ./test.nix args; + nixos.modules.services.vula = ./service.nix; + nixos.tests.test = import ./test.nix args; + nixos.examples.simple = { + path = ./example-simple.nix; + description = '' + Simple configuration for Vula. Vula nodes will automatically discover each other on networks that support [multicast DNS](https://en.wikipedia.org/wiki/Multicast_DNS) (mDNS). + + Add users to the group defined in `config.services.vula.adminGroup` to grant them permissions to manage Vula through the `vula` command. + ''; }; } diff --git a/projects/Vula/example-simple.nix b/projects/Vula/example-simple.nix new file mode 100644 index 00000000..9554c0b6 --- /dev/null +++ b/projects/Vula/example-simple.nix @@ -0,0 +1,4 @@ +{ + services.vula.enable = true; + services.vula.openFirewall = true; +} diff --git a/projects/Vula/nss-altfiles.nix b/projects/Vula/nss-altfiles.nix new file mode 100644 index 00000000..2e17a860 --- /dev/null +++ b/projects/Vula/nss-altfiles.nix @@ -0,0 +1,32 @@ +# not exposed because vula uses specific non-release rev and some build flags +{ + stdenv, + fetchFromGitHub, + lib, +}: let + inherit (lib) licenses maintainers; +in + stdenv.mkDerivation { + pname = "nss-altfiles"; + version = "unstable-2020-09-25"; + + src = fetchFromGitHub { + owner = "flatcar"; + repo = "nss-altfiles"; + rev = "9078c543ba7d2bc5011737675b3dddb882673ce7"; + sha256 = "sha256-mkZtuUsahHcwcmXvdH2thhDP7ctT5/wDpd0YUSSfd5w="; + }; + + configureFlags = [ + "--with-types=hosts" + "--with-module-name='vula'" + "--datadir=/var/lib/vula-organize/" + ]; + + meta = { + description = "NSS module for relocating default file locations, tailored for Flatcar Container Linux"; + homepage = "https://github.com/flatcar/nss-altfiles"; + license = licenses.lgpl21Only; + maintainers = with maintainers; [mightyiam]; + }; + } diff --git a/projects/Vula/service.nix b/projects/Vula/service.nix index 423988d0..c1649efb 100644 --- a/projects/Vula/service.nix +++ b/projects/Vula/service.nix @@ -7,71 +7,208 @@ }: let inherit (lib) + assertMsg + getAttr + getExe + head types mkIf mkEnableOption mkOption + mkOrder mkPackageOption + recursiveUpdate + ; + inherit + (pkgs) + writeTextFile + coreutils + callPackage ; cfg = config.services.vula; - opt = options.services.vula; + + nss-altfiles = callPackage ./nss-altfiles.nix {}; + + nssModuleName = "vula"; + + logLevelFlag = + getAttr + cfg.logLevel + { + INFO = "--info"; + WARN = "--quiet"; + DEBUG = "--verbose"; + }; + + groupsAreUnique = assertMsg (cfg.operatorsGroup != cfg.systemGroup) "The options `config.services.vula.{systemGroup,operatorsGroup}` must have different values."; + + commonServiceAttrs = { + serviceConfig = { + DevicePolicy = "closed"; + Group = cfg.systemGroup; + LockPersonality = "yes"; + MemoryDenyWriteExecute = "yes"; + NoNewPrivileges = "yes"; + PrivateDevices = "yes"; + PrivateTmp = "yes"; + ProtectControlGroups = "yes"; + ProtectHome = "read-only"; + ProtectKernelLogs = "yes"; + ProtectKernelModules = "yes"; + ProtectKernelTunables = "yes"; + Restart = "always"; + RestartSec = "5s"; + RestrictNamespaces = "yes"; + RestrictRealtime = "yes"; + RestrictSUIDSGID = "yes"; + Slice = "vula.slice"; + StandardError = "journal"; + StandardOutput = "journal"; + Type = "dbus"; + }; + wantedBy = ["multi-user.target"]; + }; in { - options.services.vula = with types; { - enable = mkEnableOption "vula"; + options.services.vula = { + enable = mkEnableOption "Enables Vula, \"automatic local network encryption\". The wireguard kernel module is required."; package = mkPackageOption pkgs "vula" {}; - user = mkOption { - type = str; - description = "Username of the system user that should own files and services related to vula."; + userPrefix = mkOption { + type = types.str; + description = "Prefix for names of vula system users."; default = "vula"; }; - group = mkOption { - type = str; - description = "Group that contains the system user that executes vula."; + systemGroup = mkOption { + type = types.str; + description = "Group name for vula system users."; default = "vula"; }; - }; - config = mkIf cfg.enable { - users.users."${cfg.user}" = { - isSystemUser = true; - group = cfg.group; + operatorsGroup = mkOption { + type = types.str; + description = "Users in this group have full permissions to control vula."; + default = "vula-ops"; }; - users.users."${cfg.user}-discover" = { - isSystemUser = true; - group = cfg.group; + openFirewall = mkOption { + type = types.bool; + description = "Opens ports 5353 and 5354, and enables [option]`${options.networking.firewall.checkReversePath}`."; + default = false; }; - users.users."${cfg.user}-discover-alt" = { - isSystemUser = true; - group = cfg.group; + logLevel = mkOption { + type = types.enum ["INFO" "WARN" "DEBUG"]; + description = "Vula daemons log level."; + default = "INFO"; + example = "WARN"; }; + }; + + config = mkIf cfg.enable { + system.nssModules = ["${nss-altfiles}"]; + system.nssDatabases.hosts = mkOrder 0 [nssModuleName]; + + users.groups."${assert groupsAreUnique; cfg.systemGroup}" = {}; + users.groups."${assert groupsAreUnique; cfg.operatorsGroup}" = {}; - users.users."${cfg.user}-publish" = { + users.users."${cfg.userPrefix}-discover" = { isSystemUser = true; - group = cfg.group; + group = cfg.systemGroup; }; - users.users."${cfg.user}-publish-alt" = { + users.users."${cfg.userPrefix}-publish" = { isSystemUser = true; - group = cfg.group; + group = cfg.systemGroup; }; - users.users."${cfg.user}-organize" = { + users.users."${cfg.userPrefix}-organize" = { isSystemUser = true; - group = cfg.group; + group = cfg.systemGroup; }; - users.groups."${cfg.group}" = {}; - environment.systemPackages = [cfg.package]; - systemd.packages = [cfg.package]; + services.dbus.packages = + [ + (writeTextFile { + name = "vula-dbus.conf"; + destination = "/share/dbus-1/system.d/local.vula.services.conf"; + text = import ./dbus.conf.nix {inherit (cfg) userPrefix operatorsGroup;}; + }) + ] + ++ (map + (name: + writeTextFile { + name = "local.vula.${name}.service"; + destination = "/share/dbus-1/system-services/local.vula.${name}.service"; + text = '' + [D-BUS Service] + Name=local.vula.${name} + Exec=${coreutils}/bin/false + User=${cfg.userPrefix}-${name} + SystemdService=vula-${name}.service + ''; + }) + ["organize" "discover" "publish"]); + + networking.firewall.allowedUDPPorts = mkIf cfg.openFirewall [ + 5353 # mdns + 5354 # port vula uses wireguard with + ]; + + networking.firewall.checkReversePath = mkIf cfg.openFirewall "loose"; + + systemd.services.vula-organize = recursiveUpdate commonServiceAttrs { + description = "Vula organize service daemon"; + after = ["network.target" "vula-discover.target" "vula-publish.target"]; + serviceConfig.AmbientCapabilities = "CAP_NET_ADMIN"; + serviceConfig.BusName = "local.vula.organize"; + serviceConfig.CapabilityBoundingSet = "CAP_NET_ADMIN"; + serviceConfig.ExecStart = "${getExe cfg.package} ${logLevelFlag} organize"; + serviceConfig.IPAddressDeny = "any"; + serviceConfig.RestrictAddressFamilies = "AF_UNIX AF_NETLINK"; + serviceConfig.StateDirectory = "vula-organize"; + serviceConfig.TasksMax = "24"; + serviceConfig.User = "${cfg.userPrefix}-organize"; + }; + + systemd.services.vula-discover = recursiveUpdate commonServiceAttrs { + description = "Vula discover service daemon"; + after = ["network.target"]; + partOf = ["vula-discover.target"]; + serviceConfig.BusName = "local.vula.discover"; + serviceConfig.ExecStart = "${getExe cfg.package} ${logLevelFlag} discover"; + serviceConfig.IPAddressAllow = "multicast"; + serviceConfig.RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX AF_NETLINK"; + serviceConfig.TasksMax = "24"; + serviceConfig.User = "${cfg.userPrefix}-discover"; + }; + + systemd.services.vula-publish = recursiveUpdate commonServiceAttrs { + description = "Vula publish service daemon"; + after = ["network.target"]; + partOf = ["vula-publish.target"]; + serviceConfig.BusName = "local.vula.publish"; + serviceConfig.ExecStart = "${getExe cfg.package} ${logLevelFlag} publish"; + serviceConfig.IPAddressAllow = "multicast"; + serviceConfig.RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX AF_NETLINK"; + serviceConfig.TasksMax = "24"; + serviceConfig.User = "${cfg.userPrefix}-publish"; + }; + + systemd.slices.vula.description = "Slice for vula services"; + systemd.slices.vula.before = ["slices.target"]; + systemd.slices.vula.sliceConfig.MemoryMax = "512M"; + systemd.slices.vula.sliceConfig.TasksMax = "72"; - services.dbus.packages = [cfg.package]; + assertions = [ + { + assertion = (head config.system.nssDatabases.hosts) == nssModuleName; + message = "Vula name security requires that the first value in `config.system.nssDatabases.hosts` is `\"vula\"`."; + } + ]; }; } diff --git a/projects/Vula/test.nix b/projects/Vula/test.nix index c88c65f8..74f69b7b 100644 --- a/projects/Vula/test.nix +++ b/projects/Vula/test.nix @@ -1,21 +1,85 @@ { sources, - pkgs, + lib, ... -}: { +}: let + inherit (lib) recursiveUpdate mkForce; +in { name = "vula"; - nodes = { - server = {config, ...}: { - imports = [ - sources.modules.default - sources.modules."services.vula" - ]; - services.vula.enable = true; - }; + nodes.a = { + imports = [ + sources.modules.default + sources.modules."services.vula" + ]; + + services.vula.enable = true; + services.vula.openFirewall = true; + + services.vula.logLevel = "DEBUG"; + + # Make sure that if hosts can resolve each others' names, + # it is thanks to vula and the nss module it uses. + networking.extraHosts = mkForce ""; + + services.vula.userPrefix = "non-default-prefix"; + services.vula.systemGroup = "non-default-system-group"; + services.vula.operatorsGroup = "vula-managers"; + users.users.user.isNormalUser = true; + users.users.admin.isNormalUser = true; + users.users.admin.extraGroups = ["vula-managers"]; }; - testScript = {nodes, ...}: '' + nodes.b.imports = [ + sources.modules.default + sources.modules."services.vula" + ./example-simple.nix + ]; + + testScript = '' start_all() + a.wait_for_unit("vula-organize.service") + a.wait_for_unit("vula-publish.service") + a.wait_for_unit("vula-discover.service") + b.wait_for_unit("vula-organize.service") + b.wait_for_unit("vula-publish.service") + b.wait_for_unit("vula-discover.service") + + def test_peer(node): + peer = 'a.local.' if node.name == 'b' else 'b.local.' + node.wait_until_succeeds(f"ping -I vula -c 1 {peer}", timeout=60) + + peer_ip = node.succeed(f"getent hosts {peer}").split()[0] + route_result = node.succeed(f"ip route get {peer_ip}") + assert " dev vula " in route_result + + test_peer(a) + test_peer(b) + + a.succeed("pgrep --uid non-default-prefix-organize vula") + a.succeed("pgrep --uid non-default-prefix-discover vula") + a.succeed("pgrep --uid non-default-prefix-publish vula") + + group_count = a.succeed("pgrep --count --group non-default-system-group vula").strip() + assert group_count == "3", "vula process group count should be 3" + + # log level + a.succeed("pgrep --full -- 'vula.* --verbose organize'") + a.succeed("pgrep --full -- 'vula.* --verbose discover'") + a.succeed("pgrep --full -- 'vula.* --verbose publish'") + + a.fail("su - user -c 'vula status'") + a.succeed("su - admin -c 'vula status'") + + a.fail("su - user -c 'vula peer'") + a.succeed("su - admin -c 'vula peer'") + + a.fail("su - user -c 'vula prefs set pin_new_peers true'") + a.succeed("su - admin -c 'vula prefs set pin_new_peers true'") ''; + + interactive.nodes.b = { + users.users.admin.isNormalUser = true; + users.users.admin.extraGroups = ["vula-admins"]; + }; }