Skip to content

Commit 39145a1

Browse files
author
Gregory Price
committed
qemu: fall back to TCP host forwarding when vsock is unavailable
When vsock is not available (VSock=no or no /dev/vsock), dynamically allocate a host port and configure QEMU user-mode networking with hostfwd to forward SSH traffic over TCP loopback. Changes: - finalize_state() and register_machine() accept explicit proxy_command and ssh_address parameters, falling back to vsock defaults when a CID is present - Dynamic port allocation via socket.bind(("127.0.0.1", 0)) avoids collisions when multiple VMs run concurrently - Use socat TCP4 (not TCP) to avoid AI_ADDRCONFIG getaddrinfo failures inside the mkosi bubblewrap sandbox. - Update run_ssh() hint to no longer require Vsock=yes Signed-off-by: Gregory Price <gourry@gourry.net>
1 parent d09708e commit 39145a1

1 file changed

Lines changed: 37 additions & 8 deletions

File tree

mkosi/qemu.py

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -708,10 +708,17 @@ def finalize_initrd(config: Config) -> Iterator[Optional[Path]]:
708708

709709

710710
@contextlib.contextmanager
711-
def finalize_state(config: Config, cid: int) -> Iterator[None]:
711+
def finalize_state(
712+
config: Config,
713+
cid: Optional[int] = None,
714+
proxy_command: Optional[str] = None,
715+
) -> Iterator[None]:
712716
statedir = INVOKING_USER.runtime_dir() / "mkosi/machine"
713717
statedir.mkdir(parents=True, exist_ok=True)
714718

719+
if proxy_command is None and cid is not None:
720+
proxy_command = f"socat - VSOCK-CONNECT:{cid}:%p"
721+
715722
with flock(statedir):
716723
if (p := statedir / f"{config.machine_or_name()}.json").exists():
717724
state = json.loads(p.read_text())
@@ -727,7 +734,7 @@ def finalize_state(config: Config, cid: int) -> Iterator[None]:
727734
{
728735
"Machine": config.machine_or_name(),
729736
"Pid": os.getpid(),
730-
"ProxyCommand": f"socat - VSOCK-CONNECT:{cid}:%p",
737+
"ProxyCommand": proxy_command,
731738
"SshKey": os.fspath(config.ssh_key) if config.ssh_key else None,
732739
},
733740
sort_keys=True,
@@ -856,10 +863,19 @@ def finalize_register(config: Config) -> bool:
856863
return True
857864

858865

859-
def register_machine(config: Config, pid: int, fname: Path, cid: Optional[int]) -> None:
866+
def register_machine(
867+
config: Config,
868+
pid: int,
869+
fname: Path,
870+
cid: Optional[int],
871+
ssh_address: Optional[str] = None,
872+
) -> None:
860873
if not finalize_register(config):
861874
return
862875

876+
if ssh_address is None and cid is not None:
877+
ssh_address = f"vsock/{cid}"
878+
863879
run(
864880
[
865881
"varlinkctl",
@@ -874,7 +890,7 @@ def register_machine(config: Config, pid: int, fname: Path, cid: Optional[int])
874890
"leader": pid,
875891
**({"rootDirectory": os.fspath(fname)} if fname.is_dir() else {}),
876892
**({"vSockCid": cid} if cid is not None else {}),
877-
**({"sshAddress": f"vsock/{cid}"} if cid is not None else {}),
893+
**({"sshAddress": ssh_address} if ssh_address is not None else {}),
878894
**({"sshPrivateKeyPath": f"{config.ssh_key}"} if config.ssh_key else {}),
879895
**({"allocateUnit": True}),
880896
}
@@ -1035,8 +1051,16 @@ def run_qemu(args: Args, config: Config) -> None:
10351051
*shm,
10361052
] # fmt: skip
10371053

1054+
ssh_hostfwd_port: Optional[int] = None
10381055
if config.runtime_network == Network.user:
1039-
cmdline += ["-nic", f"user,model={config.architecture.default_qemu_nic_model()}"]
1056+
# Let the OS pick a free port for SSH host forwarding.
1057+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
1058+
s.bind(("127.0.0.1", 0))
1059+
ssh_hostfwd_port = s.getsockname()[1]
1060+
cmdline += [
1061+
"-nic",
1062+
f"user,model={config.architecture.default_qemu_nic_model()},hostfwd=tcp::{ssh_hostfwd_port}-:22",
1063+
]
10401064
elif config.runtime_network == Network.interface:
10411065
if os.getuid() != 0:
10421066
die("RuntimeNetwork=interface requires root privileges")
@@ -1318,8 +1342,13 @@ def add_virtiofs_mount(
13181342
cmdline += config.qemu_args
13191343
cmdline += args.cmdline
13201344

1321-
if cid is not None:
1322-
stack.enter_context(finalize_state(config, cid))
1345+
proxy_command: Optional[str] = None
1346+
if cid is None and ssh_hostfwd_port is not None:
1347+
proxy_command = f"socat - TCP4:127.0.0.1:{ssh_hostfwd_port}"
1348+
1349+
stack.enter_context(
1350+
finalize_state(config, cid=cid, proxy_command=proxy_command)
1351+
)
13231352

13241353
# Reopen stdin, stdout and stderr to give qemu a private copy of them. This is a mitigation for the
13251354
# case when running mkosi under meson and one or two of the three are redirected and their pipe might
@@ -1379,7 +1408,7 @@ def run_ssh(args: Args, config: Config) -> None:
13791408
if not (p := statedir / f"{config.machine_or_name()}.json").exists():
13801409
die(
13811410
f"{p} not found, cannot SSH into virtual machine {config.machine_or_name()}",
1382-
hint="Is the machine running and was it built with Ssh=yes and Vsock=yes?",
1411+
hint="Is the machine running and was it built with Ssh=yes?",
13831412
)
13841413

13851414
state = json.loads(p.read_text())

0 commit comments

Comments
 (0)