VDDK is no longer publicly distributed, so this library reverse-engineers the vSphere NFC path and exposes ConnectEx, Open, Read, and Write without the proprietary SDK. Co-authored-by: Cursor <[email protected]>
12 KiB
Reverse-engineering procedure
This is the working method used to replace VDDK’s NBD path with Python.
Protocol details live in docs/nfc_auth.md, docs/nfc_open.md,
docs/nfc_read.md, and docs/nfc_write.md. The capture tool is
described in docs/ssl_hook.md.
This file is the sequence of steps, including dead ends, so later
NFC work can follow the same loop instead of rediscovering it.
Scope so far: VixDiskLib_ConnectEx + VixDiskLib_Open +
VixDiskLib_Read + VixDiskLib_Write against lab vCenter 8.0.1 /
ESXi 8, transport nbd. Driver: tests/integration/ (TestBase
creates a temporary empty VM with a 10 GiB disk in setUpClass and
destroys it in tearDownClass).
Rule from AGENTS.md: reuse pyVmomi for every public VIM operation.
Only reimplement what pyVmomi does not expose.
Loop
Each unknown stage (ticket SOAP, authd, NFC binary) went through:
- Name it from VDDK logs and
stringson the bundled libraries. - See it on the wire (or prove that tcpdump cannot).
- Replay the smallest working subset in Python against the lab.
- Write findings into a protocol doc and keep the hook out of the library path.
Do not skip (2). Log lines such as SESSIONID or useSSL=0 named the
wrong wire command until the intercept existed.
Lab and artifacts
| Item | Where / value |
|---|---|
| VDDK 8.0.2 | .vddk/ (libvixDiskLib, libvddkVimAccess, libvim-types) |
| pyVmomi | .venv |
| Known-good VDDK client | tests/integration/test_vddk.py / tests/integration/vixdisklib.py |
| Verbose NFC logs | vixDiskLib.nfc.LogLevel=4 in a temp VDDK config |
| ctypes Open+Read driver | /tmp/vddk_open_trace.py (not in the library) |
SSL / write hook |
/tmp/sslhook.c → /tmp/sslhook.so |
Always set LD_LIBRARY_PATH to .vddk/ so VDDK uses its own
libssl.so.3. Unset LD_PRELOAD before running the Python replacement;
a leftover write hook will crash pyVmomi’s TLS.
Step 1 — Map the public VDDK calls
tests/integration/test_vddk.py is the specification of what
“success” looks like: login, open the temporary VM’s VMDK, write a
known pattern, read it back.
Turn on VDDK verbose logging around InitEx / ConnectEx / Open. The
logs split the work that the Python API hides:
ConnectEx→ VIM login only.Open→ NFC ticket, authd, NFC handshake, AIO open, then I/O.transport_modes=nbd→ URL formvpxa-nfc://[ds] path.vmdk@esxi:902.snapshot_refdoes not appear on the ticket SOAP call.
That mapping is the table at the top of docs/nfc_auth.md. It tells you
which stage to reverse next and which arguments belong there (VM moref
on the ticket, VMDK path on NFC OPEN_FILE).
Step 2 — Strings and pyVmomi before any capture
strings -a on .vddk/*.so produced candidate tokens before a single
packet was decoded:
- SOAP:
NfcService,NfcGetVmFiles,nfcService,ha-nfc,HostServiceTicket. - authd:
SESSION,BANNER,THUMBPRINT_SHA2,PROXY,USER,PASS,SSL Required. - NFC:
NFC_HANDSHAKE,NFC_CONNECTION_DATA,NFC_AIO_MSG_*,NFC_DISK.
Then check whether pyVmomi already has the type:
from pyVmomi import vim
hasattr(vim, "NfcService") # False
hasattr(vim, "HostServiceTicket") # True
ServiceManager.QueryServiceList on the live vCenter does not list
NFC. GET /sdk/nfcServiceVersions.xml does (urn:nfc 7.0.3.2). The
moref is hardcoded in VDDK (nfcService on vCenter, ha-nfc on ESXi).
Anything public (SmartConnect, vim.VirtualMachine,
HostServiceTicket) stays in pyVmomi. Missing managed types are
registered with CreateManagedType on the same SOAP stub so cookies
and serialization are not reimplemented.
Step 3 — Confirm tcpdump is the wrong tool for TLS stages
tcpdump on 443 and 902 shows TLS records only. That is enough to prove “something talks to vCenter then to ESXi:902”, and not enough for SOAP bodies, authd lines, or NFC headers.
VDDK logs name functions and AIO opId / type / size. They do not
give magic numbers, path placement, or command spacing (BANNER \r\n).
An ESXi-impersonating service was considered (AGENTS.md) and not
needed: the lab answers VDDK, so capturing the real client is simpler
than simulating the server.
Step 4 — Interpose OpenSSL (authd and SOAP)
VDDK 8.0.2 still calls SSL_write / SSL_read. A small LD_PRELOAD
library logs those buffers as hex, tagged with the SSL * pointer.
Run a minimal ctypes program (InitEx, ConnectEx, Open, Read)
under:
export LD_LIBRARY_PATH=…/.vddk
export LD_PRELOAD=/tmp/sslhook.so
export SSLHOOK_LOG=/tmp/sslhook-open.log
python /tmp/vddk_open_trace.py
Parse offline:
- Concatenate adjacent same-direction records (authd
SSL_readis often one byte). - Split by
SSL *. vCenter HTTPS containsPOST /sdkand SOAP. ESXi:902 containsSESSION/PROXY. - An early hook without the pointer mixed both streams; always tag.
The vCenter stream identified the ticket as NfcGetVmFiles with moref
nfcService and xmlns="urn:vim25" (not a guess from strings alone).
The ESXi stream gave the authd command order, including the trailing
space on BANNER and THUMBPRINT_SHA2 PlainText.
Hook implementation notes: docs/ssl_hook.md.
Step 5 — Probe SOAP, then replay only what VDDK sends
With a SmartConnect session, raw SOAP posts were used to learn parameter names and which moref vCenter accepts:
ha-nfcon vCenter →ManagedObjectNotFound.NfcGetServerNfcLibVersionwithouthostForAccess→ invalid argument; with a host moref →11.NfcGetVmFiles(vm)→HostServiceTicket(VDDK read-only Open).NfcRandomAccessOpenReadonly(vm, diskDeviceKey, host)→ same ticket type, disk-scoped read.NfcRandomAccessOpenDisk(vm, diskDeviceKey, host)→ writable ticket.GetVmFilesplusOPEN_FILEflags0x1afails withVIX_E_FILE_READ_ONLY(0x0b).
The replacement registers those methods and calls them through pyVmomi. It does not ship a hand-rolled SOAP client for login or tickets.
Step 6 — Probe authd; record dead ends
Plaintext banner (220 … SSL Required), then ssl.wrap_socket.
Commands before TLS drop the connection.
vCenter UID/password are not sent to port 902. Attempts that failed and must not be retried for this ticket type:
| Attempt | Result |
|---|---|
USER / PASS (vCenter account) |
530 Login incorrect |
USER / PASS with sessionId |
530 Login incorrect |
SESSIONID <sessionId> |
530 Please login with USER and PASS |
CONNECT_VPXA after TLS |
530 Please login with USER and PASS |
Wait for a reply after SESSION |
Hang until BANNER / PROXY follow |
THUMBPRINT_SHA2 with SHA-1 digest |
501 Invalid arguments |
The working sequence is in docs/nfc_auth.md. useSSL=0 in the VDDK
log means skip a second NFCSSL wrap, not skip TLS on 902.
Replay: openvixdisklib/nfc_auth.py /
tests/integration/test_nfc_auth.py. Stop at 200 Connect.
Step 7 — NFC binary: extend the hook to write / read
After PROXY, SSL_write on the ESXi SSL * goes silent. VDDK logs
useSSL=0 / “plain-text connection is deprecated” and then NFC
function names. The bytes are write(SSL_get_fd(ssl), …) / read on
peer port 902.
The hook was extended to those syscalls, filtered with getpeername
port 902, and mutex-locked (VDDK is multi-threaded). Skip TLS records
(16 03 / 17 03) left over from the authd phase.
Correlate each frame with the verbose log line that has the same
type and size (NfcAioSendMessage: opId = … type = … size = …).
Name the types from the consecutive NFC_AIO_MSG_* string table in
libvixDiskLib.so. Lengths 4 and 7 on the connection-data message are
the ASCII strings vddk and nbdmode sent in the next two writes.
Classic NFC uses a 264-byte padded struct; AIO uses a 16-byte header
(magic 0xA100DA7A) plus payload; path / DDB key / sector data are
extra writes not included in size.
Step 8 — Replay the smallest subset, then compare to VDDK
Python must dup the authd fd and send NFC as raw TCP.
SSLSocket.send would encrypt; unwrap() would SSL_shutdown. VDDK
does neither.
openvixdisklib/nfc_open.py replays handshake + AIO OPEN_SESSION /
sockopts / resource pool / OPEN_FILE. VDDK’s extra DDB_GET keys
were omitted once a file handle was enough to read. Proof of open:
tests/integration/test_nfc_open.py.
Do not copy every VDDK message. Copy what the server requires for the Python API you are replacing.
Step 9 — Vary VixDiskLib_Read until IO fields stop moving
A single-sector read is not enough to decode NFC_AIO_MSG_IO. Drive
VDDK with several (startSector, numSectors) pairs in one Open
(including n=128 = 64 KiB and n=129) under the write/read hook.
What that comparison showed:
- Wire units are bytes (
offset = start * 512,length = n * 512). - Request size stays 44; data is extra after the payload.
- VDDK sends one request even when
length > 65536. The server replies with several type-7 messages that shareopId, each with a chunk length at payload offset 32 (max 65536). - Treating offset 36 as
NFC_DISK(2) was a 1-sector coincidence; VDDK repeats the byte length there. - Zeros on the wire are real transferred zeros, not a sparse skip.
Replay: NfcDisk.read loops on fragments until length bytes arrive.
Proof: tests/integration/test_nfc_read_write.py writes a known pattern
(including a 129-sector read that must assemble two fragments) and
checks the bytes that came back.
Step 10 — Writes from the same IO message
VixDiskLib_Write uses the same 44-byte NFC_AIO_MSG_IO layout as
read. The direction field at offset 8 is 0 instead of 1, and the
sector bytes are sent after the payload (like the path on
OPEN_FILE). Writable OPEN_FILE flags are 0x1a (the captured
read-only flags 0x1e with bit 0x04 cleared, matching
VIXDISKLIB_FLAG_OPEN_READ_ONLY in .vddk/vixDiskLib.h).
Writable OPEN_FILE still failed with VIX_E_FILE_READ_ONLY until
the ticket switched from NfcGetVmFiles to NfcRandomAccessOpenDisk
(libvim-types.so: vmodl randomAccessOpen ↔ WSDL
NfcRandomAccessOpenDisk). Integration tests create a temporary empty
10 GiB VM for the run so writes cannot land on other lab disks.
The Python client splits writes larger than 64 KiB; it does not send a
single oversized write the way VDDK sends an oversized read. Details:
docs/nfc_write.md. Proof: write then read in
tests/integration/test_nfc_read_write.py and the VDDK cross-check in
tests/integration/test_crosscheck.py.
What to write down
After a stage works:
| Document | Contents |
|---|---|
docs/nfc_auth.md |
Ticket SOAP + authd wire format |
docs/nfc_open.md |
Classic NFC + AIO open |
docs/nfc_read.md |
AIO IO / VixDiskLib_Read |
docs/nfc_write.md |
AIO IO / VixDiskLib_Write |
docs/ssl_hook.md |
Capture tool only |
docs/reverse_engineering_procedure.md |
This procedure (update when the method changes) |
Keep the hook and ctypes driver under /tmp. They are not part of the
replacement library.
Next stages (same procedure)
Not yet reversed, same loop as above:
DDB_GET/ disk geometry, compression, encrypted disksNFC_DELTA_DISK, CBT /QueryAllocatedBlocksVixDiskLib_GetInfocapacity- Host-switch AIO messages
useSSL=1(second NFCSSL wrap)- Direct ESXi
ha-nfcwithout vCentervpxa-nfc