# How the XPC Communication Layer Functions Between the Container CLI and Container-apiserver

> Discover how the XPC communication layer enables secure RPC between the container CLI and container-apiserver using a type-safe Swift wrapper for Mach services.

- Repository: [Apple/container](https://github.com/apple/container)
- Tags: internals
- Published: 2026-06-19

---

**The XPC communication layer in apple/container uses a type-safe Swift wrapper around macOS Mach services to enable asynchronous, authenticated RPC between the CLI and container-apiserver via the XPCMessage, XPCClient, and XPCServer types.**

The apple/container project implements a secure XPC (Cross-Process Communication) architecture to bridge the Container CLI and the container-apiserver daemon on macOS. This layer provides asynchronous request/response handling with built-in caller authentication, thread-safe message encoding, and a pluggable routing table for dispatching container operations.

## XPC Architecture Components

The communication stack isolates low-level XPC details behind Swift-native APIs defined in four primary files:

- **XPCMessage** ([`Sources/ContainerXPC/XPCMessage.swift`](https://github.com/apple/container/blob/main/Sources/ContainerXPC/XPCMessage.swift)) – A thin, thread-safe wrapper around `xpc_object_t` that encodes routes, payloads, and errors using `NSLock` for synchronization.
- **XPCClient** ([`Sources/ContainerXPC/XPCClient.swift`](https://github.com/apple/container/blob/main/Sources/ContainerXPC/XPCClient.swift)) – Manages Mach service connections, sends requests via `xpc_connection_send_message_with_reply`, and parses replies into typed messages.
- **XPCClientSession** ([`Sources/ContainerXPC/XPCClientSession.swift`](https://github.com/apple/container/blob/main/Sources/ContainerXPC/XPCClientSession.swift)) – Maintains persistent client sessions with automatic disconnect handlers for long-running CLI commands.
- **XPCServer** ([`Sources/ContainerXPC/XPCServer.swift`](https://github.com/apple/container/blob/main/Sources/ContainerXPC/XPCServer.swift)) – Listens on the Mach service endpoint, authenticates callers by audit token, dispatches to route handlers, and returns replies.

## Message Protocol and Format

All traffic traverses the wire as dictionary-valued XPC objects with two reserved keys defined in `XPCMessage`:

```swift
public static let routeKey = "com.apple.container.xpc.route"
public static let errorKey = "com.apple.container.xpc.error"

```

The **route** value is a string identifying the RPC endpoint (e.g., `"container.create"`), while the **error** key contains a JSON-encoded `ContainerXPCError` when the server encounters failures. `XPCMessage` provides typed helpers for extracting and setting values (String, Bool, UInt64, Date, FileHandle) while holding a lock on the underlying `xpc_object_t` to guarantee thread safety.

## Client-Side Flow

### Creating Connections and Sessions

The CLI initiates communication by constructing an `XPCClient` with the service identifier that matches the server’s Mach service:

```swift
let client = XPCClient(service: "com.apple.container.apiserver")

```

For long-running commands, the CLI opens a persistent session through `XPCClientSession`, which registers a disconnect handler to invoke callbacks if the server terminates unexpectedly:

```swift
let session = client.openSession()
session.onDisconnect {
    print("Server disconnected")
}

```

### Building and Sending Requests

Requests are constructed as `XPCMessage` instances with a route and payload:

```swift
var request = XPCMessage(route: "container.create")
request.set(key: "name", value: "my-container")
request.set(key: "image", value: "ubuntu:latest")

```

The client transmits the message using `xpc_connection_send_message_with_reply` under the hood, returning an `XPCMessage` representing the server’s response:

```swift
let reply = try await client.send(request, responseTimeout: .seconds(30))

```

Errors are propagated as `ContainerizationError` if the server sets the error key or if the underlying XPC connection fails.

## Server-Side Flow

### Starting the Listener

The `container-apiserver` instantiates `XPCServer` with its identifier and a routing table mapping route strings to async handlers:

```swift
let server = XPCServer(
    identifier: "com.apple.container.apiserver",
    routes: [
        "container.create": XPCServer.route(handleCreate)
    ],
    log: logger
)
try await server.listen()

```

The `listen()` method creates an `AsyncStream` of incoming `xpc_connection_t` objects, spawning tasks to handle each connection via `handleClientConnection`.

### Authenticating Callers

Before dispatching any request, the server extracts the audit token from the incoming dictionary using `xpc_dictionary_get_audit_token` and verifies that the client’s effective UID matches the server’s UID. Mismatches result in an immediate `ContainerizationError(.invalidState)` reply, preventing unauthorized cross-user access.

### Route Dispatch and Error Handling

The server extracts the route string using `XPCMessage.routeKey` and looks up the corresponding handler in its `routes` dictionary. If found, it wraps the raw XPC object in an `XPCMessage` and invokes:

```swift
let response = try await handler(message, session)

```

The server sends the reply via `xpc_connection_send_message`. Thrown `ContainerizationError` instances are caught and converted into standard error replies via `replyWithError`, while unexpected errors are wrapped in `ContainerizationError(.unknown)` before transmission.

## Practical Code Examples

### Creating a Container from the CLI

```swift
import ContainerXPC

let client = XPCClient(service: "com.apple.container.apiserver")
let request = XPCMessage(route: "container.create")
request.set(key: "name", value: "demo")
request.set(key: "image", value: "alpine:latest")

do {
    let reply = try await client.send(request, responseTimeout: .seconds(10))
    if let uuid = reply.string(key: "uuid") {
        print("Container created: \(uuid)")
    }
} catch {
    print("Failed: \(error)")
}

```

### Registering a Handler on the Server

```swift
import ContainerXPC
import Logging

func handleCreate(_ request: XPCMessage, _ session: XPCServerSession) async throws -> XPCMessage {
    guard let name = request.string(key: "name"),
          let image = request.string(key: "image") else {
        throw ContainerizationError(.invalidArgument, message: "missing name or image")
    }
    
    let containerID = UUID().uuidString
    var response = XPCMessage(route: request.string(key: XPCMessage.routeKey) ?? "")
    response.set(key: "uuid", value: containerID)
    return response
}

let logger = Logger(label: "container.apiserver")
let server = XPCServer(
    identifier: "com.apple.container.apiserver",
    routes: ["container.create": XPCServer.route(handleCreate)],
    log: logger
)
try await server.listen()

```

### Handling Disconnects

Client-side cleanup uses the session’s disconnect callback:

```swift
let session = client.openSession()
session.onDisconnect {
    print("Server went away – cleaning up local state")
}

```

## Summary

- The XPC layer uses **XPCMessage** as a type-safe wrapper around raw `xpc_object_t` dictionaries with reserved keys for routing and errors.
- **XPCClient** and **XPCClientSession** in [`Sources/ContainerXPC/XPCClient.swift`](https://github.com/apple/container/blob/main/Sources/ContainerXPC/XPCClient.swift) manage Mach service connections, timeouts, and disconnect handling for the CLI.
- **XPCServer** in [`Sources/ContainerXPC/XPCServer.swift`](https://github.com/apple/container/blob/main/Sources/ContainerXPC/XPCServer.swift) authenticates callers via `xpc_dictionary_get_audit_token` and UID verification before dispatching to pluggable route handlers.
- Communication is asynchronous, using `xpc_connection_send_message_with_reply` and `AsyncStream` to handle concurrent connections with automatic error propagation as `ContainerizationError`.

## Frequently Asked Questions

### How does the XPC layer authenticate clients?

The server extracts the audit token from each incoming XPC dictionary using `xpc_dictionary_get_audit_token` and validates that the client’s effective UID matches the server’s UID. If the UIDs differ, the server immediately returns a `ContainerizationError(.invalidState)` without executing the requested route.

### What happens if the container-apiserver disconnects during a CLI operation?

The **XPCClientSession** type registers a disconnect handler with the underlying XPC connection. When the server closes the connection or crashes, the handler fires, invoking any callbacks registered via `session.onDisconnect`, allowing the CLI to clean up local state and fail gracefully.

### Can the CLI use short-lived connections instead of persistent sessions?

Yes. While the CLI typically uses `XPCClientSession` for long-running commands, you can send one-off requests directly through `XPCClient.send(request:)` without calling `openSession()`. This creates a transient connection that closes automatically after the reply or timeout.

### How are server-side errors transmitted back to the CLI?

When a handler throws a `ContainerizationError`, the server catches it in `XPCServer` and encodes it into the reply dictionary under the `com.apple.container.xpc.error` key as a JSON-encoded `ContainerXPCError`. The client-side `XPCClient` checks for this key when parsing the reply and re-throws the error as a Swift exception.