A QEMU config file section cannot hold more than one hostfwd entry, so the forwards move out of the ini files into qemu/<vm>/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/<vm>/config.ini and qemu/<vm>/<os>-<arch>.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 <noreply@anthropic.com>
97 lines
3.5 KiB
Python
97 lines
3.5 KiB
Python
"""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)
|