Skip to content

gh-92658: Add Hyper-V socket support #92755

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions Doc/library/socket.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
^^^^^^^^^

Expand Down
68 changes: 68 additions & 0 deletions Lib/test/test_socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import signal
import math
import pickle
import re
import struct
import random
import shutil
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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'):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for connecting and binding to Hyper-V sockets on Windows Hyper-V hosts and guests.
131 changes: 131 additions & 0 deletions Modules/socketmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,9 @@ shutdown(how) -- shut down traffic in one or both directions\n\
# include <fcntl.h>
# endif

/* Helpers needed for AF_HYPERV */
# include <Rpc.h>

/* Macros based on the IPPROTO enum, see: https://bugs.python.org/issue29515 */
#ifdef MS_WINDOWS
#define IPPROTO_ICMP IPPROTO_ICMP
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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... */

Expand Down Expand Up @@ -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... */

Expand Down Expand Up @@ -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);
Expand Down
12 changes: 12 additions & 0 deletions Modules/socketmodule.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 <hvsocket.h>
#endif /* MS_WINDOWS */

#ifdef HAVE_SYS_UN_H
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion PCbuild/_socket.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
</PropertyGroup>
<ItemDefinitionGroup>
<Link>
<AdditionalDependencies>ws2_32.lib;iphlpapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>ws2_32.lib;iphlpapi.lib;Rpcrt4.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
Expand Down