Skip to content

Commit

Permalink
keyd: add keyd service and test
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
woojiq and pennae committed Mar 22, 2023
1 parent a747c1d commit 296e7f9
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 0 deletions.
2 changes: 2 additions & 0 deletions nixos/doc/manual/release-notes/rl-2305.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
112 changes: 112 additions & 0 deletions nixos/modules/services/hardware/keyd.nix
Original file line number Diff line number Diff line change
@@ -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";
};
};
};
}
1 change: 1 addition & 0 deletions nixos/tests/all-tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 {};
Expand Down
82 changes: 82 additions & 0 deletions nixos/tests/keyd.nix
Original file line number Diff line number Diff line change
@@ -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)";
};
};
}
6 changes: 6 additions & 0 deletions pkgs/tools/inputmethods/keyd/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
, systemd
, runtimeShell
, python3
, nixosTests
}:

let
Expand Down Expand Up @@ -59,11 +60,16 @@ stdenv.mkDerivation rec {

enableParallelBuilding = true;

# post-2.4.2 may need this to unbreak the test
# makeFlags = [ "SOCKET_PATH/run/keyd/keyd.socket" ];

postInstall = ''
ln -sf ${lib.getExe appMap} $out/bin/${appMap.pname}
rm -rf $out/etc
'';

passthru.tests.keyd = nixosTests.keyd;

meta = with lib; {
description = "A key remapping daemon for linux.";
license = licenses.mit;
Expand Down

0 comments on commit 296e7f9

Please sign in to comment.