From 2af4e3982a10602cd0c60836c3f7a9b0f0495689 Mon Sep 17 00:00:00 2001 From: David Wronek Date: Wed, 10 Dec 2025 21:28:36 +0100 Subject: [PATCH] nixos-rebuild-ng: improve IPv6 support These changes improve support for using an IPv6 address for the target host. This approach is similar to the one found in nix [1]. Previously, nixos-rebuild-ng would pass a host string to `ssh` that cannot be parsed. Since the host string is being treated like a URL, we encode the scope ID symbol (%) as %25. This is required for compatibility with other nix commands (e.g. nix-copy-closure). Example: ``` nixos-rebuild \ --target-host "root@[fe80::9272:6aff:fe15:faf3%25enp4s0]" \ --flake path:/etc/nixos#xiatian -L \ switch ``` [1] https://github.com/NixOS/nix/blob/8b955d8/src/libstore/store-reference.cc#L143-L166 Signed-off-by: David Wronek --- .../src/nixos_rebuild/process.py | 22 +++++++++++++++++-- .../src/tests/test_process.py | 20 +++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py index 1f57111a72ef..902797189c69 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py @@ -7,6 +7,7 @@ import shlex import subprocess from collections.abc import Sequence from dataclasses import dataclass +from ipaddress import AddressValueError, IPv6Address from typing import Final, Self, TypedDict, Unpack from . import tmpdir @@ -63,6 +64,23 @@ class Remote: "'--ask-sudo-password' option instead" ) + def ssh_host(self) -> str: + """Fix up host string for SSH. + + It returns an SSH-compatible host string if the host is using an + IPv6 address, otherwise it returns the passed host string unmodified. + """ + try: + host_split = self.host.split("@") + host_fixed = host_split[-1].strip("[]").replace("%25", "%") + IPv6Address(host_fixed) + if len(host_split) > 1: + return host_split[0] + "@" + host_fixed + else: + return host_fixed + except AddressValueError: + return self.host + # Not exhaustive, but we can always extend it later. class RunKwargs(TypedDict, total=False): @@ -112,7 +130,7 @@ def run_wrapper( "ssh", *remote.opts, *SSH_DEFAULT_OPTS, - remote.host, + remote.ssh_host(), "--", # SSH will join the parameters here and pass it to the shell, so we # need to quote it to avoid issues. @@ -187,7 +205,7 @@ def _kill_long_running_ssh_process(args: Args, remote: Remote) -> None: "ssh", *remote.opts, *SSH_DEFAULT_OPTS, - remote.host, + remote.ssh_host(), "--", "pkill", "--signal", diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_process.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_process.py index 84eba633b21b..84b5c084bf38 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_process.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_process.py @@ -140,3 +140,23 @@ def test_remote_from_name(monkeypatch: MonkeyPatch) -> None: opts=["-f", "foo", "-b", "bar", "-t"], sudo_password="password", ) + + +def test_ssh_host() -> None: + remotes = { + "user@[fe80::1%25eth0]": "user@fe80::1%eth0", + "[fe80::c98b%25enp4s0]": "fe80::c98b%enp4s0", + "user@[2001::5fce:a:198]": "user@2001::5fce:a:198", + "[2001::dead:beef]": "2001::dead:beef", + "root@192.168.178.1": "root@192.168.178.1", + "192.168.178.1": "192.168.178.1", + "user@localhost": "user@localhost", + "localhost": "localhost", + "user@example.org": "user@example.org", + "example.org": "example.org", + } + + for host_input, expected in remotes.items(): + remote = m.Remote.from_arg(host_input, None, False) + assert remote is not None + assert remote.ssh_host() == expected