From fbd11f3edd6d2034774d802e048261e613ffcbf5 Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Wed, 25 May 2022 06:37:06 +1000 Subject: [PATCH] gh-92658: Add Hyper-V socket support (GH-92755) --- Doc/library/socket.rst | 43 ++++++ Lib/test/test_socket.py | 68 +++++++++ ...2-05-13-00-57-18.gh-issue-92658.YdhFE2.rst | 1 + Modules/socketmodule.c | 131 ++++++++++++++++++ Modules/socketmodule.h | 12 ++ PCbuild/_socket.vcxproj | 2 +- 6 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2022-05-13-00-57-18.gh-issue-92658.YdhFE2.rst diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 2d25646134a..1b801955dff 100755 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -225,6 +225,29 @@ created. Socket addresses are represented as follows: .. versionadded:: 3.9 +- :const:`AF_HYPERV` is a Windows-only socket based interface for communicating + with Hyper-V hosts and guests. The address family is represented as a + ``(vm_id, service_id)`` tuple where the ``vm_id`` and ``service_id`` are + UUID strings. + + The ``vm_id`` is the virtual machine identifier or a set of known VMID values + if the target is not a specific virtual machine. Known VMID constants + defined on ``socket`` are: + + - ``HV_GUID_ZERO`` + - ``HV_GUID_BROADCAST`` + - ``HV_GUID_WILDCARD`` - Used to bind on itself and accept connections from + all partitions. + - ``HV_GUID_CHILDREN`` - Used to bind on itself and accept connection from + child partitions. + - ``HV_GUID_LOOPBACK`` - Used as a target to itself. + - ``HV_GUID_PARENT`` - When used as a bind accepts connection from the parent + partition. When used as an address target it will connect to the parent parition. + + The ``service_id`` is the service identifier of the registered service. + + .. versionadded:: 3.12 + If you use a hostname in the *host* portion of IPv4/v6 socket address, the program may show a nondeterministic behavior, as Python uses the first address returned from the DNS resolution. The socket address will be resolved @@ -589,6 +612,26 @@ Constants .. availability:: Linux >= 3.9 +.. data:: AF_HYPERV + HV_PROTOCOL_RAW + HVSOCKET_CONNECT_TIMEOUT + HVSOCKET_CONNECT_TIMEOUT_MAX + HVSOCKET_CONTAINER_PASSTHRU + HVSOCKET_CONNECTED_SUSPEND + HVSOCKET_ADDRESS_FLAG_PASSTHRU + HV_GUID_ZERO + HV_GUID_WILDCARD + HV_GUID_BROADCAST + HV_GUID_CHILDREN + HV_GUID_LOOPBACK + HV_GUID_LOOPBACK + + Constants for Windows Hyper-V sockets for host/guest communications. + + .. availability:: Windows. + + .. versionadded:: 3.12 + Functions ^^^^^^^^^ diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index 1aaa9e44f90..c9819038241 100755 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -22,6 +22,7 @@ import signal import math import pickle +import re import struct import random import shutil @@ -143,6 +144,17 @@ def _have_socket_bluetooth(): return True +def _have_socket_hyperv(): + """Check whether AF_HYPERV sockets are supported on this host.""" + try: + s = socket.socket(socket.AF_HYPERV, socket.SOCK_STREAM, socket.HV_PROTOCOL_RAW) + except (AttributeError, OSError): + return False + else: + s.close() + return True + + @contextlib.contextmanager def socket_setdefaulttimeout(timeout): old_timeout = socket.getdefaulttimeout() @@ -171,6 +183,8 @@ def socket_setdefaulttimeout(timeout): HAVE_SOCKET_BLUETOOTH = _have_socket_bluetooth() +HAVE_SOCKET_HYPERV = _have_socket_hyperv() + # Size in bytes of the int type SIZEOF_INT = array.array("i").itemsize @@ -2459,6 +2473,60 @@ def testCreateScoSocket(self): pass +@unittest.skipUnless(HAVE_SOCKET_HYPERV, + 'Hyper-V sockets required for this test.') +class BasicHyperVTest(unittest.TestCase): + + def testHyperVConstants(self): + socket.HVSOCKET_CONNECT_TIMEOUT + socket.HVSOCKET_CONNECT_TIMEOUT_MAX + socket.HVSOCKET_CONTAINER_PASSTHRU + socket.HVSOCKET_CONNECTED_SUSPEND + socket.HVSOCKET_ADDRESS_FLAG_PASSTHRU + socket.HV_GUID_ZERO + socket.HV_GUID_WILDCARD + socket.HV_GUID_BROADCAST + socket.HV_GUID_CHILDREN + socket.HV_GUID_LOOPBACK + socket.HV_GUID_LOOPBACK + + def testCreateHyperVSocketWithUnknownProtoFailure(self): + expected = "A protocol was specified in the socket function call " \ + "that does not support the semantics of the socket type requested" + with self.assertRaisesRegex(OSError, expected): + socket.socket(socket.AF_HYPERV, socket.SOCK_STREAM) + + def testCreateHyperVSocketAddrNotTupleFailure(self): + expected = "connect(): AF_HYPERV address must be tuple, not str" + with socket.socket(socket.AF_HYPERV, socket.SOCK_STREAM, socket.HV_PROTOCOL_RAW) as s: + with self.assertRaisesRegex(TypeError, re.escape(expected)): + s.connect(socket.HV_GUID_ZERO) + + def testCreateHyperVSocketAddrNotTupleOf2StrsFailure(self): + expected = "AF_HYPERV address must be a str tuple (vm_id, service_id)" + with socket.socket(socket.AF_HYPERV, socket.SOCK_STREAM, socket.HV_PROTOCOL_RAW) as s: + with self.assertRaisesRegex(TypeError, re.escape(expected)): + s.connect((socket.HV_GUID_ZERO,)) + + def testCreateHyperVSocketAddrNotTupleOfStrsFailure(self): + expected = "AF_HYPERV address must be a str tuple (vm_id, service_id)" + with socket.socket(socket.AF_HYPERV, socket.SOCK_STREAM, socket.HV_PROTOCOL_RAW) as s: + with self.assertRaisesRegex(TypeError, re.escape(expected)): + s.connect((1, 2)) + + def testCreateHyperVSocketAddrVmIdNotValidUUIDFailure(self): + expected = "connect(): AF_HYPERV address vm_id is not a valid UUID string" + with socket.socket(socket.AF_HYPERV, socket.SOCK_STREAM, socket.HV_PROTOCOL_RAW) as s: + with self.assertRaisesRegex(ValueError, re.escape(expected)): + s.connect(("00", socket.HV_GUID_ZERO)) + + def testCreateHyperVSocketAddrServiceIdNotValidUUIDFailure(self): + expected = "connect(): AF_HYPERV address service_id is not a valid UUID string" + with socket.socket(socket.AF_HYPERV, socket.SOCK_STREAM, socket.HV_PROTOCOL_RAW) as s: + with self.assertRaisesRegex(ValueError, re.escape(expected)): + s.connect((socket.HV_GUID_ZERO, "00")) + + class BasicTCPTest(SocketConnectedTest): def __init__(self, methodName='runTest'): diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-05-13-00-57-18.gh-issue-92658.YdhFE2.rst b/Misc/NEWS.d/next/Core and Builtins/2022-05-13-00-57-18.gh-issue-92658.YdhFE2.rst new file mode 100644 index 00000000000..887b3d61596 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2022-05-13-00-57-18.gh-issue-92658.YdhFE2.rst @@ -0,0 +1 @@ +Add support for connecting and binding to Hyper-V sockets on Windows Hyper-V hosts and guests. diff --git a/Modules/socketmodule.c b/Modules/socketmodule.c index f376513fead..0bc99011b2c 100644 --- a/Modules/socketmodule.c +++ b/Modules/socketmodule.c @@ -271,6 +271,9 @@ shutdown(how) -- shut down traffic in one or both directions\n\ # include # endif +/* Helpers needed for AF_HYPERV */ +# include + /* Macros based on the IPPROTO enum, see: https://bugs.python.org/issue29515 */ #ifdef MS_WINDOWS #define IPPROTO_ICMP IPPROTO_ICMP @@ -1579,6 +1582,35 @@ makesockaddr(SOCKET_T sockfd, struct sockaddr *addr, size_t addrlen, int proto) } #endif /* HAVE_SOCKADDR_ALG */ +#ifdef AF_HYPERV + case AF_HYPERV: + { + SOCKADDR_HV *a = (SOCKADDR_HV *) addr; + + wchar_t *guidStr; + RPC_STATUS res = UuidToStringW(&a->VmId, &guidStr); + if (res != RPC_S_OK) { + PyErr_SetFromWindowsErr(res); + return 0; + } + PyObject *vmId = PyUnicode_FromWideChar(guidStr, -1); + res = RpcStringFreeW(&guidStr); + assert(res == RPC_S_OK); + + res = UuidToStringW(&a->ServiceId, &guidStr); + if (res != RPC_S_OK) { + Py_DECREF(vmId); + PyErr_SetFromWindowsErr(res); + return 0; + } + PyObject *serviceId = PyUnicode_FromWideChar(guidStr, -1); + res = RpcStringFreeW(&guidStr); + assert(res == RPC_S_OK); + + return Py_BuildValue("NN", vmId, serviceId); + } +#endif /* AF_HYPERV */ + /* More cases here... */ default: @@ -2375,6 +2407,76 @@ getsockaddrarg(PySocketSockObject *s, PyObject *args, return 1; } #endif /* HAVE_SOCKADDR_ALG */ +#ifdef AF_HYPERV + case AF_HYPERV: + { + switch (s->sock_proto) { + case HV_PROTOCOL_RAW: + { + PyObject *vm_id_obj = NULL; + PyObject *service_id_obj = NULL; + + SOCKADDR_HV *addr = &addrbuf->hv; + + memset(addr, 0, sizeof(*addr)); + addr->Family = AF_HYPERV; + + if (!PyTuple_Check(args)) { + PyErr_Format(PyExc_TypeError, + "%s(): AF_HYPERV address must be tuple, not %.500s", + caller, Py_TYPE(args)->tp_name); + return 0; + } + if (!PyArg_ParseTuple(args, + "UU;AF_HYPERV address must be a str tuple (vm_id, service_id)", + &vm_id_obj, &service_id_obj)) + { + return 0; + } + + wchar_t *guid_str = PyUnicode_AsWideCharString(vm_id_obj, NULL); + if (guid_str == NULL) { + PyErr_Format(PyExc_ValueError, + "%s(): AF_HYPERV address vm_id is not a valid UUID string", + caller); + return 0; + } + RPC_STATUS rc = UuidFromStringW(guid_str, &addr->VmId); + PyMem_Free(guid_str); + if (rc != RPC_S_OK) { + PyErr_Format(PyExc_ValueError, + "%s(): AF_HYPERV address vm_id is not a valid UUID string", + caller); + return 0; + } + + guid_str = PyUnicode_AsWideCharString(service_id_obj, NULL); + if (guid_str == NULL) { + PyErr_Format(PyExc_ValueError, + "%s(): AF_HYPERV address service_id is not a valid UUID string", + caller); + return 0; + } + rc = UuidFromStringW(guid_str, &addr->ServiceId); + PyMem_Free(guid_str); + if (rc != RPC_S_OK) { + PyErr_Format(PyExc_ValueError, + "%s(): AF_HYPERV address service_id is not a valid UUID string", + caller); + return 0; + } + + *len_ret = sizeof(*addr); + return 1; + } + default: + PyErr_Format(PyExc_OSError, + "%s(): unsupported AF_HYPERV protocol: %d", + caller, s->sock_proto); + return 0; + } + } +#endif /* AF_HYPERV */ /* More cases here... */ @@ -2524,6 +2626,13 @@ getsockaddrlen(PySocketSockObject *s, socklen_t *len_ret) return 1; } #endif /* HAVE_SOCKADDR_ALG */ +#ifdef AF_HYPERV + case AF_HYPERV: + { + *len_ret = sizeof (SOCKADDR_HV); + return 1; + } +#endif /* AF_HYPERV */ /* More cases here... */ @@ -7351,6 +7460,28 @@ PyInit__socket(void) /* Linux LLC */ PyModule_AddIntMacro(m, AF_LLC); #endif +#ifdef AF_HYPERV + /* Hyper-V sockets */ + PyModule_AddIntMacro(m, AF_HYPERV); + + /* for proto */ + PyModule_AddIntMacro(m, HV_PROTOCOL_RAW); + + /* for setsockopt() */ + PyModule_AddIntMacro(m, HVSOCKET_CONNECT_TIMEOUT); + PyModule_AddIntMacro(m, HVSOCKET_CONNECT_TIMEOUT_MAX); + PyModule_AddIntMacro(m, HVSOCKET_CONTAINER_PASSTHRU); + PyModule_AddIntMacro(m, HVSOCKET_CONNECTED_SUSPEND); + PyModule_AddIntMacro(m, HVSOCKET_ADDRESS_FLAG_PASSTHRU); + + /* for bind() or connect() */ + PyModule_AddStringConstant(m, "HV_GUID_ZERO", "00000000-0000-0000-0000-000000000000"); + PyModule_AddStringConstant(m, "HV_GUID_WILDCARD", "00000000-0000-0000-0000-000000000000"); + PyModule_AddStringConstant(m, "HV_GUID_BROADCAST", "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF"); + PyModule_AddStringConstant(m, "HV_GUID_CHILDREN", "90DB8B89-0D35-4F79-8CE9-49EA0AC8B7CD"); + PyModule_AddStringConstant(m, "HV_GUID_LOOPBACK", "E0E16197-DD56-4A10-9195-5EE7A155A838"); + PyModule_AddStringConstant(m, "HV_GUID_PARENT", "A42E7CDA-D03F-480C-9CC2-A4DE20ABB878"); +#endif /* AF_HYPERV */ #ifdef USE_BLUETOOTH PyModule_AddIntMacro(m, AF_BLUETOOTH); diff --git a/Modules/socketmodule.h b/Modules/socketmodule.h index 1b35b11cdee..66d9ccf24e3 100644 --- a/Modules/socketmodule.h +++ b/Modules/socketmodule.h @@ -76,6 +76,15 @@ struct SOCKADDR_BTH_REDEF { # else typedef int socklen_t; # endif /* IPPROTO_IPV6 */ + +/* Remove ifdef once Py_WINVER >= 0x0604 + * socket.h only defines AF_HYPERV if _WIN32_WINNT is at that level or higher + * so for now it's just manually defined. + */ +# ifndef AF_HYPERV +# define AF_HYPERV 34 +# endif +# include #endif /* MS_WINDOWS */ #ifdef HAVE_SYS_UN_H @@ -288,6 +297,9 @@ typedef union sock_addr { #ifdef HAVE_LINUX_TIPC_H struct sockaddr_tipc tipc; #endif +#ifdef AF_HYPERV + SOCKADDR_HV hv; +#endif } sock_addr_t; /* The object holding a socket. It holds some extra information, diff --git a/PCbuild/_socket.vcxproj b/PCbuild/_socket.vcxproj index 8fd75f90e7e..78fa4d6729a 100644 --- a/PCbuild/_socket.vcxproj +++ b/PCbuild/_socket.vcxproj @@ -93,7 +93,7 @@ - ws2_32.lib;iphlpapi.lib;%(AdditionalDependencies) + ws2_32.lib;iphlpapi.lib;Rpcrt4.lib;%(AdditionalDependencies)