Skip to content

Commit

Permalink
#3666 add hyper-v socket URL parsing
Browse files Browse the repository at this point in the history
automatically convert port numbers into Linux VM vsock serviceid uuid strings,
and vmid names into their corresponding uuid constants
  • Loading branch information
totaam committed Nov 16, 2024
1 parent e76c7a9 commit d103063
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 9 deletions.
23 changes: 21 additions & 2 deletions xpra/net/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@
from typing import Any, Union, TypeAlias, Final
from collections.abc import Callable, Sequence

from xpra.exit_codes import ExitCode
from xpra.net.compression import Compressed, Compressible, LargeStructure
from xpra.common import noop, SizedBuffer
from xpra.os_util import LINUX, FREEBSD, WIN32
from xpra.scripts.config import str_to_bool
from xpra.scripts.config import str_to_bool, InitExit
from xpra.util.str_fn import repr_ellipsized
from xpra.util.env import envint, envbool

Expand Down Expand Up @@ -76,7 +77,12 @@ class ConnectionClosedException(Exception):
AUTO_ABSTRACT_SOCKET = envbool("XPRA_AUTO_ABSTRACT_SOCKET", LINUX)
ABSTRACT_SOCKET_PREFIX: Final[str] = "xpra/"

SOCKET_TYPES: Sequence[str] = ("tcp", "ws", "wss", "ssl", "ssh", "rfb", "vsock", "socket", "named-pipe", "quic")
SOCKET_TYPES: Sequence[str] = (
"tcp", "ws", "wss", "ssl", "ssh", "rfb",
"vsock", "hyperv" "socket",
"named-pipe",
"quic",
)

IP_SOCKTYPES: Sequence[str] = ("tcp", "ssl", "ws", "wss", "ssh", "quic")
TCP_SOCKTYPES: Sequence[str] = ("tcp", "ssl", "ws", "wss", "ssh")
Expand All @@ -96,6 +102,8 @@ class ConnectionClosedException(Exception):
"xpraws": "ws",
"xpra+wss": "wss",
"xprawss": "wss",
"xpra+hyperv": "hyperv",
"xprahyperv": "hyperv",
"rfb": "vnc",
}

Expand Down Expand Up @@ -256,3 +264,14 @@ def has_websocket_handler() -> bool:
<p>Error code explanation: 400 = Bad request syntax or unsupported method.
</body>
"""


def verify_hyperv_available() -> None:
try:
import socket
s = socket.socket(socket.AF_HYPERV, socket.SOCK_STREAM, socket.HV_PROTOCOL_RAW)
except (AttributeError, OSError) as e:
raise InitExit(ExitCode.UNSUPPORTED,
f"hyperv sockets are not supported on this platform: {e}") from None
else:
s.close()
29 changes: 23 additions & 6 deletions xpra/scripts/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@
dict_to_validated_config, get_xpra_defaults_dirs, get_defaults, read_xpra_conf,
make_defaults_struct, str_to_bool, parse_bool_or, has_audio_support, name_to_field,
)
from xpra.net.common import DEFAULT_PORTS, SOCKET_TYPES, AUTO_ABSTRACT_SOCKET, ABSTRACT_SOCKET_PREFIX
from xpra.net.common import (
DEFAULT_PORTS, SOCKET_TYPES, AUTO_ABSTRACT_SOCKET, ABSTRACT_SOCKET_PREFIX,
verify_hyperv_available,
)
from xpra.log import is_debug_enabled, Logger, get_debug_args

assert callable(error), "used by modules importing this function from here"
Expand Down Expand Up @@ -429,7 +432,7 @@ def run_mode(script_file: str, cmdline, error_cb, options, args, full_mode: str,
) and not NO_ROOT_WARNING:
warn("\nWarning: running as root\n")

display_is_remote = isdisplaytype(args, "ssh", "tcp", "ssl", "vsock", "quic")
display_is_remote = isdisplaytype(args, "ssh", "tcp", "ssl", "vsock", "hyperv", "quic")
if mode.startswith("shadow") and WIN32 and not envbool("XPRA_PAEXEC_WRAP", False):
# are we started from a non-interactive context?
from xpra.platform.win32.gui import get_desktop_name
Expand Down Expand Up @@ -551,7 +554,7 @@ def DotXpra(*args, **kwargs):
def do_run_mode(script_file: str, cmdline, error_cb, options, args, full_mode: str, defaults) -> ExitValue:
mode_parts = full_mode.split(",", 1)
mode = MODE_ALIAS.get(mode_parts[0], mode_parts[0])
display_is_remote = isdisplaytype(args, "ssh", "tcp", "ssl", "vsock", "quic")
display_is_remote = isdisplaytype(args, "ssh", "tcp", "ssl", "vsock", "hyperv", "quic")
if args and mode in ("seamless", "desktop", "monitor"):
# all args that aren't specifying a connection will be interpreted as a start-child command:
# ie: "xpra" "start" "xterm"
Expand Down Expand Up @@ -933,9 +936,12 @@ def display_desc_to_uri(display_desc: dict[str, Any]) -> str:
port = display_desc.get("port")
if port and port != DEFAULT_PORTS.get(dtype):
uri += f":{port:d}"
elif dtype == "vsock":
cid, iport = display_desc["vsock"]
uri += f"{cid}:{iport}"
elif dtype == "vsock":
cid, iport = display_desc["vsock"]
uri += f"{cid}:{iport}"
elif dtype == "hyperv":
vmid, service = display_desc["hyperv"]
uri += f"{vmid}:{service}"
else:
raise NotImplementedError(f"{dtype} is not implemented yet")
uri += "/" + display_desc_to_display_path(display_desc)
Expand Down Expand Up @@ -1302,6 +1308,17 @@ def sockpathfail_cb(msg) -> NoReturn:
)
return conn

if dtype == "hyperv":
vmid, service = display_desc["hyperv"]
verify_hyperv_available()
sock = socket.socket(socket.AF_HYPERV, socket.SOCK_STREAM, socket.HV_PROTOCOL_RAW)
sock.connect((vmid, service))
sock.timeout = VSOCK_TIMEOUT
sock.settimeout(None)
conn = SocketConnection(sock, "local", "host", (vmid, service), dtype)
conn.target = f"hyperv://{vmid}.{service}"
return conn

if dtype == "quic":
host = display_desc["host"]
port = display_desc["port"]
Expand Down
60 changes: 59 additions & 1 deletion xpra/scripts/parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
# pylint: disable=import-outside-toplevel

import re
import uuid
import shlex
import os.path
import optparse
Expand All @@ -23,7 +24,7 @@
from xpra.util.parsing import parse_simple_dict
from xpra.util.env import envbool
from xpra.exit_codes import ExitCode
from xpra.net.common import DEFAULT_PORT, DEFAULT_PORTS
from xpra.net.common import DEFAULT_PORT, DEFAULT_PORTS, verify_hyperv_available
from xpra.os_util import WIN32, OSX, POSIX, get_user_uuid
from xpra.util.io import warn
from xpra.scripts.config import (
Expand Down Expand Up @@ -502,6 +503,23 @@ def add_query() -> None:
opts.display = display_name
return desc

if protocol == "hyperv":
add_credentials()
add_query()
vmid = parse_hyperv_vmid(parsed.hostname)
service = parse_hyperv_serviceid(parsed.port)
verify_hyperv_available()
desc.update(
{
"local": False,
"display": display_name,
"hyperv": (vmid, service),
}
)
print("hyperv1")
opts.display = display_name
return desc

if protocol in ("ssh", "vnc+ssh"):
desc.update(
{
Expand Down Expand Up @@ -2082,5 +2100,45 @@ def parse_vsock_cid(cid_str: str) -> int:
return cid


def parse_uuid(name: str, value: str) -> str:
try:
return str(uuid.UUID(value))
except ValueError as e:
raise ValueError(f"{name} {value!r} is not a valid uuid: {e}") from None


def parse_hyperv_serviceid(serviceid: str | int) -> str:
# we support plain port numbers,
# as used with linux servers vsock style interfaces
# or uuid strings for hyper-v native addresses
if isinstance(serviceid, int) or len(serviceid) < 6:
# ie: "20000"
try:
port = int(serviceid)
except ValueError:
raise ValueError(f"short serviceid {serviceid!r} is not numeric") from None
if port < 0 or port > 65535:
raise ValueError(f"short serviceid {port} is out of range")
# assume that this is a Linux vsock port:
return f"{port:08x}-facb-11e6-bd58-64006a7986d3"
return parse_uuid("serviceid", serviceid)


def parse_hyperv_vmid(vmid: str) -> str:
import socket
if vmid in ("0", ""):
# cannot be used for connecting!
return socket.HV_GUID_ZERO
if vmid == "*":
return socket.HV_GUID_BROADCAST
if vmid.lower() == "children":
return socket.HV_GUID_CHILDREN
if vmid.lower() in ("lo", "loopback"):
return socket.HV_GUID_LOOPBACK
if vmid.lower() == "parent":
return socket.HV_GUID_PARENT
return parse_uuid("vmid", vmid)


def is_local(host: str) -> bool:
return host.lower() in ("localhost", "127.0.0.1", "::1")

0 comments on commit d103063

Please sign in to comment.