Add nbdssl support

This commit is contained in:
Lucian Petrut
2026-09-07 12:46:00 +00:00
parent c3e3fa5769
commit b7131ec0af
14 changed files with 375 additions and 99 deletions
+35 -7
View File
@@ -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)
+40 -8
View File
@@ -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)
+32 -17
View File
@@ -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: