Commit 296e7f92 authored by woojiq's avatar woojiq Committed by pennae
Browse files

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: default avatarpennae <82953136+pennae@users.noreply.github.com>
parent a747c1d8
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -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).
+1 −0
Original line number Diff line number Diff line
@@ -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
+112 −0
Original line number Diff line number Diff line
{ 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";
      };
    };
  };
}
+1 −0
Original line number Diff line number Diff line
@@ -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 {};

nixos/tests/keyd.nix

0 → 100644
+82 −0
Original line number Diff line number Diff line
# 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)";
    };
  };
}
Loading