Add nbdssl support
This commit is contained in:
@@ -242,10 +242,25 @@ def _expect_code(line: str, code: str, what: str) -> str:
|
||||
return line[len(code):].lstrip()
|
||||
|
||||
|
||||
def nfcssl_service_name(service: str) -> str:
|
||||
"""Return the NFCSSL authd PROXY service for an NFC service name.
|
||||
|
||||
VCenter tickets still report ``vpxa-nfc``. NBDSSL uses
|
||||
``PROXY vpxa-nfcssl`` (or ``ha-nfcssl`` on a direct ESXi ticket).
|
||||
|
||||
Args:
|
||||
service: Ticket ``service`` field, for example ``vpxa-nfc``.
|
||||
"""
|
||||
if service.endswith("ssl"):
|
||||
return service
|
||||
return f"{service}ssl"
|
||||
|
||||
|
||||
def connect_authd(
|
||||
ticket: vim.HostServiceTicket,
|
||||
allow_untrusted: bool = False,
|
||||
timeout: float = 30.0) -> ssl.SSLSocket:
|
||||
timeout: float = 30.0,
|
||||
nfc_ssl: bool = True) -> ssl.SSLSocket:
|
||||
"""Complete the ESXi authd handshake using an NFC HostServiceTicket.
|
||||
|
||||
Wire sequence captured from VDDK against authd on TCP 902:
|
||||
@@ -253,14 +268,20 @@ def connect_authd(
|
||||
1. Read the plaintext 220 banner, then wrap the socket with TLS.
|
||||
2. SESSION <sessionId>
|
||||
3. BANNER
|
||||
4. THUMBPRINT_SHA2 PlainText (NFC data stays on this TLS socket)
|
||||
5. PROXY <ticket.service> (vpxa-nfc when connecting via vCenter)
|
||||
4. THUMBPRINT_SHA2 PlainText
|
||||
5. PROXY <ticket.service> (vpxa-nfc / nbd) or vpxa-nfcssl (nbdssl)
|
||||
|
||||
``THUMBPRINT_SHA2 PlainText`` is used for both transports. NBDSSL
|
||||
is selected by the PROXY service name; after ``200 Connect
|
||||
ha-nfcssl`` a second TLS handshake is started in ``nfc_open``.
|
||||
|
||||
Args:
|
||||
ticket: One-time ticket from get_nfc_ticket().
|
||||
allow_untrusted: If False, require the peer SHA-1 thumbprint to match
|
||||
ticket.sslThumbprint.
|
||||
timeout: Socket timeout in seconds.
|
||||
nfc_ssl: When True (the default), PROXY to the NFCSSL service
|
||||
used by nbdssl. Pass False for plaintext NFC (nbd).
|
||||
"""
|
||||
host = ticket.host
|
||||
port = ticket.port or AUTHD_DEFAULT_PORT
|
||||
@@ -294,6 +315,8 @@ def connect_authd(
|
||||
_expect_code(_readline(ssock), "200", "THUMBPRINT_SHA2")
|
||||
|
||||
service = ticket.service or "vpxa-nfc"
|
||||
if nfc_ssl:
|
||||
service = nfcssl_service_name(service)
|
||||
ssock.sendall(f"PROXY {service}\r\n".encode("ascii"))
|
||||
_expect_code(_readline(ssock), "200", "PROXY")
|
||||
return ssock
|
||||
@@ -309,10 +332,12 @@ class NfcAuthSession:
|
||||
self,
|
||||
si: vim.ServiceInstance,
|
||||
ticket: vim.HostServiceTicket,
|
||||
authd_sock: ssl.SSLSocket) -> None:
|
||||
authd_sock: ssl.SSLSocket,
|
||||
nfc_ssl: bool = True) -> None:
|
||||
self.si = si
|
||||
self.ticket = ticket
|
||||
self.authd_sock = authd_sock
|
||||
self.nfc_ssl = nfc_ssl
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the authd socket and logout of the VIM session."""
|
||||
@@ -338,7 +363,8 @@ def authenticate(
|
||||
allow_untrusted: bool = False,
|
||||
disk_device_key: Optional[int] = None,
|
||||
disk_path: Optional[str] = None,
|
||||
read_only: bool = True) -> NfcAuthSession:
|
||||
read_only: bool = True,
|
||||
nfc_ssl: bool = True) -> NfcAuthSession:
|
||||
"""Login to vSphere and complete NFC authd authentication for a VM.
|
||||
|
||||
Args:
|
||||
@@ -354,6 +380,8 @@ def authenticate(
|
||||
disk_path: Datastore path used to resolve ``disk_device_key``.
|
||||
read_only: When False, request a writable ``NfcRandomAccessOpenDisk``
|
||||
ticket.
|
||||
nfc_ssl: When True (the default), complete authd with the NFCSSL
|
||||
PROXY service used by nbdssl. Pass False for nbd.
|
||||
"""
|
||||
si = connect_vim(
|
||||
host, username, password, port=port,
|
||||
@@ -364,8 +392,8 @@ def authenticate(
|
||||
si, vm, disk_device_key=disk_device_key,
|
||||
disk_path=disk_path, read_only=read_only)
|
||||
authd_sock = connect_authd(
|
||||
ticket, allow_untrusted=allow_untrusted)
|
||||
ticket, allow_untrusted=allow_untrusted, nfc_ssl=nfc_ssl)
|
||||
except Exception:
|
||||
Disconnect(si)
|
||||
raise
|
||||
return NfcAuthSession(si, ticket, authd_sock)
|
||||
return NfcAuthSession(si, ticket, authd_sock, nfc_ssl=nfc_ssl)
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
|
||||
"""VDDK-compatible NFC disk open, sector read, and sector write.
|
||||
|
||||
After ``nfc_auth.connect_authd`` returns ``200 Connect``, VDDK stops using
|
||||
``SSL_write`` on the authd socket. ``THUMBPRINT_SHA2 PlainText`` means the
|
||||
NFC binary protocol runs as raw TCP on that same file descriptor
|
||||
(``useSSL=0``). This module dups that fd and speaks:
|
||||
After ``nfc_auth.connect_authd`` returns ``200 Connect``, NBD
|
||||
(``useSSL=0``) stops using ``SSL_write`` and speaks NFC as raw TCP on
|
||||
that file descriptor. NBDSSL (``useSSL=1``) starts a second TLS
|
||||
handshake on the same TCP connection (``200 Connect ha-nfcssl``) and
|
||||
speaks the same NFC frames as TLS application data. This module dups
|
||||
the authd fd and speaks:
|
||||
|
||||
1. Classic 264-byte NFC messages (handshake, version, connection data,
|
||||
AIO session open).
|
||||
@@ -24,7 +26,7 @@ import socket
|
||||
import ssl
|
||||
import struct
|
||||
|
||||
from openvixdisklib.nfc_auth import NfcAuthSession
|
||||
from openvixdisklib.nfc_auth import NfcAuthSession, _ssl_client_context
|
||||
|
||||
NFC_MSG_SIZE = 264
|
||||
NFC_AIO_MAGIC = 0xA100DA7A
|
||||
@@ -93,6 +95,30 @@ def takeover_authd_socket(ssock: ssl.SSLSocket) -> socket.socket:
|
||||
return raw
|
||||
|
||||
|
||||
def wrap_nfcssl_socket(
|
||||
ssock: ssl.SSLSocket,
|
||||
server_hostname: str) -> ssl.SSLSocket:
|
||||
"""Start the second TLS session used by NBDSSL after PROXY.
|
||||
|
||||
After ``200 Connect ha-nfcssl``, authd TLS is finished and
|
||||
``ha-nfcssl`` expects a new ClientHello on the same TCP connection.
|
||||
The fd is dup'd so the original authd ``SSLSocket`` can be closed
|
||||
later without ``SSL_shutdown`` of this NFCSSL session.
|
||||
|
||||
Args:
|
||||
ssock: The TLS socket from ``nfc_auth.connect_authd``.
|
||||
server_hostname: Host name passed to ``SSLContext.wrap_socket``.
|
||||
"""
|
||||
raw = takeover_authd_socket(ssock)
|
||||
ssl_context = _ssl_client_context(verify=False)
|
||||
try:
|
||||
return ssl_context.wrap_socket(
|
||||
raw, server_hostname=server_hostname)
|
||||
except Exception:
|
||||
raw.close()
|
||||
raise
|
||||
|
||||
|
||||
def _recvn(sock: socket.socket, size: int) -> bytes:
|
||||
buf = bytearray()
|
||||
while len(buf) < size:
|
||||
@@ -146,7 +172,7 @@ class NfcDisk:
|
||||
"""Wrap an AIO session that already has ``path`` open.
|
||||
|
||||
Args:
|
||||
sock: Raw NFC socket after handshake.
|
||||
sock: NFC socket after handshake (raw TCP for nbd, TLS for nbdssl).
|
||||
path: Datastore path that was opened.
|
||||
handle: Server file handle from OPEN_FILE.
|
||||
sector_size: Sector size from the OPEN_FILE reply.
|
||||
@@ -401,7 +427,9 @@ def open_disk(
|
||||
|
||||
Matches VDDK ``VixDiskLib_Open`` of a datastore path after the NFC
|
||||
ticket and authd PROXY handshake: session init, AIO open, then
|
||||
``NFC_AIO_MSG_OPEN_FILE`` with type ``NFC_DISK``.
|
||||
``NFC_AIO_MSG_OPEN_FILE`` with type ``NFC_DISK``. NBD dups the
|
||||
authd fd and sends plaintext NFC. NBDSSL wraps that dup in a
|
||||
second TLS session (``session.nfc_ssl``).
|
||||
|
||||
Args:
|
||||
session: Result of ``nfc_auth.authenticate``.
|
||||
@@ -412,7 +440,11 @@ def open_disk(
|
||||
version: Client NFC protocol version (lab ESXi answered 11).
|
||||
read_only: When True, open with VDDK's read-only NFC flags.
|
||||
"""
|
||||
sock = takeover_authd_socket(session.authd_sock)
|
||||
if session.nfc_ssl:
|
||||
sock = wrap_nfcssl_socket(
|
||||
session.authd_sock, session.ticket.host)
|
||||
else:
|
||||
sock = takeover_authd_socket(session.authd_sock)
|
||||
try:
|
||||
_handshake(sock, client_name, op_id, version)
|
||||
disk = NfcDisk(sock, disk_path, handle=0, sector_size=NFC_SECTOR_SIZE)
|
||||
|
||||
@@ -70,13 +70,20 @@ def _parse_vm_moref(vmx_spec: Optional[str]) -> str:
|
||||
return vmx_spec
|
||||
|
||||
|
||||
def _require_nbd(transport_modes: Optional[str]) -> None:
|
||||
def _select_transport(transport_modes: Optional[str]) -> str:
|
||||
"""Return the first requested transport this replacement implements.
|
||||
|
||||
``None`` defaults to ``nbdssl``. A colon-separated list (VDDK
|
||||
style, for example ``file:nbdssl:nbd``) picks the first of
|
||||
``nbdssl`` or ``nbd``.
|
||||
"""
|
||||
if transport_modes is None:
|
||||
return
|
||||
modes = [m for m in transport_modes.split(":") if m]
|
||||
if "nbd" not in modes:
|
||||
raise NotImplementedError(
|
||||
f"only nbd transport is supported, got {transport_modes!r}")
|
||||
return "nbdssl"
|
||||
for mode in transport_modes.split(":"):
|
||||
if mode in ("nbdssl", "nbd"):
|
||||
return mode
|
||||
raise NotImplementedError(
|
||||
f"supported transports are nbdssl and nbd, got {transport_modes!r}")
|
||||
|
||||
|
||||
class _Connection:
|
||||
@@ -89,13 +96,15 @@ class _Connection:
|
||||
snapshot_ref: Optional[str],
|
||||
thumbprint: Optional[str],
|
||||
allow_untrusted: bool,
|
||||
read_only: bool) -> None:
|
||||
read_only: bool,
|
||||
transport_mode: str) -> None:
|
||||
self.si = si
|
||||
self.vm_moref = vm_moref
|
||||
self.snapshot_ref = snapshot_ref
|
||||
self.thumbprint = thumbprint
|
||||
self.allow_untrusted = allow_untrusted
|
||||
self.read_only = read_only
|
||||
self.transport_mode = transport_mode
|
||||
|
||||
|
||||
class _DiskHandle:
|
||||
@@ -104,9 +113,11 @@ class _DiskHandle:
|
||||
def __init__(
|
||||
self,
|
||||
disk: nfc_open.NfcDisk,
|
||||
authd_sock) -> None:
|
||||
authd_sock,
|
||||
transport_mode: str) -> None:
|
||||
self.disk = disk
|
||||
self.authd_sock = authd_sock
|
||||
self.transport_mode = transport_mode
|
||||
|
||||
|
||||
class VixDiskLibHandle:
|
||||
@@ -161,12 +172,11 @@ class VixDiskLibHandle:
|
||||
|
||||
def get_transport_modes(self) -> list[str]:
|
||||
"""Return the transport modes this replacement implements."""
|
||||
return ["nbd"]
|
||||
return ["nbdssl", "nbd"]
|
||||
|
||||
def get_transport_mode(self, disk_handle: _DiskHandle) -> str:
|
||||
"""Return the transport used for ``disk_handle``."""
|
||||
del disk_handle
|
||||
return "nbd"
|
||||
return disk_handle.transport_mode
|
||||
|
||||
@contextlib.contextmanager
|
||||
def connect(
|
||||
@@ -196,12 +206,14 @@ class VixDiskLibHandle:
|
||||
vmx_spec: VM selector, ``moref=vm-…``.
|
||||
snapshot_ref: Snapshot moref; unused on the NFC ticket.
|
||||
read_only: When False, the disk may be opened for write.
|
||||
transport_modes: ``nbd`` or a colon list that includes ``nbd``.
|
||||
transport_modes: ``nbdssl``, ``nbd``, or a colon list. The
|
||||
first supported mode is used; ``None`` defaults to
|
||||
``nbdssl``.
|
||||
port: HTTPS port, usually 443.
|
||||
allow_untrusted: Skip management TLS verification when True.
|
||||
"""
|
||||
LOG.debug("Connecting VixDiskLib: %s", server_name)
|
||||
_require_nbd(transport_modes)
|
||||
transport_mode = _select_transport(transport_modes)
|
||||
vm_moref = _parse_vm_moref(vmx_spec)
|
||||
si = nfc_auth.connect_vim(
|
||||
server_name,
|
||||
@@ -212,7 +224,8 @@ class VixDiskLibHandle:
|
||||
allow_untrusted=allow_untrusted or not thumbprint)
|
||||
conn = _Connection(
|
||||
si, vm_moref, snapshot_ref, thumbprint,
|
||||
allow_untrusted or not thumbprint, read_only)
|
||||
allow_untrusted or not thumbprint, read_only,
|
||||
transport_mode)
|
||||
try:
|
||||
yield conn
|
||||
finally:
|
||||
@@ -243,18 +256,20 @@ class VixDiskLibHandle:
|
||||
"ConnectEx was read-only; cannot open for write")
|
||||
|
||||
vm = vim.VirtualMachine(conn.vm_moref, conn.si._stub)
|
||||
nfc_ssl = conn.transport_mode == "nbdssl"
|
||||
ticket = nfc_auth.get_nfc_ticket(
|
||||
conn.si, vm, read_only=read_only, disk_path=disk_path)
|
||||
authd_sock = nfc_auth.connect_authd(
|
||||
ticket, allow_untrusted=conn.allow_untrusted)
|
||||
session = nfc_auth.NfcAuthSession(conn.si, ticket, authd_sock)
|
||||
ticket, allow_untrusted=conn.allow_untrusted, nfc_ssl=nfc_ssl)
|
||||
session = nfc_auth.NfcAuthSession(
|
||||
conn.si, ticket, authd_sock, nfc_ssl=nfc_ssl)
|
||||
try:
|
||||
disk = nfc_open.open_disk(
|
||||
session, disk_path, read_only=read_only)
|
||||
except Exception:
|
||||
authd_sock.close()
|
||||
raise
|
||||
handle = _DiskHandle(disk, authd_sock)
|
||||
handle = _DiskHandle(disk, authd_sock, conn.transport_mode)
|
||||
try:
|
||||
yield handle
|
||||
finally:
|
||||
|
||||
Reference in New Issue
Block a user