VDDK always decompresses the chunks it retrieves. However, some callers may be interested in the compressed chunks, which may be forwarded to another service involved in the backup process. We'll add a read flag (skip_decompression). If set, the read operation will return a "ReadResult" container, containing a list of fragments and their compressed / decompressed lengths. If the returned buffer size matches the AIO buffer size (which now becomes configurable), at most one fragment will be returned. While at it, we're adding perf tests that check various AIO buffer sizes, cross checking against VDDK.
12 KiB
VDDK NFC disk open
This document records how VMware VDDK opens a VMDK over NBD/NFC after
the authd handshake in docs/nfc_auth.md, and how OpenVixDiskLib
(openvixdisklib/nfc_open.py) reproduces that path. Findings come from
VDDK 8.0.2 verbose logs
(vixDiskLib.nfc.LogLevel=4) plus an LD_PRELOAD intercept of
write / read on the ESXi:902 file descriptor. Capture method:
docs/reverse_engineering_procedure.md.
Authentication is already done: VIM login, NFC ticket (NfcGetVmFiles
for read-only, NfcRandomAccessOpenDisk for write), TLS to authd,
SESSION / BANNER / THUMBPRINT_SHA2 PlainText / PROXY. This
stage starts at 200 Connect ha-nfc (NBD) or 200 Connect ha-nfcssl
(NBDSSL) and ends with an open file handle that can read and write
sectors. Flags 0x1a require the writable ticket; the same flags on a
GetVmFiles ticket fail with VIX_E_FILE_READ_ONLY.
Mapping from VDDK
| VDDK call / log | Wire effect |
|---|---|
VixDiskLib_Open |
Ticket + authd (see nfc_auth.md), then this protocol |
NBD_ClientOpen vpxa-nfc://[ds] path.vmdk@esxi:902 |
Datastore path is the NFC open argument, not the ticket |
NBD_ClientOpen vpxa-nfcssl://… / useSSL=1 |
Same NFC after a second TLS handshake on the authd fd |
useSSL=0 |
NFC bytes are raw TCP, not SSL_write |
NfcProcessSessionParams flags 0x3 |
Classic 264-byte session messages |
SendConnectionDataMsg payloadInfo 4 and 7 |
Client name vddk (4) and opId nbdmode (7) |
| Server version 11 | Classic version message; 11 on this ESXi 8 lab |
NfcAio_OpenSession |
AIO framing after the classic handshake |
NfcUtil_PrintFileInfoOpenFlag NFC_DISK 0x1e |
NFC_AIO_MSG_OPEN_FILE (read-only) |
Open without VIXDISKLIB_FLAG_OPEN_READ_ONLY |
OPEN_FILE flags 0x1a (read-write) |
VixDiskLib_Read / VixDiskLib_Write |
NFC_AIO_MSG_IO + sector bytes |
VIXDISKLIB_FLAG_OPEN_COMPRESSION_FASTLZ |
IO opcode high bits 2; extra data is FastLZ |
snapshot_ref is still not on the wire. Integration tests pass the
flat VMDK created with the temporary lab VM.
After PROXY: NBD plaintext vs NBDSSL wrap
THUMBPRINT_SHA2 PlainText is used for both transports. The PROXY
service name selects whether NFC gets a second TLS session.
NBD (PROXY vpxa-nfc → 200 Connect ha-nfc, VDDK useSSL=0):
- Authd commands stay inside the original TLS session (
SSL_write/SSL_read). - After
200 Connect ha-nfc, NFC iswrite(SSL_get_fd(ssl), …)/readon that descriptor. Those buffers are not TLS records (0x17 0x03 …). - An SSL hook that only interposes
SSL_write/SSL_readgoes silent after PROXY; awrite/readhook on port 902 shows the frames. - Python must not use
SSLSocket.sendhere: that would encrypt bytes the server now reads as NFC.nfc_open.takeover_authd_socketdups the fd.unwrap()/SSL_shutdownis not used.
NBDSSL (PROXY vpxa-nfcssl → 200 Connect ha-nfcssl, useSSL=1):
- Authd commands are the same, including
THUMBPRINT_SHA2 PlainText. - After
200 Connect ha-nfcssl, both sides abandon the authd TLS session.ha-nfcsslexpects a new ClientHello on the same TCP connection. nfc_open.wrap_nfcssl_socketdups the fd andSSLContext.wrap_sockets it. NFC then usesSSLSocket.sendall/recv(TLS application data). The classic 264-byte handshake still sends the ASCII bodyPlainText; that is NFC's own encoding, not the authd transport.
Classic 264-byte messages
Before AIO, both peers send a fixed 264-byte struct, little-endian:
| Offset | Type | Meaning |
|---|---|---|
| 0 | uint32 |
Message type |
| 4 | remainder | Type-specific fields, zero-padded to 264 |
Types seen in this Open (names from libvixDiskLib strings matched to
the first uint32):
| Type | Name (inferred) | Body |
|---|---|---|
| 43 | NFC_HANDSHAKE |
ASCII PlainText at offset 4 |
| 33 | NFC_SESSION_PARAMS |
zeros |
| 36 | session-params reply | uint32 1 at offset 16 |
| 51 | version | uint32 protocol version (11) at offset 4 |
| 54 | NFC_CONNECTION_DATA |
uint32 nameLen, uint32 opIdLen |
| 55 | session features | uint32 0x3 (interruption | switch) |
| 52 | NFC_AIO_SESSION_OPEN |
zeros |
| 4 | NFC_SESSION_COMPLETE |
zeros (sent on close) |
After type 54, VDDK writes the two connection-data payloads as raw
strings, not 264-byte frames: vddk then nbdmode. Lengths 4 and 7
are the payloadInfo values in the VDDK log.
Handshake order (client → server unless noted):
C: 43 PlainText
C: 33
S: 36
C: 51 version=11
S: 51 version=11
C: 54 nameLen=4 opIdLen=7
C: "vddk"
C: "nbdmode"
C: 55 features=3
C: 52
S: 52
Server version 11 is what this lab returned. VDDK logs that connection info requires version ≥ 3.
AIO framing
Once type 52 has been acknowledged, I/O uses a 16-byte header:
uint32 magic # 0xA100DA7A, wire bytes 7a da 00 a1
uint32 type # NfcAioSendMessage "type ="
uint32 size # payload bytes that follow the header
uint32 opId # monotonic, starting at 0
Then size bytes of payload. Variable-length extras (VMDK path, DDB
key name, read data) are separate write/read calls after that
payload, not counted in size.
The server echoes the same header (magic, type, size, opId)
and a payload of size bytes.
Magic mismatch is the invalid msg hdr magic string in VDDK. Type 1
is NFC_AIO_MSG_ERROR.
AIO types used for Open / Read / Close, correlated with the consecutive
NFC_AIO_MSG_* string table and VDDK logs:
| Type | Name | Payload size | Extra on the wire |
|---|---|---|---|
| 2 | OPEN_SESSION |
16 | |
| 9 | SET_SOCK_OPTS |
12 | |
| 22 | SET_RES_POOL |
4 | |
| 4 | OPEN_FILE |
60 | path string |
| 11 | DDB_GET |
16 | key name (VDDK only) |
| 7 | IO |
44 | sector bytes (read reply / write request) |
| 5 | CLOSE_FILE |
8 | |
| 3 | CLOSE_SESSION |
4 |
opId increases by one per client message. Replies reuse the request
opId.
VDDK Open also issues several DDB_GET queries (resumeConsolidateSector,
isDigest, iofilters, …). The server answered “key is not found”
(16 zero bytes) on this unencrypted disk. They are not required to
obtain a file handle or to read sector 0.
OPEN_SESSION / sockopts / resource pool
OPEN_SESSION payload is 16 bytes, little-endian:
| Offset | Type | Meaning |
|---|---|---|
| 0 | uint32 |
0 (unused in captures) |
| 4 | uint32 |
AIO buffer size in bytes (VDDK default 65536) |
| 8 | uint32 |
Buffer count (VDDK nfcAio.Session.BufCount) |
| 12 | uint32 |
0 |
VDDK config vixDiskLib.nfcAio.Session.BufSizeIn64KB is that byte size
divided by 64 KiB (1 → 65536, 32 → 2097152). The server replies
with 16 zeros; it still uses the requested size for IO extras.
A 129-sector read is two fragments at 64 KiB, and one 66048-byte
fragment at 2 MiB. Lab ESXi 8 accepted 2 MiB (BufCount 1 and 4) and
rejected 16 MiB and 32 MiB (OPEN_SESSION AIO error). Broadcom's 16 MiB
figure is session memory (size × count), not a larger extra; the
per-buffer max on the wire is 2 MiB. Probe:
docs/probing_samples/vddk_aio_bufsize_probe.py.
SET_SOCK_OPTS is 12 zero bytes (server returns send/recv buffer sizes
and a uint32 flag), then uint32 1 (SET_RES_POOL, log: “Setting
Resource Pool(1)”).
OPEN_FILE
60-byte payload, little-endian:
| Offset | Type | Value on a VDDK open |
|---|---|---|
| 0 | uint32 |
Path length in bytes |
| 4 | uint32 |
0 |
| 8 | uint32 |
0 |
| 12 | uint32 |
0 |
| 16 | uint32 |
2 (NFC_DISK) |
| 20 | uint32 |
0x0000001e (read-only) or 0x1a (read-write) |
| 24 | 36 bytes | zeros |
Immediately afterwards the client writes the path, no NUL terminator
(for example [datastore0] ovdl-test-…/ovdl-test-….vmdk).
Reply payload (60 bytes), fields that matter:
| Offset | Type | Meaning |
|---|---|---|
| 8 | uint64 |
File handle (opaque, per open) |
| 16 | uint32 |
File type (2 = NFC_DISK) |
| 20 | uint32 |
Flags echoed (0x1e or 0x1a) |
| 36 | uint32 |
Sector size (512 on this VM) |
Later AIO messages pass that handle as a uint64.
IO (read / write)
Sector reads and writes are NFC_AIO_MSG_IO (type 7). Request layout,
read fragments, and write extras are documented in docs/nfc_read.md
and docs/nfc_write.md. NfcDisk.read / NfcDisk.write match
VixDiskLib_Read / VixDiskLib_Write.
Close
CLOSE_FILE (handle as uint64), CLOSE_SESSION (uint32 0), then
classic type 4 NFC_SESSION_COMPLETE.
OpenVixDiskLib
| Piece | Module |
|---|---|
| VIM + authd | openvixdisklib.nfc_auth.authenticate |
| Dup fd, skip TLS for NFC | openvixdisklib.nfc_open.takeover_authd_socket |
| Second TLS for nbdssl | openvixdisklib.nfc_open.wrap_nfcssl_socket |
| FastLZ for NBD compression | openvixdisklib.fastlz (pip pyfastlz) |
| Handshake + AIO + OPEN_FILE | openvixdisklib.nfc_open.open_disk |
| AIO extra size / pool count | open_disk(..., aio_buffer_size=, aio_buffer_count=) |
| Sector read / write / close | openvixdisklib.nfc_open.NfcDisk |
Run:
.venv/bin/pytest tests/integration/test_nfc_open.py
The test opens the temporary lab VMDK, asserts an opaque handle and
sector_size=512, writes sector 0, and reads it back. Multi-sector
I/O: docs/nfc_read.md, docs/nfc_write.md, and
tests/integration/test_nfc_read_write.py.
What is still VDDK-only
DDB_GET/ geometry / zlib and skipz compression / encryption keysNFC_DELTA_DISK, change-block tracking- Host-switch (
NFC_AIO_SWITCH_HOST_*) - Direct ESXi
ha-nfcwithout vCentervpxa-nfc
Reads after open are in docs/nfc_read.md. Writes are in
docs/nfc_write.md.