From 78f6fd424b2d35d80f7c78e0a862bca6e4d8944b Mon Sep 17 00:00:00 2001 From: Guofang Date: Thu, 29 Oct 2026 14:55:30 +0800 Subject: [PATCH] Read port forwards from per-VM forwards.conf files A QEMU config file section cannot hold more than one hostfwd entry, so the forwards move out of the ini files into qemu//forwards.conf, one "LABEL HOST_PORT GUEST_PORT" rule per line. The new vmforwards.py parses them, skips rules whose host port is taken and builds the -netdev argument, which every launcher now passes instead of relying on a netdev section in the ini or on -nic. The ini files move to qemu//config.ini and qemu//-.ini. The *_SSH_PORT / *_RDP_PORT variables and --rdp-port / --ssh-port still replace the host port of the SSH and RDP rules. Co-Authored-By: Claude Sonnet 5.5 --- README.md | 47 ++++++--- qemu/almalinux10/forwards.conf | 2 + qemu/{debian13.ini => debian13/config.ini} | 9 +- .../darwin-arm64.ini} | 2 +- qemu/debian13/forwards.conf | 2 + .../linux-amd64.ini} | 2 +- .../linux-arm64.ini} | 2 +- qemu/{fedora44.ini => fedora44/config.ini} | 9 +- .../darwin-arm64.ini} | 2 +- qemu/fedora44/forwards.conf | 2 + .../linux-amd64.ini} | 2 +- .../linux-arm64.ini} | 2 +- .../{ubuntu2604.ini => ubuntu2604/config.ini} | 9 +- .../darwin-arm64.ini} | 2 +- qemu/ubuntu2604/forwards.conf | 2 + .../linux-amd64.ini} | 2 +- .../linux-arm64.ini} | 2 +- qemu/{windows11.ini => windows11/config.ini} | 6 +- .../darwin-arm64.ini} | 2 +- qemu/windows11/forwards.conf | 5 + .../linux-amd64.ini} | 2 +- .../linux-arm64.ini} | 2 +- run-almalinux10.py | 22 ++++- run-debian13.py | 34 ++++--- run-fedora44.py | 32 ++++--- run-ubuntu2604.py | 36 ++++--- run-windows11.py | 62 +++++------- vmforwards.py | 96 +++++++++++++++++++ windows11-arm64-install-speed.md | 4 +- 29 files changed, 272 insertions(+), 131 deletions(-) create mode 100644 qemu/almalinux10/forwards.conf rename qemu/{debian13.ini => debian13/config.ini} (81%) rename qemu/{debian13-darwin-arm64.ini => debian13/darwin-arm64.ini} (95%) create mode 100644 qemu/debian13/forwards.conf rename qemu/{debian13-linux-amd64.ini => debian13/linux-amd64.ini} (89%) rename qemu/{debian13-linux-arm64.ini => debian13/linux-arm64.ini} (95%) rename qemu/{fedora44.ini => fedora44/config.ini} (81%) rename qemu/{fedora44-darwin-arm64.ini => fedora44/darwin-arm64.ini} (95%) create mode 100644 qemu/fedora44/forwards.conf rename qemu/{fedora44-linux-amd64.ini => fedora44/linux-amd64.ini} (89%) rename qemu/{fedora44-linux-arm64.ini => fedora44/linux-arm64.ini} (95%) rename qemu/{ubuntu2604.ini => ubuntu2604/config.ini} (81%) rename qemu/{ubuntu2604-darwin-arm64.ini => ubuntu2604/darwin-arm64.ini} (94%) create mode 100644 qemu/ubuntu2604/forwards.conf rename qemu/{ubuntu2604-linux-amd64.ini => ubuntu2604/linux-amd64.ini} (87%) rename qemu/{ubuntu2604-linux-arm64.ini => ubuntu2604/linux-arm64.ini} (94%) rename qemu/{windows11.ini => windows11/config.ini} (90%) rename qemu/{windows11-darwin-arm64.ini => windows11/darwin-arm64.ini} (98%) create mode 100644 qemu/windows11/forwards.conf rename qemu/{windows11-linux-amd64.ini => windows11/linux-amd64.ini} (95%) rename qemu/{windows11-linux-arm64.ini => windows11/linux-arm64.ini} (97%) create mode 100644 vmforwards.py diff --git a/README.md b/README.md index 4637c22..9d91fef 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,12 @@ original qcow2 image as its backing file. The base image therefore stays unchanged. Run `./run-debian13.py --help` for the supported environment variables. -The Debian VM hardware (8G memory, 8 CPUs, drives, network) is -defined in the QEMU config file `qemu/debian13.ini`; the machine type and -architecture-specific devices are in `qemu/debian13--.ini` +The Debian VM hardware (8G memory, 8 CPUs, drives) is +defined in the QEMU config file `qemu/debian13/config.ini`; the machine type and +architecture-specific devices are in `qemu/debian13/-.ini` (os `linux` or `darwin`, arch `amd64` or `arm64`), together with the accelerators. Edit these files to change the settings. The -launcher fills in the `${...}` file path placeholders of `qemu/debian13.ini`, +launcher fills in the `${...}` file path placeholders of `qemu/debian13/config.ini`, writes the result to `vm/debian13/qemu.ini` and starts QEMU with `-readconfig` for that file followed by the one for the host system and guest architecture. QEMU arguments given after `--` override the config files, for example @@ -122,9 +122,9 @@ On the first boot, cloud-init updates package metadata and installs Fedora's GNOME package group through its native package module, then reboots the guest. Keep the VM running until this initial installation and reboot have completed. -Like the Debian VM, the Fedora VM hardware (8G memory, 8 CPUs, drives, SSH -port `2224`) is defined in `qemu/fedora44.ini`, with the accelerators, machine type and -platform-specific devices in `qemu/fedora44--.ini`. Other Fedora settings use the `FEDORA_VM_` prefix. +Like the Debian VM, the Fedora VM hardware (8G memory, 8 CPUs, drives) is +defined in `qemu/fedora44/config.ini`, with the accelerators, machine type and +platform-specific devices in `qemu/fedora44/-.ini`. Other Fedora settings use the `FEDORA_VM_` prefix. Run `./run-fedora44.py --help` for the complete list of overrides. ## Ubuntu 26.04 @@ -150,9 +150,9 @@ On the first boot, cloud-init installs `ubuntu-desktop-minimal` (GNOME) and guest. Keep the VM running until this initial installation and reboot have completed. -The VM hardware (8G memory, 8 CPUs, drives, SSH port `2225`) is defined in -`qemu/ubuntu2604.ini`, with the accelerators, machine type and platform-specific -devices in `qemu/ubuntu2604--.ini`. Other Ubuntu +The VM hardware (8G memory, 8 CPUs, drives) is defined in +`qemu/ubuntu2604/config.ini`, with the accelerators, machine type and platform-specific +devices in `qemu/ubuntu2604/-.ini`. Other Ubuntu settings use the `UBUNTU_VM_` prefix. Run `./run-ubuntu2604.py --help` for the complete list of overrides. @@ -182,10 +182,10 @@ seed ISO also carries the host's SSH public key (`$SSH_PUBLIC_KEY_FILE`, else th first of `~/.ssh/id_ed25519.pub`, `id_ecdsa.pub`, `id_rsa.pub`) for passwordless administrator login. The Windows VM hardware (8G memory, 8 CPUs, system disk, devices) -is defined in `qemu/windows11.ini`, with the accelerators and machine type in -`qemu/windows11--.ini`; the launcher renders the former to +is defined in `qemu/windows11/config.ini`, with the accelerators and machine type in +`qemu/windows11/-.ini`; the launcher renders the former to `vm/windows11/qemu.ini` and loads both with `-readconfig`. Only the network -and the installation media are still set by the launcher. QEMU arguments after +(see Port forwards) and the installation media are still set by the launcher. QEMU arguments after `--` override the config files, for example `./run-windows11.py -- -m 4G -smp 4`. The generated answer-file ISO is stored beside the disk as `vm/windows11/seed.iso`. @@ -202,3 +202,24 @@ is elsewhere: ```bash WIN11_VM_TOOLS_ISO=/custom/path/virtio-win.iso ./run-windows11.py --install-from-iso ``` + +## Port forwards + +The host ports forwarded to the guest are not in the QEMU config files (a +config file section cannot hold more than one `hostfwd`) but in one file per +VM, `qemu//forwards.conf`, which the launcher turns into the `-netdev` +argument. Each line is `LABEL HOST_PORT GUEST_PORT`; `#` starts a comment: + +``` +# qemu/windows11/forwards.conf +RDP 3389 3389 +SSH 2222 22 +``` + +Add a line to forward another port. A rule whose host port is already in use +is skipped with a warning. `ALMA_VM_FORWARDS`, `FEDORA_VM_FORWARDS`, +`UBUNTU_VM_FORWARDS`, `VM_FORWARDS` (Debian) and `WIN11_VM_FORWARDS` select +another file. `ALMA_VM_SSH_PORT`, `WIN11_VM_RDP_PORT`, `WIN11_VM_SSH_PORT` +and `--rdp-port`/`--ssh-port` still replace the host port of the rule with the +label `SSH` or `RDP`. The launchers import `vmforwards.py` from the repository +root. diff --git a/qemu/almalinux10/forwards.conf b/qemu/almalinux10/forwards.conf new file mode 100644 index 0000000..f788b09 --- /dev/null +++ b/qemu/almalinux10/forwards.conf @@ -0,0 +1,2 @@ +# Host ports forwarded to the guest: LABEL HOST_PORT GUEST_PORT +SSH 2223 22 diff --git a/qemu/debian13.ini b/qemu/debian13/config.ini similarity index 81% rename from qemu/debian13.ini rename to qemu/debian13/config.ini index 3899ee7..06c4301 100644 --- a/qemu/debian13.ini +++ b/qemu/debian13/config.ini @@ -1,6 +1,6 @@ # QEMU configuration for the Debian 13 virtual machine, shared by all # architectures. The accelerators, machine type and -# platform-specific devices are in debian13--.ini (os is linux or darwin), +# platform-specific devices are in -.ini (os is linux or darwin), # which QEMU reads after this file. # # run-debian13.py fills in the file path placeholders below, writes the result @@ -50,10 +50,9 @@ driver = "virtio-blk-pci" drive = "seed0" -# User-mode networking with guest SSH forwarded to the host. -[netdev "net0"] - type = "user" - hostfwd = "tcp::2222-:22" +# The NIC is attached to the user-mode network "net0", which the launcher +# defines with -netdev from qemu/debian13/forwards.conf because a config file +# section cannot hold more than one hostfwd. [device "nic0"] driver = "virtio-net-pci" diff --git a/qemu/debian13-darwin-arm64.ini b/qemu/debian13/darwin-arm64.ini similarity index 95% rename from qemu/debian13-darwin-arm64.ini rename to qemu/debian13/darwin-arm64.ini index 6eddee8..4c4df03 100644 --- a/qemu/debian13-darwin-arm64.ini +++ b/qemu/debian13/darwin-arm64.ini @@ -1,4 +1,4 @@ -# macOS host, arm64 part of the Debian 13 QEMU configuration; read after debian13.ini. +# macOS host, arm64 part of the Debian 13 QEMU configuration; read after config.ini. # Accelerators are tried in order: HVF on macOS, else emulation. [accel] diff --git a/qemu/debian13/forwards.conf b/qemu/debian13/forwards.conf new file mode 100644 index 0000000..b5ad56a --- /dev/null +++ b/qemu/debian13/forwards.conf @@ -0,0 +1,2 @@ +# Host ports forwarded to the guest: LABEL HOST_PORT GUEST_PORT +SSH 2222 22 diff --git a/qemu/debian13-linux-amd64.ini b/qemu/debian13/linux-amd64.ini similarity index 89% rename from qemu/debian13-linux-amd64.ini rename to qemu/debian13/linux-amd64.ini index b13077b..911325a 100644 --- a/qemu/debian13-linux-amd64.ini +++ b/qemu/debian13/linux-amd64.ini @@ -1,4 +1,4 @@ -# Linux host, amd64 part of the Debian 13 QEMU configuration; read after debian13.ini. +# Linux host, amd64 part of the Debian 13 QEMU configuration; read after config.ini. # Accelerators are tried in order: KVM on Linux, else emulation. [accel] diff --git a/qemu/debian13-linux-arm64.ini b/qemu/debian13/linux-arm64.ini similarity index 95% rename from qemu/debian13-linux-arm64.ini rename to qemu/debian13/linux-arm64.ini index 86def34..bd23a8f 100644 --- a/qemu/debian13-linux-arm64.ini +++ b/qemu/debian13/linux-arm64.ini @@ -1,4 +1,4 @@ -# Linux host, arm64 part of the Debian 13 QEMU configuration; read after debian13.ini. +# Linux host, arm64 part of the Debian 13 QEMU configuration; read after config.ini. # Accelerators are tried in order: KVM on Linux, else emulation. [accel] diff --git a/qemu/fedora44.ini b/qemu/fedora44/config.ini similarity index 81% rename from qemu/fedora44.ini rename to qemu/fedora44/config.ini index f98037e..9226d38 100644 --- a/qemu/fedora44.ini +++ b/qemu/fedora44/config.ini @@ -1,6 +1,6 @@ # QEMU configuration for the Fedora 44 virtual machine, shared by all # architectures. The accelerators, machine type and -# platform-specific devices are in fedora44--.ini (os is linux or darwin), +# platform-specific devices are in -.ini (os is linux or darwin), # which QEMU reads after this file. # # run-fedora44.py fills in the file path placeholders below, writes the result @@ -50,10 +50,9 @@ driver = "virtio-blk-pci" drive = "seed0" -# User-mode networking with guest SSH forwarded to the host. -[netdev "net0"] - type = "user" - hostfwd = "tcp::2224-:22" +# The NIC is attached to the user-mode network "net0", which the launcher +# defines with -netdev from qemu/fedora44/forwards.conf because a config file +# section cannot hold more than one hostfwd. [device "nic0"] driver = "virtio-net-pci" diff --git a/qemu/fedora44-darwin-arm64.ini b/qemu/fedora44/darwin-arm64.ini similarity index 95% rename from qemu/fedora44-darwin-arm64.ini rename to qemu/fedora44/darwin-arm64.ini index 20c8426..2c5fb6d 100644 --- a/qemu/fedora44-darwin-arm64.ini +++ b/qemu/fedora44/darwin-arm64.ini @@ -1,4 +1,4 @@ -# macOS host, arm64 part of the Fedora 44 QEMU configuration; read after fedora44.ini. +# macOS host, arm64 part of the Fedora 44 QEMU configuration; read after config.ini. # Accelerators are tried in order: HVF on macOS, else emulation. [accel] diff --git a/qemu/fedora44/forwards.conf b/qemu/fedora44/forwards.conf new file mode 100644 index 0000000..bf1c34f --- /dev/null +++ b/qemu/fedora44/forwards.conf @@ -0,0 +1,2 @@ +# Host ports forwarded to the guest: LABEL HOST_PORT GUEST_PORT +SSH 2224 22 diff --git a/qemu/fedora44-linux-amd64.ini b/qemu/fedora44/linux-amd64.ini similarity index 89% rename from qemu/fedora44-linux-amd64.ini rename to qemu/fedora44/linux-amd64.ini index a72a827..4c3f91d 100644 --- a/qemu/fedora44-linux-amd64.ini +++ b/qemu/fedora44/linux-amd64.ini @@ -1,4 +1,4 @@ -# Linux host, amd64 part of the Fedora 44 QEMU configuration; read after fedora44.ini. +# Linux host, amd64 part of the Fedora 44 QEMU configuration; read after config.ini. # Accelerators are tried in order: KVM on Linux, else emulation. [accel] diff --git a/qemu/fedora44-linux-arm64.ini b/qemu/fedora44/linux-arm64.ini similarity index 95% rename from qemu/fedora44-linux-arm64.ini rename to qemu/fedora44/linux-arm64.ini index f03985e..ac5351b 100644 --- a/qemu/fedora44-linux-arm64.ini +++ b/qemu/fedora44/linux-arm64.ini @@ -1,4 +1,4 @@ -# Linux host, arm64 part of the Fedora 44 QEMU configuration; read after fedora44.ini. +# Linux host, arm64 part of the Fedora 44 QEMU configuration; read after config.ini. # Accelerators are tried in order: KVM on Linux, else emulation. [accel] diff --git a/qemu/ubuntu2604.ini b/qemu/ubuntu2604/config.ini similarity index 81% rename from qemu/ubuntu2604.ini rename to qemu/ubuntu2604/config.ini index bbaf7e7..d033c75 100644 --- a/qemu/ubuntu2604.ini +++ b/qemu/ubuntu2604/config.ini @@ -1,6 +1,6 @@ # QEMU configuration for the Ubuntu 26.04 virtual machine, shared by all # architectures. The accelerators, machine type and -# platform-specific devices are in ubuntu2604--.ini (os is linux or darwin), +# platform-specific devices are in -.ini (os is linux or darwin), # which QEMU reads after this file. # # run-ubuntu2604.py fills in the file path placeholders below, writes the result @@ -50,10 +50,9 @@ driver = "virtio-blk-pci" drive = "seed0" -# User-mode networking with guest SSH forwarded to the host. -[netdev "net0"] - type = "user" - hostfwd = "tcp::2225-:22" +# The NIC is attached to the user-mode network "net0", which the launcher +# defines with -netdev from qemu/ubuntu2604/forwards.conf because a config file +# section cannot hold more than one hostfwd. [device "nic0"] driver = "virtio-net-pci" diff --git a/qemu/ubuntu2604-darwin-arm64.ini b/qemu/ubuntu2604/darwin-arm64.ini similarity index 94% rename from qemu/ubuntu2604-darwin-arm64.ini rename to qemu/ubuntu2604/darwin-arm64.ini index 105c1ed..9c78fc4 100644 --- a/qemu/ubuntu2604-darwin-arm64.ini +++ b/qemu/ubuntu2604/darwin-arm64.ini @@ -1,4 +1,4 @@ -# macOS host, arm64 part of the Ubuntu 26.04 QEMU configuration; read after ubuntu2604.ini. +# macOS host, arm64 part of the Ubuntu 26.04 QEMU configuration; read after config.ini. # Accelerators are tried in order: HVF on macOS, else emulation. [accel] diff --git a/qemu/ubuntu2604/forwards.conf b/qemu/ubuntu2604/forwards.conf new file mode 100644 index 0000000..536f427 --- /dev/null +++ b/qemu/ubuntu2604/forwards.conf @@ -0,0 +1,2 @@ +# Host ports forwarded to the guest: LABEL HOST_PORT GUEST_PORT +SSH 2225 22 diff --git a/qemu/ubuntu2604-linux-amd64.ini b/qemu/ubuntu2604/linux-amd64.ini similarity index 87% rename from qemu/ubuntu2604-linux-amd64.ini rename to qemu/ubuntu2604/linux-amd64.ini index b7dba90..c61cf28 100644 --- a/qemu/ubuntu2604-linux-amd64.ini +++ b/qemu/ubuntu2604/linux-amd64.ini @@ -1,4 +1,4 @@ -# Linux host, amd64 part of the Ubuntu 26.04 QEMU configuration; read after ubuntu2604.ini. +# Linux host, amd64 part of the Ubuntu 26.04 QEMU configuration; read after config.ini. # Accelerators are tried in order: KVM on Linux, else emulation. [accel] diff --git a/qemu/ubuntu2604-linux-arm64.ini b/qemu/ubuntu2604/linux-arm64.ini similarity index 94% rename from qemu/ubuntu2604-linux-arm64.ini rename to qemu/ubuntu2604/linux-arm64.ini index 6b4d3c7..c384756 100644 --- a/qemu/ubuntu2604-linux-arm64.ini +++ b/qemu/ubuntu2604/linux-arm64.ini @@ -1,4 +1,4 @@ -# Linux host, arm64 part of the Ubuntu 26.04 QEMU configuration; read after ubuntu2604.ini. +# Linux host, arm64 part of the Ubuntu 26.04 QEMU configuration; read after config.ini. # Accelerators are tried in order: KVM on Linux, else emulation. [accel] diff --git a/qemu/windows11.ini b/qemu/windows11/config.ini similarity index 90% rename from qemu/windows11.ini rename to qemu/windows11/config.ini index b39b9f5..2f9401b 100644 --- a/qemu/windows11.ini +++ b/qemu/windows11/config.ini @@ -1,5 +1,5 @@ # QEMU configuration for the Windows 11 virtual machine. The accelerators and machine type -# are in windows11--.ini (os is linux or darwin), which QEMU reads after +# are in -.ini (os is linux or darwin), which QEMU reads after # this file. # # run-windows11.py fills in the file path placeholders below, writes the result @@ -30,8 +30,8 @@ # System disk image; the device that exposes it (and the NIC, which is # attached to the user-mode network "net0" that the launcher defines with -# -netdev because a config file section cannot hold more than one hostfwd) is in -# the architecture file. +# -netdev from qemu/windows11/forwards.conf because a config file section cannot hold +# more than one hostfwd) is in the architecture file. [drive "system_disk"] if = "none" format = "qcow2" diff --git a/qemu/windows11-darwin-arm64.ini b/qemu/windows11/darwin-arm64.ini similarity index 98% rename from qemu/windows11-darwin-arm64.ini rename to qemu/windows11/darwin-arm64.ini index 70fa6bf..86c4c8f 100644 --- a/qemu/windows11-darwin-arm64.ini +++ b/qemu/windows11/darwin-arm64.ini @@ -1,4 +1,4 @@ -# macOS host, arm64 part of the Windows 11 QEMU configuration; read after windows11.ini. +# macOS host, arm64 part of the Windows 11 QEMU configuration; read after config.ini. # Accelerators are tried in order: HVF on macOS, else emulation. [accel] diff --git a/qemu/windows11/forwards.conf b/qemu/windows11/forwards.conf new file mode 100644 index 0000000..628e3b5 --- /dev/null +++ b/qemu/windows11/forwards.conf @@ -0,0 +1,5 @@ +# Host ports forwarded to the guest: LABEL HOST_PORT GUEST_PORT +# --rdp-port and --ssh-port (or WIN11_VM_RDP_PORT and WIN11_VM_SSH_PORT) +# replace the host port of the RDP and SSH rules. +RDP 3389 3389 +SSH 2222 22 diff --git a/qemu/windows11-linux-amd64.ini b/qemu/windows11/linux-amd64.ini similarity index 95% rename from qemu/windows11-linux-amd64.ini rename to qemu/windows11/linux-amd64.ini index 3fbf28c..7a91e5b 100644 --- a/qemu/windows11-linux-amd64.ini +++ b/qemu/windows11/linux-amd64.ini @@ -1,4 +1,4 @@ -# Linux host, amd64 part of the Windows 11 QEMU configuration; read after windows11.ini. +# Linux host, amd64 part of the Windows 11 QEMU configuration; read after config.ini. # Accelerators are tried in order: KVM on Linux, else emulation. [accel] diff --git a/qemu/windows11-linux-arm64.ini b/qemu/windows11/linux-arm64.ini similarity index 97% rename from qemu/windows11-linux-arm64.ini rename to qemu/windows11/linux-arm64.ini index 8de02f9..b3925e2 100644 --- a/qemu/windows11-linux-arm64.ini +++ b/qemu/windows11/linux-arm64.ini @@ -1,4 +1,4 @@ -# Linux host, arm64 part of the Windows 11 QEMU configuration; read after windows11.ini. +# Linux host, arm64 part of the Windows 11 QEMU configuration; read after config.ini. # Accelerators are tried in order: KVM on Linux, else emulation. [accel] diff --git a/run-almalinux10.py b/run-almalinux10.py index 603315a..c157f30 100755 --- a/run-almalinux10.py +++ b/run-almalinux10.py @@ -8,6 +8,8 @@ import subprocess import sys from pathlib import Path +import vmforwards + script_dir = Path(__file__).resolve().parent env = os.environ.get @@ -55,7 +57,9 @@ state_dir = Path(env("ALMA_VM_STATE_DIR") or script_dir / "vm" / "almalinux10") disk_size = env("ALMA_VM_DISK_SIZE") or "20G" memory = env("ALMA_VM_MEMORY") or "8G" cpus = env("ALMA_VM_CPUS") or "8" -ssh_port = env("ALMA_VM_SSH_PORT") or "2223" +ssh_port = env("ALMA_VM_SSH_PORT") +forwards_file = Path( + env("ALMA_VM_FORWARDS") or script_dir / "qemu" / "almalinux10" / "forwards.conf") def normalize_arch(arch): @@ -127,6 +131,8 @@ Usage: {Path(sys.argv[0]).name} [--daemon] [--download-only] [--state-dir DIR] [ Start an AlmaLinux 10 Generic Cloud virtual machine. The launcher obtains a missing base image through ../vm-images/download-images.sh and creates a writable overlay in the state directory, leaving the base image unchanged. +Host port forwards, one "LABEL HOST_PORT GUEST_PORT" rule per line, are read +from qemu/almalinux10/forwards.conf. Options: --daemon Run in the background while keeping the graphical window @@ -138,7 +144,9 @@ Options: Environment variables: ALMA_VM_MEMORY Guest memory (default: 8G) ALMA_VM_CPUS Virtual CPU count (default: 8) - ALMA_VM_SSH_PORT Host port forwarded to guest SSH (default: 2223) + ALMA_VM_SSH_PORT Host port of the SSH rule in the port forward file + (default: from the file, 2223) + ALMA_VM_FORWARDS Port forward file (default: qemu/almalinux10/forwards.conf) ALMA_VM_BASE_IMAGE Base qcow2 image path (default: resolved by shared image helper) ALMA_VM_SEED_IMAGE cloud-init seed ISO path (default: alongside the VM disk) ALMA_VM_STATE_DIR Runtime state directory (default: vm/almalinux10) @@ -240,6 +248,9 @@ if not disk_image.is_file(): is_macos = platform.system() == "Darwin" display_backend = env("ALMA_VM_DISPLAY") or ("cocoa" if is_macos else "gtk") +forwards = vmforwards.usable_forwards(vmforwards.load_forwards( + forwards_file, {"SSH": ssh_port} if ssh_port else None)) + qemu_args = [ "-name", "almalinux10", "-machine", machine_type, @@ -252,7 +263,8 @@ qemu_args = [ "-device", "virtio-blk-pci,drive=disk0", "-drive", f"file={seed_image},if=none,id=seed0,format=raw,readonly=on", "-device", "virtio-blk-pci,drive=seed0", - "-nic", f"user,model=virtio-net-pci,hostfwd=tcp::{ssh_port}-:22", + "-netdev", vmforwards.netdev_argument(forwards), + "-device", "virtio-net-pci,netdev=net0", ] if machine_type == "virt": @@ -274,8 +286,8 @@ else: qemu_args += ["-accel", "tcg", "-cpu", "max"] accelerator = "TCG" -print(f"Starting AlmaLinux 10 ({host_arch}) with {accelerator} acceleration; " - f"SSH is forwarded to localhost:{ssh_port}.", flush=True) +print(f"Starting AlmaLinux 10 ({host_arch}) with {accelerator} acceleration.", flush=True) +vmforwards.print_forwards(forwards) if daemonize: log_file.write_text("") diff --git a/run-debian13.py b/run-debian13.py index 041f09f..934e87f 100755 --- a/run-debian13.py +++ b/run-debian13.py @@ -3,13 +3,14 @@ import os import platform -import re import shutil import string import subprocess import sys from pathlib import Path +import vmforwards + script_dir = Path(__file__).resolve().parent env = os.environ.get @@ -68,11 +69,13 @@ def normalize_arch(arch): native_arch = normalize_arch(platform.machine()) host_arch = normalize_arch(env("VM_IMAGE_ARCH") or native_arch) qemu_binary = "qemu-system-aarch64" if host_arch == "arm64" else "qemu-system-x86_64" -qemu_config_template = Path(env("VM_QEMU_CONFIG") or script_dir / "qemu" / "debian13.ini") +qemu_config_template = Path(env("VM_QEMU_CONFIG") or script_dir / "qemu" / "debian13" / "config.ini") host_os = platform.system().lower() if host_os == "darwin" and host_arch == "amd64": die("amd64 guests are not supported on macOS hosts; use an arm64 image") -qemu_platform_config = script_dir / "qemu" / f"debian13-{host_os}-{host_arch}.ini" +qemu_platform_config = script_dir / "qemu" / "debian13" / f"{host_os}-{host_arch}.ini" +forwards_file = Path( + env("VM_FORWARDS") or script_dir / "qemu" / "debian13" / "forwards.conf") def pick_firmware(override, *candidates): @@ -125,10 +128,12 @@ Usage: {Path(sys.argv[0]).name} [--daemon] [--download-only] [--state-dir DIR] [ Start the Debian 13 virtual machine. A writable overlay is created in the state directory (default: vm/debian13/), so the downloaded base image is never -modified. The VM hardware (memory, CPUs, SSH port forward, ...) is defined in -qemu/debian13.ini, which is rendered to qemu.ini in the state directory and -loaded with QEMU's -readconfig, followed by qemu/debian13--.ini (os is linux or darwin, arch is amd64 or arm64). Later +modified. The VM hardware (memory, CPUs, ...) is defined in +qemu/debian13/config.ini, which is rendered to qemu.ini in the state directory and +loaded with QEMU's -readconfig, followed by qemu/debian13/-.ini (os is linux or darwin, arch is amd64 or arm64). Later QEMU arguments override them, e.g. `-- -m 4G -smp 4`. +Host port forwards, one "LABEL HOST_PORT GUEST_PORT" rule per line, are +read from qemu/debian13/forwards.conf. Options: --daemon Run in the background while keeping the graphical window @@ -148,7 +153,8 @@ Environment variables: VM_UEFI_VARS UEFI variable-store template path (auto-detected for the selected architecture) VM_DISPLAY QEMU display backend (default: cocoa on macOS, gtk elsewhere) VM_QEMU_CONFIG QEMU config template rendered into the state directory and - passed to -readconfig (default: qemu/debian13.ini) + passed to -readconfig (default: qemu/debian13/config.ini) + VM_FORWARDS Port forward file (default: qemu/debian13/forwards.conf) """) @@ -264,24 +270,24 @@ except (KeyError, ValueError) as error: die(f"invalid placeholder in {qemu_config_template}: {error}") qemu_config.write_text(qemu_config_text) +forwards = vmforwards.usable_forwards(vmforwards.load_forwards(forwards_file)) + qemu_args = [ "-readconfig", str(qemu_config), "-readconfig", str(qemu_platform_config), # "max" is the host CPU under KVM/HVF and the fullest model under TCG. "-cpu", "max", - # The config file's [netdev] does not stop QEMU from adding its default - # NIC, a second user network without the SSH port forward. + # A config file section cannot hold two hostfwd entries, so the network + # that the NIC in the config file uses is defined here. -nic none stops + # QEMU from adding its default NIC as well. + "-netdev", vmforwards.netdev_argument(forwards), "-nic", "none", "-display", display_backend, ] print(f"Starting {host_arch} VM from {qemu_config_template} " f"and {qemu_platform_config}.", flush=True) -# The SSH port forward lives in the config file, e.g. hostfwd = "tcp::2222-:22". -ssh_forward = re.search(r'hostfwd\s*=\s*"tcp:[^:"]*:(\d+)-[^:"]*:22"', qemu_config_text) -if ssh_forward: - print(f"SSH is forwarded to localhost:{ssh_forward.group(1)}: " - f"ssh -p {ssh_forward.group(1)} guofang@localhost", flush=True) +vmforwards.print_forwards(forwards) if daemonize: log_file.write_text("") diff --git a/run-fedora44.py b/run-fedora44.py index 50a179b..6d6ac13 100755 --- a/run-fedora44.py +++ b/run-fedora44.py @@ -3,13 +3,14 @@ import os import platform -import re import shutil import string import subprocess import sys from pathlib import Path +import vmforwards + script_dir = Path(__file__).resolve().parent env = os.environ.get @@ -69,11 +70,13 @@ native_arch = normalize_arch(platform.machine()) host_arch = normalize_arch(env("VM_IMAGE_ARCH") or native_arch) qemu_binary = "qemu-system-aarch64" if host_arch == "arm64" else "qemu-system-x86_64" qemu_config_template = Path( - env("FEDORA_VM_QEMU_CONFIG") or script_dir / "qemu" / "fedora44.ini") + env("FEDORA_VM_QEMU_CONFIG") or script_dir / "qemu" / "fedora44" / "config.ini") host_os = platform.system().lower() if host_os == "darwin" and host_arch == "amd64": die("amd64 guests are not supported on macOS hosts; use an arm64 image") -qemu_platform_config = script_dir / "qemu" / f"fedora44-{host_os}-{host_arch}.ini" +qemu_platform_config = script_dir / "qemu" / "fedora44" / f"{host_os}-{host_arch}.ini" +forwards_file = Path( + env("FEDORA_VM_FORWARDS") or script_dir / "qemu" / "fedora44" / "forwards.conf") def pick_firmware(override, *candidates): @@ -127,10 +130,12 @@ Usage: {Path(sys.argv[0]).name} [--daemon] [--download-only] [--state-dir DIR] [ Start a Fedora 44 Cloud Base virtual machine. The launcher obtains a missing base image through the shared image helper and creates a writable overlay in the state directory, leaving the base image unchanged. The VM hardware -(memory, CPUs, SSH port forward, ...) is defined in qemu/fedora44.ini, which is +(memory, CPUs, ...) is defined in qemu/fedora44/config.ini, which is rendered to qemu.ini in the state directory and loaded with QEMU's -readconfig, -followed by qemu/fedora44--.ini (os is linux or darwin, arch is amd64 or arm64). Later QEMU arguments override them, e.g. +followed by qemu/fedora44/-.ini (os is linux or darwin, arch is amd64 or arm64). Later QEMU arguments override them, e.g. `-- -m 4G -smp 4`. +Host port forwards, one "LABEL HOST_PORT GUEST_PORT" rule per line, are +read from qemu/fedora44/forwards.conf. Options: --daemon Run in the background while keeping the graphical window @@ -150,7 +155,8 @@ Environment variables: FEDORA_VM_UEFI_VARS UEFI variable-store template path (auto-detected for the selected architecture) FEDORA_VM_DISPLAY QEMU display backend (default: cocoa on macOS, gtk elsewhere) FEDORA_VM_QEMU_CONFIG QEMU config template rendered into the state directory and - passed to -readconfig (default: qemu/fedora44.ini) + passed to -readconfig (default: qemu/fedora44/config.ini) + FEDORA_VM_FORWARDS Port forward file (default: qemu/fedora44/forwards.conf) """) @@ -266,24 +272,24 @@ except (KeyError, ValueError) as error: die(f"invalid placeholder in {qemu_config_template}: {error}") qemu_config.write_text(qemu_config_text) +forwards = vmforwards.usable_forwards(vmforwards.load_forwards(forwards_file)) + qemu_args = [ "-readconfig", str(qemu_config), "-readconfig", str(qemu_platform_config), # "max" is the host CPU under KVM/HVF and the fullest model under TCG. "-cpu", "max", - # The config file's [netdev] does not stop QEMU from adding its default - # NIC, a second user network without the SSH port forward. + # A config file section cannot hold two hostfwd entries, so the network + # that the NIC in the config file uses is defined here. -nic none stops + # QEMU from adding its default NIC as well. + "-netdev", vmforwards.netdev_argument(forwards), "-nic", "none", "-display", display_backend, ] print(f"Starting Fedora 44 ({host_arch}) from {qemu_config_template} " f"and {qemu_platform_config}.", flush=True) -# The SSH port forward lives in the config file, e.g. hostfwd = "tcp::2224-:22". -ssh_forward = re.search(r'hostfwd\s*=\s*"tcp:[^:"]*:(\d+)-[^:"]*:22"', qemu_config_text) -if ssh_forward: - print(f"SSH is forwarded to localhost:{ssh_forward.group(1)}: " - f"ssh -p {ssh_forward.group(1)} guofang@localhost", flush=True) +vmforwards.print_forwards(forwards) if daemonize: log_file.write_text("") diff --git a/run-ubuntu2604.py b/run-ubuntu2604.py index de06bd8..ec034be 100755 --- a/run-ubuntu2604.py +++ b/run-ubuntu2604.py @@ -3,13 +3,14 @@ import os import platform -import re import shutil import string import subprocess import sys from pathlib import Path +import vmforwards + script_dir = Path(__file__).resolve().parent env = os.environ.get @@ -69,11 +70,13 @@ native_arch = normalize_arch(platform.machine()) host_arch = normalize_arch(env("VM_IMAGE_ARCH") or native_arch) qemu_binary = "qemu-system-aarch64" if host_arch == "arm64" else "qemu-system-x86_64" qemu_config_template = Path( - env("UBUNTU_VM_QEMU_CONFIG") or script_dir / "qemu" / "ubuntu2604.ini") + env("UBUNTU_VM_QEMU_CONFIG") or script_dir / "qemu" / "ubuntu2604" / "config.ini") host_os = platform.system().lower() if host_os == "darwin" and host_arch == "amd64": die("amd64 guests are not supported on macOS hosts; use an arm64 image") -qemu_platform_config = script_dir / "qemu" / f"ubuntu2604-{host_os}-{host_arch}.ini" +qemu_platform_config = script_dir / "qemu" / "ubuntu2604" / f"{host_os}-{host_arch}.ini" +forwards_file = Path( + env("UBUNTU_VM_FORWARDS") or script_dir / "qemu" / "ubuntu2604" / "forwards.conf") def pick_firmware(override, *candidates): @@ -126,11 +129,13 @@ Usage: {Path(sys.argv[0]).name} [--daemon] [--download-only] [--state-dir DIR] [ Start an Ubuntu 26.04 LTS cloud image virtual machine. A writable overlay is created in the state directory (default: vm/ubuntu2604/), so the downloaded -base image is never modified. The VM hardware (memory, CPUs, SSH port forward, -...) is defined in qemu/ubuntu2604.ini, which is rendered to qemu.ini in the -state directory and loaded with QEMU's -readconfig, followed by -qemu/ubuntu2604--.ini (os is linux or darwin, arch is amd64 or arm64). Later QEMU arguments override them, e.g. +base image is never modified. The VM hardware (memory, CPUs, ...) is defined +in qemu/ubuntu2604/config.ini, which is rendered to qemu.ini in the state directory +and loaded with QEMU's -readconfig, followed by +qemu/ubuntu2604/-.ini (os is linux or darwin, arch is amd64 or arm64). Later QEMU arguments override them, e.g. `-- -m 4G -smp 4`. +Host port forwards, one "LABEL HOST_PORT GUEST_PORT" rule per line, are +read from qemu/ubuntu2604/forwards.conf. Options: --daemon Run in the background while keeping the graphical window @@ -150,7 +155,8 @@ Environment variables: UBUNTU_VM_UEFI_VARS UEFI variable-store template path (auto-detected for the selected architecture) UBUNTU_VM_DISPLAY QEMU display backend (default: cocoa on macOS, gtk elsewhere) UBUNTU_VM_QEMU_CONFIG QEMU config template rendered into the state directory and - passed to -readconfig (default: qemu/ubuntu2604.ini) + passed to -readconfig (default: qemu/ubuntu2604/config.ini) + UBUNTU_VM_FORWARDS Port forward file (default: qemu/ubuntu2604/forwards.conf) """) @@ -266,24 +272,24 @@ except (KeyError, ValueError) as error: die(f"invalid placeholder in {qemu_config_template}: {error}") qemu_config.write_text(qemu_config_text) +forwards = vmforwards.usable_forwards(vmforwards.load_forwards(forwards_file)) + qemu_args = [ "-readconfig", str(qemu_config), "-readconfig", str(qemu_platform_config), # "max" is the host CPU under KVM/HVF and the fullest model under TCG. "-cpu", "max", - # The config file's [netdev] does not stop QEMU from adding its default - # NIC, a second user network without the SSH port forward. + # A config file section cannot hold two hostfwd entries, so the network + # that the NIC in the config file uses is defined here. -nic none stops + # QEMU from adding its default NIC as well. + "-netdev", vmforwards.netdev_argument(forwards), "-nic", "none", "-display", display_backend, ] print(f"Starting Ubuntu 26.04 ({host_arch}) from {qemu_config_template} " f"and {qemu_platform_config}.", flush=True) -# The SSH port forward lives in the config file, e.g. hostfwd = "tcp::2225-:22". -ssh_forward = re.search(r'hostfwd\s*=\s*"tcp:[^:"]*:(\d+)-[^:"]*:22"', qemu_config_text) -if ssh_forward: - print(f"SSH is forwarded to localhost:{ssh_forward.group(1)}: " - f"ssh -p {ssh_forward.group(1)} guofang@localhost", flush=True) +vmforwards.print_forwards(forwards) if daemonize: log_file.write_text("") diff --git a/run-windows11.py b/run-windows11.py index ff61a2a..a106a31 100755 --- a/run-windows11.py +++ b/run-windows11.py @@ -4,12 +4,13 @@ import os import platform import shutil -import socket import string import subprocess import sys from pathlib import Path +import vmforwards + script_dir = Path(__file__).resolve().parent env = os.environ.get @@ -34,17 +35,6 @@ def capture(command): return result.stdout.rstrip("\n") -def port_in_use(port): - """True if a host TCP port cannot be bound, as QEMU does for a hostfwd rule.""" - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe: - probe.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - try: - probe.bind(("", int(port))) - except OSError: - return True - return False - - def is_executable(path): return path.is_file() and os.access(path, os.X_OK) @@ -63,10 +53,12 @@ def find_image_downloader(): state_dir = Path(env("WIN11_VM_STATE_DIR") or script_dir / "vm" / "windows11") disk_size = env("WIN11_VM_DISK_SIZE") or "64G" -rdp_port = env("WIN11_VM_RDP_PORT") or "3389" -ssh_port = env("WIN11_VM_SSH_PORT") or "2222" +rdp_port = env("WIN11_VM_RDP_PORT") +ssh_port = env("WIN11_VM_SSH_PORT") +forwards_file = Path( + env("WIN11_VM_FORWARDS") or script_dir / "qemu" / "windows11" / "forwards.conf") qemu_config_template = Path( - env("WIN11_VM_QEMU_CONFIG") or script_dir / "qemu" / "windows11.ini") + env("WIN11_VM_QEMU_CONFIG") or script_dir / "qemu" / "windows11" / "config.ini") def normalize_arch(arch): @@ -95,8 +87,8 @@ Usage: {Path(sys.argv[0]).name} [--daemon] [--download-only] [--seed-only] [--vn If the system disk (DIR/disk.qcow2, see --state-dir) is missing, create it from the Windows 11 QCOW2 base image. The VM hardware (accelerator, memory, CPUs, drives, devices) is defined in -qemu/windows11.ini, which is rendered to qemu.ini in the state directory and -loaded with QEMU's -readconfig, followed by qemu/windows11--.ini (os is linux or darwin, arch is amd64 or arm64, chosen from the host system and +qemu/windows11/config.ini, which is rendered to qemu.ini in the state directory and +loaded with QEMU's -readconfig, followed by qemu/windows11/-.ini (os is linux or darwin, arch is amd64 or arm64, chosen from the host system and architecture, --arch or VM_IMAGE_ARCH). Later QEMU arguments override them, e.g. -- -m 4G -smp 4. Use --install-from-iso to perform an automatic ISO installation instead, or --install-from-img to install from a disk image built from the ISO. @@ -124,10 +116,10 @@ Options: host runs under slow TCG emulation --state-dir DIR Directory for the VM disk, seed ISO and runtime files (default: vm/windows11; overrides WIN11_VM_STATE_DIR) - --rdp-port PORT Host port forwarded to guest RDP (default: 3389; - overrides WIN11_VM_RDP_PORT) - --ssh-port PORT Host port forwarded to guest SSH (default: 2222; - overrides WIN11_VM_SSH_PORT) + --rdp-port PORT Host port of the RDP rule in the port forward file + (default: 3389; overrides WIN11_VM_RDP_PORT) + --ssh-port PORT Host port of the SSH rule in the port forward file + (default: 2222; overrides WIN11_VM_SSH_PORT) -h, --help Show this help Environment variables: @@ -135,8 +127,9 @@ Environment variables: WIN11_VM_INSTALL_ISO Windows 11 installation ISO used with --install-from-iso (--install-from-img uses the same path ending in .img) WIN11_VM_TOOLS_ISO VirtIO Windows tools ISO (default: resolved by shared image helper) - WIN11_VM_RDP_PORT Host RDP port (default: 3389) - WIN11_VM_SSH_PORT Host SSH port (default: 2222) + WIN11_VM_RDP_PORT Host RDP port (default: from the port forward file, 3389) + WIN11_VM_SSH_PORT Host SSH port (default: from the port forward file, 2222) + WIN11_VM_FORWARDS Port forward file (default: qemu/windows11/forwards.conf) WIN11_VM_STATE_DIR Runtime directory (default: vm/windows11) WIN11_VM_DISK_IMAGE System disk path WIN11_VM_DISK_SIZE New system disk size (default: 64G) @@ -144,7 +137,7 @@ Environment variables: WIN11_VM_UEFI_CODE OVMF read-only firmware path (auto-detected if unset) WIN11_VM_UEFI_VARS OVMF variable-store template path (auto-detected if unset) WIN11_VM_QEMU_CONFIG QEMU config template passed to -readconfig - (default: qemu/windows11.ini) + (default: qemu/windows11/config.ini) WIN11_VM_DISPLAY QEMU display backend (default: cocoa on macOS, gtk elsewhere) """) @@ -209,7 +202,7 @@ qemu_binary = "qemu-system-aarch64" if host_arch == "arm64" else "qemu-system-x8 host_os = platform.system().lower() if host_os == "darwin" and host_arch == "amd64": die("amd64 guests are not supported on macOS hosts; use an arm64 image") -qemu_platform_config = script_dir / "qemu" / f"windows11-{host_os}-{host_arch}.ini" +qemu_platform_config = script_dir / "qemu" / "windows11" / f"{host_os}-{host_arch}.ini" def pick_firmware(override, *candidates): @@ -430,18 +423,9 @@ except (KeyError, ValueError) as error: die(f"invalid placeholder in {qemu_config_template}: {error}") qemu_config.write_text(qemu_config_text) -# QEMU refuses to start when a forwarded host port is taken, so warn and leave -# that forward out instead. -forwards = [] -forwarded_ports = [] -for label, host_port, guest_port in (("RDP", rdp_port, 3389), ("SSH", ssh_port, 22)): - in_use = port_in_use(host_port) - forwarded_ports.append((label, host_port, not in_use)) - if in_use: - print(f"Warning: host port {host_port} is already in use; " - f"{label} will not be forwarded to the guest.", file=sys.stderr) - else: - forwards.append(f"hostfwd=tcp::{host_port}-:{guest_port}") +# Each rule of the port forward file is one hostfwd entry of the network. +forwards = vmforwards.usable_forwards(vmforwards.load_forwards( + forwards_file, {label: port for label, port in (("RDP", rdp_port), ("SSH", ssh_port)) if port})) # Neither Windows Setup nor the base image has the viogpudo driver yet, and # without it the virtio-gpu in the config file shows "display output is not @@ -456,7 +440,7 @@ qemu_args += [ # A config file section cannot hold two hostfwd entries, so the network # that the NIC in the config file uses is defined here. -nic none stops # QEMU from adding its default NIC as well. - "-netdev", ",".join(["user,id=net0", *forwards]), + "-netdev", vmforwards.netdev_argument(forwards), "-nic", "none", "-display", display_backend, # "-display", "none", "-spice", "port=5900,addr=127.0.0.1,disable-ticketing=on,agent-mouse=off", "-device", "virtio-vga", @@ -499,7 +483,7 @@ elif installing: else: print("Starting Windows 11 normally.") print("Account: guofang / password; " + "; ".join( - f"{label}: 0.0.0.0:{port}" for label, port, forwarded in forwarded_ports if forwarded), + f"{forward.label}: 0.0.0.0:{forward.host_port}" for forward in forwards), flush=True) if daemonize: diff --git a/vmforwards.py b/vmforwards.py new file mode 100644 index 0000000..15d96ad --- /dev/null +++ b/vmforwards.py @@ -0,0 +1,96 @@ +"""Host port forwards for the QEMU user-mode network, read from a .forwards file. + +Each line of the file is "LABEL HOST_PORT GUEST_PORT"; "#" starts a comment. +A QEMU config file section cannot hold more than one hostfwd entry, so the +launchers build the -netdev argument from these rules instead. +""" + +import socket +import sys +from collections import namedtuple + +Forward = namedtuple("Forward", "label host_port guest_port") + + +def _die(message): + print(f"Error: {message}", file=sys.stderr) + sys.exit(1) + + +def _port(text, where): + if not (text.isascii() and text.isdigit() and 1 <= int(text) <= 65535): + _die(f"{where}: not a port number from 1 to 65535: {text}") + return int(text) + + +def port_in_use(port): + """True if a host TCP port cannot be bound, as QEMU does for a hostfwd rule.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe: + probe.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + try: + probe.bind(("", int(port))) + except OSError: + return True + return False + + +def load_forwards(path, host_port_overrides=None): + """Read the rules in a .forwards file. + + host_port_overrides maps a label to a host port that replaces the one in + the file, e.g. {"SSH": "2200"}. + """ + if not path.is_file(): + _die(f"port forward file was not found: {path}") + forwards = [] + for number, line in enumerate(path.read_text().splitlines(), 1): + fields = line.partition("#")[0].split() + if not fields: + continue + where = f"{path}:{number}" + if len(fields) != 3: + _die(f"{where}: expected LABEL HOST_PORT GUEST_PORT: {line.strip()}") + label, host_port, guest_port = fields + if any(forward.label == label for forward in forwards): + _die(f"{where}: duplicate label: {label}") + forwards.append(Forward(label, _port(host_port, where), _port(guest_port, where))) + for label, host_port in (host_port_overrides or {}).items(): + if not any(forward.label == label for forward in forwards): + _die(f"{path} has no {label} rule to override the port of") + forwards = [forward._replace(host_port=_port(str(host_port), f"{label} port")) + if forward.label == label else forward for forward in forwards] + seen = set() + for forward in forwards: + if forward.host_port in seen: + _die(f"{path}: host port {forward.host_port} is used by more than one rule") + seen.add(forward.host_port) + return forwards + + +def usable_forwards(forwards): + """Drop (with a warning) the rules whose host port is taken. + + QEMU refuses to start when a forwarded host port is taken. + """ + usable = [] + for forward in forwards: + if port_in_use(forward.host_port): + print(f"Warning: host port {forward.host_port} is already in use; " + f"{forward.label} will not be forwarded to the guest.", file=sys.stderr) + else: + usable.append(forward) + return usable + + +def netdev_argument(forwards, netdev_id="net0"): + """The value of -netdev for a user-mode network with the given rules.""" + return ",".join([f"user,id={netdev_id}", *( + f"hostfwd=tcp::{forward.host_port}-:{forward.guest_port}" for forward in forwards)]) + + +def print_forwards(forwards): + for forward in forwards: + print(f"{forward.label}: localhost:{forward.host_port} -> guest port " + f"{forward.guest_port}", flush=True) + if forward.guest_port == 22: + print(f" ssh -p {forward.host_port} guofang@localhost", flush=True) diff --git a/windows11-arm64-install-speed.md b/windows11-arm64-install-speed.md index 2f0ae06..7f379ca 100644 --- a/windows11-arm64-install-speed.md +++ b/windows11-arm64-install-speed.md @@ -20,7 +20,7 @@ |---|---| | 主机 | Apple M5 Max,arm64,macOS 26.4,18 核,48 GB 内存 | | QEMU | 11.1.2,`qemu-system-aarch64`,加速器 HVF | -| 客户机配置 | `qemu/windows11.ini` + `qemu/windows11-darwin-arm64.ini`(`virt` 机型,8 vCPU,8G 内存,NVMe 系统盘,virtio-net,ramfb,usb-kbd/usb-tablet) | +| 客户机配置 | `qemu/windows11/config.ini` + `qemu/windows11/darwin-arm64.ini`(`virt` 机型,8 vCPU,8G 内存,NVMe 系统盘,virtio-net,ramfb,usb-kbd/usb-tablet) | | 安装 ISO | `Windows11_Client_arm64_en-us_26300_9457.iso`(卷标 `CCCOMA_A64FRE_EN-US_DV9`,`install.wim` 7.6 GB,`boot.wim` 630 MB) | | 驱动 ISO | `virtio-win.iso`(含 ARM64 驱动目录,根目录只有 x64/x86 的 MSI) | @@ -200,7 +200,7 @@ UTM 5.0.8 带有这个修复;Homebrew 的 QEMU 11.1.2 没有,所以上游版 ### 处理 -在 `qemu/windows11-darwin-arm64.ini` 的 `[machine]` 加 `kernel-irqchip = "off"`,改用模拟的中断控制器。 +在 `qemu/windows11/darwin-arm64.ini` 的 `[machine]` 加 `kernel-irqchip = "off"`,改用模拟的中断控制器。 这一项改完后速度明显提升,已确认和 UTM 一致。 ```ini