nixos/test-driver: add backdoor based on systemd-ssh-proxy & AF_VSOCK (#392030)

Changed files
+100 -4
nixos
doc
lib
test-driver
src
test_driver
testing
modules
+29
nixos/doc/manual/development/running-nixos-tests-interactively.section.md
···
Once the connection is established, you can enter commands in the socat terminal
where socat is running.
+
## SSH Access for test machines {#sec-nixos-test-ssh-access}
+
+
An SSH-based backdoor to log into machines can be enabled with
+
+
```nix
+
{
+
name = "…";
+
nodes.machines = { /* … */ };
+
sshBackdoor.enable = true;
+
}
+
```
+
+
This creates a [vsock socket](https://man7.org/linux/man-pages/man7/vsock.7.html)
+
for each VM to log in with SSH. This configures root login with an empty password.
+
+
When the VMs get started interactively with the test-driver, it's possible to
+
connect to `machine` with
+
+
```
+
$ ssh vsock/3 -o User=root
+
```
+
+
The socket numbers correspond to the node number of the test VM, but start
+
at three instead of one because that's the lowest possible
+
vsock number.
+
+
On non-NixOS systems you'll probably need to enable
+
the SSH config from {manpage}`systemd-ssh-proxy(1)` yourself.
+
## Port forwarding to NixOS test VMs {#sec-nixos-test-port-forwarding}
If your test has only a single VM, you may use e.g.
+6
nixos/doc/manual/redirects.json
···
"sec-test-options-reference": [
"index.html#sec-test-options-reference"
],
+
"test-opt-sshBackdoor.enable": [
+
"index.html#test-opt-sshBackdoor.enable"
+
],
"test-opt-defaults": [
"index.html#test-opt-defaults"
],
···
],
"sec-nixos-test-shell-access": [
"index.html#sec-nixos-test-shell-access"
+
],
+
"sec-nixos-test-ssh-access": [
+
"index.html#sec-nixos-test-ssh-access"
],
"sec-nixos-test-port-forwarding": [
"index.html#sec-nixos-test-port-forwarding"
+7
nixos/lib/test-driver/src/test_driver/__init__.py
···
help="the test script to run",
type=Path,
)
+
arg_parser.add_argument(
+
"--dump-vsocks",
+
help="indicates that the interactive SSH backdoor is active and dumps information about it on start",
+
action="store_true",
+
)
args = arg_parser.parse_args()
···
if args.interactive:
history_dir = os.getcwd()
history_path = os.path.join(history_dir, ".nixos-test-history")
+
if args.dump_vsocks:
+
driver.dump_machine_ssh()
ptpython.ipython.embed(
user_ns=driver.test_symbols(),
history_filename=history_path,
+15
nixos/lib/test-driver/src/test_driver/driver.py
···
from typing import Any
from unittest import TestCase
+
from colorama import Style
+
from test_driver.errors import MachineError, RequestedAssertionFailed
from test_driver.logger import AbstractLogger
from test_driver.machine import Machine, NixStartScript, retry
···
+ ", ".join(list(general_symbols.keys()))
)
return {**general_symbols, **machine_symbols, **vlan_symbols}
+
+
def dump_machine_ssh(self) -> None:
+
print("SSH backdoor enabled, the machines can be accessed like this:")
+
print(
+
f"{Style.BRIGHT}Note:{Style.RESET_ALL} this requires {Style.BRIGHT}systemd-ssh-proxy(1){Style.RESET_ALL} to be enabled (default on NixOS 25.05 and newer)."
+
)
+
names = [machine.name for machine in self.machines]
+
longest_name = len(max(names, key=len))
+
for num, name in enumerate(names, start=3):
+
spaces = " " * (longest_name - len(name) + 2)
+
print(
+
f" {name}:{spaces}{Style.BRIGHT}ssh -o User=root vsock/{num}{Style.RESET_ALL}"
+
)
def test_script(self) -> None:
"""Run the test script"""
+1
nixos/lib/testing-python.nix
···
),
extraPythonPackages ? (_: [ ]),
interactive ? { },
+
sshBackdoor ? { },
}@t:
let
testConfig =
+22 -4
nixos/lib/testing/nodes.nix
···
mapAttrs
mkDefault
mkIf
+
mkMerge
mkOption
mkForce
optional
···
{
options = {
+
sshBackdoor = {
+
enable = mkOption {
+
default = false;
+
type = types.bool;
+
description = "Whether to turn on the VSOCK-based access to all VMs. This provides an unauthenticated access intended for debugging.";
+
};
+
};
+
node.type = mkOption {
type = types.raw;
default = baseOS.type;
···
passthru.nodes = config.nodesCompat;
-
defaults = mkIf config.node.pkgsReadOnly {
-
nixpkgs.pkgs = config.node.pkgs;
-
imports = [ ../../modules/misc/nixpkgs/read-only.nix ];
-
};
+
extraDriverArgs = mkIf config.sshBackdoor.enable [
+
"--dump-vsocks"
+
];
+
+
defaults = mkMerge [
+
(mkIf config.node.pkgsReadOnly {
+
nixpkgs.pkgs = config.node.pkgs;
+
imports = [ ../../modules/misc/nixpkgs/read-only.nix ];
+
})
+
(mkIf config.sshBackdoor.enable {
+
testing.sshBackdoor.enable = true;
+
})
+
];
};
}
+20
nixos/modules/testing/test-instrumentation.nix
···
machine.switch_root() to leave stage 1 and proceed to stage 2
'';
+
sshBackdoor = {
+
enable = mkEnableOption "vsock-based ssh backdoor for the VM";
+
};
+
};
config = {
···
'';
}
];
+
+
services.openssh = mkIf config.testing.sshBackdoor.enable {
+
enable = true;
+
settings = {
+
PermitRootLogin = "yes";
+
PermitEmptyPasswords = "yes";
+
};
+
};
+
+
security.pam.services.sshd = mkIf config.testing.sshBackdoor.enable {
+
allowNullPassword = true;
+
};
systemd.services.backdoor = lib.mkMerge [
backdoorService
···
# we avoid defining attributes if not possible.
# TODO: refactor such that test-instrumentation can import qemu-vm
package = lib.mkDefault pkgs.qemu_test;
+
+
options = mkIf config.testing.sshBackdoor.enable [
+
"-device vhost-vsock-pci,guest-cid=${toString (config.virtualisation.test.nodeNumber + 2)}"
+
];
};
};