about summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
authorwoojiq <yurii.shymon@gmail.com>2023-03-15 15:15:38 +0200
committerpennae <82953136+pennae@users.noreply.github.com>2023-03-22 15:12:29 +0100
commit296e7f92cde7cd3cba094400d9bfdd614a04fcea (patch)
treea8069dec6bf8fdfee0560026ab8cc88628f9f045 /nixos
parenta747c1d84105d7de357c1fe91a79a610d8fbbb1e (diff)
keyd: add keyd service and test
The keyd package already exists, but without a systemd service.

Keyd requires write access to /var/run to create its socket. Currently
the directory it uses can be changed with an environment variable, but
the keyd repo state suggests that this may turn into a compile-time
option. with that set, and some supplementary groups added, we can run
the service under DynamicUser.

Co-authored-by: pennae <82953136+pennae@users.noreply.github.com>
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/release-notes/rl-2305.section.md2
-rw-r--r--nixos/modules/module-list.nix1
-rw-r--r--nixos/modules/services/hardware/keyd.nix112
-rw-r--r--nixos/tests/all-tests.nix1
-rw-r--r--nixos/tests/keyd.nix82
5 files changed, 198 insertions, 0 deletions
diff --git a/nixos/doc/manual/release-notes/rl-2305.section.md b/nixos/doc/manual/release-notes/rl-2305.section.md
index 801cd0c6dc8da..c09ebe7467af1 100644
--- a/nixos/doc/manual/release-notes/rl-2305.section.md
+++ b/nixos/doc/manual/release-notes/rl-2305.section.md
@@ -59,6 +59,8 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - [QDMR](https://dm3mat.darc.de/qdmr/), a GUI application and command line tool for programming DMR radios [programs.qdmr](#opt-programs.qdmr.enable)
 
+- [keyd](https://github.com/rvaiya/keyd), a key remapping daemon for linux. Available as [services.keyd](#opt-services.keyd.enable).
+
 - [v2rayA](https://v2raya.org), a Linux web GUI client of Project V which supports V2Ray, Xray, SS, SSR, Trojan and Pingtunnel. Available as [services.v2raya](options.html#opt-services.v2raya.enable).
 
 - [ulogd](https://www.netfilter.org/projects/ulogd/index.html), a userspace logging daemon for netfilter/iptables related logging. Available as [services.ulogd](options.html#opt-services.ulogd.enable).
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 9c28fb5d97549..6ebbe3ff2a098 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -511,6 +511,7 @@
   ./services/hardware/usbmuxd.nix
   ./services/hardware/usbrelayd.nix
   ./services/hardware/vdr.nix
+  ./services/hardware/keyd.nix
   ./services/home-automation/evcc.nix
   ./services/home-automation/home-assistant.nix
   ./services/home-automation/zigbee2mqtt.nix
diff --git a/nixos/modules/services/hardware/keyd.nix b/nixos/modules/services/hardware/keyd.nix
new file mode 100644
index 0000000000000..64c769405fabc
--- /dev/null
+++ b/nixos/modules/services/hardware/keyd.nix
@@ -0,0 +1,112 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.keyd;
+  settingsFormat = pkgs.formats.ini { };
+in
+{
+  options = {
+    services.keyd = {
+      enable = mkEnableOption (lib.mdDoc "keyd, a key remapping daemon");
+
+      ids = mkOption {
+        type = types.listOf types.string;
+        default = [ "*" ];
+        example = [ "*" "-0123:0456" ];
+        description = lib.mdDoc ''
+          Device identifiers, as shown by {manpage}`keyd(1)`.
+        '';
+      };
+
+      settings = mkOption {
+        type = settingsFormat.type;
+        default = { };
+        example = {
+          main = {
+            capslock = "overload(control, esc)";
+            rightalt = "layer(rightalt)";
+          };
+
+          rightalt = {
+            j = "down";
+            k = "up";
+            h = "left";
+            l = "right";
+          };
+        };
+        description = lib.mdDoc ''
+          Configuration, except `ids` section, that is written to {file}`/etc/keyd/default.conf`.
+          See <https://github.com/rvaiya/keyd> how to configure.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.etc."keyd/default.conf".source = pkgs.runCommand "default.conf"
+      {
+        ids = ''
+          [ids]
+          ${concatStringsSep "\n" cfg.ids}
+        '';
+        passAsFile = [ "ids" ];
+      } ''
+      cat $idsPath <(echo) ${settingsFormat.generate "keyd-main.conf" cfg.settings} >$out
+    '';
+
+    hardware.uinput.enable = lib.mkDefault true;
+
+    systemd.services.keyd = {
+      description = "Keyd remapping daemon";
+      documentation = [ "man:keyd(1)" ];
+
+      wantedBy = [ "multi-user.target" ];
+
+      restartTriggers = [
+        config.environment.etc."keyd/default.conf".source
+      ];
+
+      # this is configurable in 2.4.2, later versions seem to remove this option.
+      # post-2.4.2 may need to set makeFlags in the derivation:
+      #
+      #     makeFlags = [ "SOCKET_PATH/run/keyd/keyd.socket" ];
+      environment.KEYD_SOCKET = "/run/keyd/keyd.sock";
+
+      serviceConfig = {
+        ExecStart = "${pkgs.keyd}/bin/keyd";
+        Restart = "always";
+
+        DynamicUser = true;
+        SupplementaryGroups = [
+          config.users.groups.input.name
+          config.users.groups.uinput.name
+        ];
+
+        RuntimeDirectory = "keyd";
+
+        # Hardening
+        CapabilityBoundingSet = "";
+        DeviceAllow = [
+          "char-input rw"
+          "/dev/uinput rw"
+        ];
+        ProtectClock = true;
+        PrivateNetwork = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        PrivateUsers = true;
+        PrivateMounts = true;
+        RestrictNamespaces = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectControlGroups = true;
+        MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+        LockPersonality = true;
+        ProtectProc = "noaccess";
+        UMask = "0077";
+      };
+    };
+  };
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index ff2549395a0b2..d767408ed167b 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -346,6 +346,7 @@ in {
   keter = handleTest ./keter.nix {};
   kexec = handleTest ./kexec.nix {};
   keycloak = discoverTests (import ./keycloak.nix);
+  keyd = handleTest ./keyd.nix {};
   keymap = handleTest ./keymap.nix {};
   knot = handleTest ./knot.nix {};
   komga = handleTest ./komga.nix {};
diff --git a/nixos/tests/keyd.nix b/nixos/tests/keyd.nix
new file mode 100644
index 0000000000000..d492cc194895c
--- /dev/null
+++ b/nixos/tests/keyd.nix
@@ -0,0 +1,82 @@
+# The test template is taken from the `./keymap.nix`
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+
+let
+  readyFile = "/tmp/readerReady";
+  resultFile = "/tmp/readerResult";
+
+  testReader = pkgs.writeScript "test-input-reader" ''
+    rm -f ${resultFile} ${resultFile}.tmp
+    logger "testReader: START: Waiting for $1 characters, expecting '$2'."
+    touch ${readyFile}
+    read -r -N $1 chars
+    rm -f ${readyFile}
+    if [ "$chars" == "$2" ]; then
+      logger -s "testReader: PASS: Got '$2' as expected." 2>${resultFile}.tmp
+    else
+      logger -s "testReader: FAIL: Expected '$2' but got '$chars'." 2>${resultFile}.tmp
+    fi
+    # rename after the file is written to prevent a race condition
+    mv  ${resultFile}.tmp ${resultFile}
+  '';
+
+
+  mkKeyboardTest = name: { settings, test }: with pkgs.lib; makeTest {
+    inherit name;
+
+    nodes.machine = {
+      services.keyd = {
+        enable = true;
+        inherit settings;
+      };
+    };
+
+    testScript = ''
+      import shlex
+
+      machine.wait_for_unit("keyd.service")
+
+      def run_test_case(cmd, test_case_name, inputs, expected):
+          with subtest(test_case_name):
+              assert len(inputs) == len(expected)
+              machine.execute("rm -f ${readyFile} ${resultFile}")
+              # set up process that expects all the keys to be entered
+              machine.succeed(
+                  "{} {} {} {} >&2 &".format(
+                      cmd,
+                      "${testReader}",
+                      len(inputs),
+                      shlex.quote("".join(expected)),
+                  )
+              )
+              # wait for reader to be ready
+              machine.wait_for_file("${readyFile}")
+              # send all keys
+              for key in inputs:
+                  machine.send_key(key)
+              # wait for result and check
+              machine.wait_for_file("${resultFile}")
+              machine.succeed("grep -q 'PASS:' ${resultFile}")
+      test = ${builtins.toJSON test}
+      run_test_case("openvt -sw --", "${name}", test["press"], test["expect"])
+    '';
+  };
+
+in
+pkgs.lib.mapAttrs mkKeyboardTest {
+  swap-ab_and_ctrl-as-shift = {
+    test.press = [ "a" "ctrl-b" "c" ];
+    test.expect = [ "b" "A" "c" ];
+
+    settings.main = {
+      "a" = "b";
+      "b" = "a";
+      "control" = "oneshot(shift)";
+    };
+  };
+}