How the XPC Communication Layer Functions Between the Container CLI and Container-apiserver
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) – A thin, thread-safe wrapper aroundxpc_object_tthat encodes routes, payloads, and errors usingNSLockfor synchronization. - XPCClient (
Sources/ContainerXPC/XPCClient.swift) – Manages Mach service connections, sends requests viaxpc_connection_send_message_with_reply, and parses replies into typed messages. - XPCClientSession (
Sources/ContainerXPC/XPCClientSession.swift) – Maintains persistent client sessions with automatic disconnect handlers for long-running CLI commands. - XPCServer (
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:
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:
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:
let session = client.openSession()
session.onDisconnect {
print("Server disconnected")
}
Building and Sending Requests
Requests are constructed as XPCMessage instances with a route and payload:
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:
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:
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:
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
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
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:
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_tdictionaries with reserved keys for routing and errors. - XPCClient and XPCClientSession in
Sources/ContainerXPC/XPCClient.swiftmanage Mach service connections, timeouts, and disconnect handling for the CLI. - XPCServer in
Sources/ContainerXPC/XPCServer.swiftauthenticates callers viaxpc_dictionary_get_audit_tokenand UID verification before dispatching to pluggable route handlers. - Communication is asynchronous, using
xpc_connection_send_message_with_replyandAsyncStreamto handle concurrent connections with automatic error propagation asContainerizationError.
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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →