# Copyright 2026 Cloudbase Solutions Srl # All Rights Reserved. """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: 1. Classic 264-byte NFC messages (handshake, version, connection data, AIO session open). 2. NFC AIO frames (16-byte header plus payload) to open a VMDK and read or write sectors. pyVmomi is not involved here; the ticket and TLS authd handshake already happened in ``nfc_auth``. """ from __future__ import annotations import os import socket import ssl import struct from openvixdisklib.nfc_auth import NfcAuthSession NFC_MSG_SIZE = 264 NFC_AIO_MAGIC = 0xA100DA7A NFC_AIO_HDR_SIZE = 16 NFC_SECTOR_SIZE = 512 NFC_PROTOCOL_VERSION = 11 # Max data bytes in one AIO IO reply fragment (NfcAioInitSession buffer). NFC_AIO_BUFFER_SIZE = 65536 # Classic NFC message types observed on the wire (uint32 at offset 0). NFC_MSG_SESSION_COMPLETE = 4 NFC_MSG_SESSION_PARAMS = 33 NFC_MSG_SESSION_PARAMS_REPLY = 36 NFC_MSG_HANDSHAKE = 43 NFC_MSG_VERSION = 51 NFC_MSG_AIO_SESSION_OPEN = 52 NFC_MSG_CONNECTION_DATA = 54 NFC_MSG_SESSION_FEATURES = 55 # SessionParams / feature bits from VDDK logs (interruption | switch). NFC_SESSION_FEATURE_INTERRUPTION_SWITCH = 3 # AIO message types (NfcAioSendMessage "type = N"). NFC_AIO_MSG_ERROR = 1 NFC_AIO_MSG_OPEN_SESSION = 2 NFC_AIO_MSG_CLOSE_SESSION = 3 NFC_AIO_MSG_OPEN_FILE = 4 NFC_AIO_MSG_CLOSE_FILE = 5 NFC_AIO_MSG_IO = 7 NFC_AIO_MSG_SET_SOCK_OPTS = 9 NFC_AIO_MSG_DDB_GET = 11 NFC_AIO_MSG_SET_RES_POOL = 22 # Open-file body: file type NFC_DISK. 0x1e is what VDDK sends for # VIXDISKLIB_FLAG_OPEN_READ_ONLY; writable opens clear bit 0x04 (0x1a). NFC_DISK = 2 NFC_OPEN_FLAGS_READ_ONLY = 0x1E NFC_OPEN_FLAGS_READ_WRITE = 0x1A NFC_AIO_IO_WRITE = 0 NFC_AIO_IO_READ = 1 class NfcProtocolError(ConnectionError): """Raised when an NFC message is malformed or reports failure.""" def takeover_authd_socket(ssock: ssl.SSLSocket) -> socket.socket: """Return a raw socket on the authd TCP connection. VDDK writes NFC with ``write(SSL_get_fd(ssl), ...)`` after PROXY, so those bytes are not TLS records. Duping the fd lets Python do the same without ``SSLSocket.send`` re-encrypting, and without ``SSL_shutdown``. Args: ssock: The TLS socket from ``nfc_auth.connect_authd``. """ timeout = ssock.gettimeout() raw = socket.socket( family=ssock.family, type=ssock.type, proto=ssock.proto, fileno=os.dup(ssock.fileno())) raw.settimeout(timeout) return raw def _recvn(sock: socket.socket, size: int) -> bytes: buf = bytearray() while len(buf) < size: chunk = sock.recv(size - len(buf)) if not chunk: raise NfcProtocolError( f"NFC connection closed, needed {size} bytes, got {len(buf)}") buf.extend(chunk) return bytes(buf) def _send_nfc_msg( sock: socket.socket, msg_type: int, body: bytes = b"") -> None: if len(body) > NFC_MSG_SIZE - 4: raise ValueError("NFC classic message body too large") frame = struct.pack(" tuple[int, bytes]: frame = _recvn(sock, NFC_MSG_SIZE) msg_type = struct.unpack_from(" bytes: return struct.pack( " tuple[int, int, int]: magic, msg_type, size, op_id = struct.unpack_from(" None: """Wrap an AIO session that already has ``path`` open. Args: sock: Raw NFC socket after handshake. path: Datastore path that was opened. handle: Server file handle from OPEN_FILE. sector_size: Sector size from the OPEN_FILE reply. """ self._sock = sock self._op_id = 0 self.path = path self.handle = handle self.sector_size = sector_size self._closed = False def _next_op_id(self) -> int: op_id = self._op_id self._op_id += 1 return op_id def _aio_roundtrip( self, msg_type: int, payload: bytes, extra: bytes = b"", extra_recv: int = 0) -> bytes: """Send one AIO request and return the reply payload (+ extra).""" op_id = self._next_op_id() self._sock.sendall( _pack_aio_hdr(msg_type, len(payload), op_id) + payload) if extra: self._sock.sendall(extra) rhdr = _recvn(self._sock, NFC_AIO_HDR_SIZE) magic, rtype, rsize, rop = struct.unpack_from(" bytes: """Read ``num_sectors`` starting at ``start_sector``. Matches ``VixDiskLib_Read``: one ``NFC_AIO_MSG_IO`` request in byte units. If the length exceeds the AIO buffer (64 KiB) the server replies with several same-``opId`` fragments. Args: start_sector: Sector offset from the start of the disk. num_sectors: Number of sectors to read. """ if num_sectors < 1: raise ValueError("num_sectors must be at least 1") length = num_sectors * self.sector_size offset = start_sector * self.sector_size payload = struct.pack( " remaining: raise NfcProtocolError( f"AIO IO chunk length {chunk_len} invalid, " f"remaining {remaining}") data.extend(_recvn(self._sock, chunk_len)) return bytes(data) def write( self, start_sector: int, num_sectors: int, data: bytes) -> None: """Write ``num_sectors`` starting at ``start_sector``. Matches ``VixDiskLib_Write``: one ``NFC_AIO_MSG_IO`` request per chunk in byte units, with sector bytes sent after the 44-byte payload. Chunks larger than the AIO buffer (64 KiB) are split. Args: start_sector: Sector offset from the start of the disk. num_sectors: Number of sectors to write. data: Bytes to write; length must be ``num_sectors * sector_size``. """ if num_sectors < 1: raise ValueError("num_sectors must be at least 1") length = num_sectors * self.sector_size if len(data) != length: raise ValueError( f"write data is {len(data)} bytes, need {length}") max_sectors = NFC_AIO_BUFFER_SIZE // self.sector_size offset_sectors = start_sector remaining = data while remaining: n_sectors = min(len(remaining) // self.sector_size, max_sectors) chunk = remaining[:n_sectors * self.sector_size] self._write_once(offset_sectors, n_sectors, chunk) offset_sectors += n_sectors remaining = remaining[n_sectors * self.sector_size:] def _write_once( self, start_sector: int, num_sectors: int, data: bytes) -> None: length = num_sectors * self.sector_size offset = start_sector * self.sector_size payload = struct.pack( " None: """Close the VMDK, the AIO session, and the classic NFC session.""" if self._closed: return self._closed = True try: self._aio_roundtrip( NFC_AIO_MSG_CLOSE_FILE, struct.pack(" "NfcDisk": return self def __exit__(self, exc_type, exc, tb) -> None: self.close() def _handshake( sock: socket.socket, client_name: str, op_id: str, version: int) -> None: """Run the classic NFC session handshake used by VDDK NBD.""" _send_nfc_msg(sock, NFC_MSG_HANDSHAKE, b"PlainText") _send_nfc_msg(sock, NFC_MSG_SESSION_PARAMS) reply_type, _ = _recv_nfc_msg(sock) if reply_type != NFC_MSG_SESSION_PARAMS_REPLY: raise NfcProtocolError( f"expected session-params reply {NFC_MSG_SESSION_PARAMS_REPLY}, " f"got {reply_type}") _send_nfc_msg(sock, NFC_MSG_VERSION, struct.pack(" None: disk._aio_roundtrip( NFC_AIO_MSG_OPEN_SESSION, bytes(16)) disk._aio_roundtrip( NFC_AIO_MSG_SET_SOCK_OPTS, bytes(12)) disk._aio_roundtrip( NFC_AIO_MSG_SET_RES_POOL, struct.pack(" tuple[int, int]: if len(body) < 40: raise NfcProtocolError(f"OPEN_FILE reply too short: {len(body)}") handle, file_type, _flags = struct.unpack_from(" NfcDisk: """Open ``disk_path`` over the authenticated authd socket. 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``. Args: session: Result of ``nfc_auth.authenticate``. disk_path: Datastore path, for example ``[datastore0] vm/vm.vmdk``. client_name: NFC client name; VDDK sends ``vddk``. op_id: NFC operation id; VDDK NBD sends ``nbdmode``. 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) try: _handshake(sock, client_name, op_id, version) disk = NfcDisk(sock, disk_path, handle=0, sector_size=NFC_SECTOR_SIZE) _aio_prepare(disk) path_b = disk_path.encode("utf-8") open_flags = ( NFC_OPEN_FLAGS_READ_ONLY if read_only else NFC_OPEN_FLAGS_READ_WRITE) open_body = struct.pack( "