"""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)