How the Guest Daemon vphoned Communicates with the Host Over VSock Using a Length-Prefixed JSON Protocol

The guest daemon vphoned communicates with the host by opening a VSock listener on port 1337 and exchanging length-prefixed JSON messages over AF_VSOCK, where each packet consists of a 4-byte big-endian length header followed by UTF-8 encoded JSON payloads containing protocol version, message type, and command-specific data.

The vphoned guest daemon runs inside the iOS virtual machine as an Objective-C launch daemon, enabling bidirectional control between the host and guest in the Lakr233/vphone-cli project. It implements a robust length-prefixed JSON protocol over AF_VSOCK to handle commands ranging from HID input to file transfers. Understanding this communication architecture reveals how the host-side VPhoneControl.swift establishes reliable command and control channels with the virtualized iOS environment.

VSock Connection Architecture

The communication stack relies on the AF_VSOCK address family, which provides socket communication between virtual machines and their hosts without network-level overhead.

Guest-Side Listener Configuration

Inside the iOS VM, vphoned (implemented in scripts/vphoned/vphoned.m) initializes a VSock listener bound to port 1337 (VPHONED_PORT). The daemon uses standard BSD socket APIs with AF_VSOCK to accept incoming connections from the hypervisor. When a host connects, vphoned spawns a client handler thread running handle_client to manage the persistent connection.

Host-Side Socket Device

On the host side, VPhoneControl.swift (located in sources/vphone-cli/VPhoneControl.swift) creates a VZVirtioSocketDevice attachment and connects to the guest's port 1337. This establishes the bidirectional byte stream that carries the length-prefixed JSON protocol between the macOS host process and the virtualized iOS guest.

Length-Prefixed JSON Protocol Format

Both endpoints use an identical framing strategy referred to as the "vphone-control" protocol, ensuring message boundary integrity over the byte-stream VSock connection.

Message Framing Structure

Every message follows a strict length-prefixed binary format:

  1. Header: 4 bytes representing the payload length as a uint32 in big-endian (network) order
  2. Payload: UTF-8 encoded JSON data exactly matching the header's length specification

The C implementation in scripts/vphoned/vphoned_protocol.h provides vp_write_message, which serializes an NSDictionary to JSON and prepends the length header:

// vphoned_protocol.h – writeMessage implementation
BOOL vp_write_message(int fd, NSDictionary *dict);
// Serializes dict to JSON, writes 4-byte big-endian length, then the UTF-8 bytes

The Swift counterpart in VPhoneControl.swift mirrors this exactly:

// VPhoneControl.swift – writeMessage
private func writeMessage(fd: Int32, dict: [String: Any]) -> Bool {
    let json = try? JSONSerialization.data(withJSONObject: dict)
    var header = UInt32(json!.count).bigEndian
    // Write header (4 bytes) followed by JSON payload
}

JSON Payload Schema

Every JSON message contains mandatory fields:

  • "v": Protocol version (currently 1)
  • "t": Message type string (e.g., "hello", "hid", "file_get", "update")
  • "id" (optional): Hexadecimal request identifier echoed in responses for correlation

Additional fields depend on the specific command type.

Handshake and Capability Negotiation

Upon connection establishment, the host initiates a handshake by sending a "hello" message. This message may include a bin_hash field containing the SHA-256 hash of the host's vphoned binary to enable automatic guest updates.

The guest processes this in vphoned.m within the handle_client function (lines 86-103):

// vphoned.m – handshake processing
NSDictionary *hello = vp_read_message(fd);
// Verify protocol version "v"
NSMutableDictionary *helloResp = @{
    @"v": @PROTOCOL_VERSION,
    @"t": @"hello",
    @"name": @"vphoned",
    @"caps": caps  // Array of capability strings
}.mutableCopy;
// Report IPv4 address and check if update needed
if (![hostHash isEqualToString:localHash]) {
    helloResp[@"need_update"] = @YES;
}
vp_write_message(fd, helloResp);

If the binary hashes differ, the guest sets "need_update": true, triggering the host to push a new binary via the update flow.

Command Exchange and Dispatch

Following the handshake, the connection enters a command loop where the host sends operation requests and the guest returns structured responses.

Host Request Construction

In VPhoneControl.swift, host commands are constructed as dictionaries with protocol metadata and command parameters. For example, sending an HID key press:

// VPhoneControl.swift – HID command construction
func sendHIDPress(page: UInt32, usage: UInt32) {
    nextRequestId += 1
    let msg: [String: Any] = [
        "v": Self.protocolVersion,
        "t": "hid",
        "id": String(nextRequestId, radix: 16),
        "page": page,
        "usage": usage
    ]
    guard let fd = connection?.fileDescriptor,
          writeMessage(fd: fd, dict: msg) else { return }
}

Guest Command Dispatch

The guest's handle_client function in vphoned.m loops on vp_read_message(fd), dispatching based on the "t" field:

// vphoned.m – command dispatch (lines 88-122)
NSString *type = msg[@"t"];
NSString *reqId = msg[@"id"];

if ([type isEqualToString:@"hid"]) {
    uint32_t page = [msg[@"page"] unsignedIntValue];
    uint32_t usage = [msg[@"usage"] unsignedIntValue];
    if (msg[@"down"] != nil) {
        vp_hid_key(page, usage, [msg[@"down"] boolValue]);
    } else {
        vp_hid_press(page, usage);
    }
    return vp_make_response(@"ok", reqId);
}
else if ([type isEqualToString:@"touch"]) {
    // Process touch coordinates
    return vp_make_response(@"ok", reqId);
}

The helper vp_make_response constructs standardized response dictionaries containing the echoed "id" and a status string.

Binary Data Transfer Flow

For operations requiring raw binary data (file transfers, clipboard images, or binary updates), the protocol extends the JSON header with a "size" field indicating the subsequent byte count.

Host-Side Binary Push

When pushing an update, the host writes the JSON header followed immediately by raw bytes:

// VPhoneControl.swift – binary update transmission
private func pushUpdate(fd: Int32) {
    guard let data = guestBinaryData else { return }
    nextRequestId += 1
    let header: [String: Any] = [
        "v": Self.protocolVersion,
        "t": "update",
        "id": String(nextRequestId, radix: 16),
        "size": data.count
    ]
    writeMessage(fd: fd, dict: header)
    // Stream raw binary after the JSON header
    data.withUnsafeBytes { buf in
        Self.writeFully(fd: fd, buf: buf.baseAddress!, count: data.count)
    }
}

Guest-Side Binary Reception

The guest reads the size from the JSON header, then uses vp_read_fully to consume the exact byte count:

// vphoned.m – receive_update (lines 42-73)
static BOOL receive_update(int fd, NSUInteger size) {
    // Create temporary file for atomic replacement
    while (remaining > 0) {
        size_t chunk = remaining < sizeof(buf) ? remaining : sizeof(buf);
        if (!vp_read_fully(fd, buf, chunk)) {
            // Handle read error
            return NO;
        }
        write(tmp_fd, buf, chunk);
        remaining -= chunk;
    }
    // chmod +x, rename to CACHE_PATH, and restart daemon
}

Error Handling and Request Timeouts

Both endpoints implement robust error reporting using an "err" message type containing a descriptive "msg" field. The host tracks pending requests in a dictionary keyed by the hex "id", using armRequestTimeout to fail requests that exceed the timeout threshold without receiving a response.

On the guest side, vp_read_message and vp_write_message return Boolean status codes that trigger connection cleanup and thread termination when socket errors occur, ensuring the daemon remains available for new host connections.

Summary

  • Transport: The guest daemon opens an AF_VSOCK listener on port 1337, while the host connects via VZVirtioSocketDevice to establish a raw byte stream.
  • Framing: All messages use 4-byte big-endian length prefixes followed by UTF-8 JSON payloads, implemented in vphoned_protocol.h and VPhoneControl.swift.
  • Structure: Every JSON message includes "v" (version), "t" (type), and optional "id" (correlation ID) fields.
  • Handshake: The hello exchange negotiates capabilities and triggers automatic binary updates via SHA-256 hash comparison.
  • Binary transfers: Commands include a "size" field after which raw bytes stream directly over the socket without additional framing.
  • Reliability: Request timeouts, error message types, and full-read helper functions (vp_read_fully, writeFully) ensure data integrity across the virtualized boundary.

Frequently Asked Questions

What port does vphoned use for VSock communication?

The guest daemon listens on port 1337 (VPHONED_PORT) over the AF_VSOCK address family. The host-side VPhoneControl.swift connects to this specific port to establish the control channel.

How does the length-prefixed framing prevent message corruption?

Each message begins with a 4-byte big-endian integer specifying the exact byte count of the following JSON payload. This allows the receiver to allocate precise buffers and use blocking reads (vp_read_fully or readFully) to consume exactly the intended data, preventing buffer overflows or partial message parsing errors common in stream-based protocols.

What message types does the vphoned protocol support?

The protocol supports types including "hello" (handshake), "hid" (keyboard/mouse input), "touch" (touchscreen events), "file_get" and "file_put" (file transfer), "update" (binary replacement), and "err" (error reporting). Each type expects specific additional fields in the JSON dictionary.

How does the host handle guest binary updates automatically?

During the handshake, the host sends a bin_hash field containing its local SHA-256 hash. The guest compares this against its running binary's hash. If they differ, the guest sets "need_update": true in the response, prompting the host to transmit a new binary using the "update" message type with a "size" field, streaming the raw executable bytes immediately after the JSON header.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →