//===----------------------------------------------------------------------===// // Copyright © 2026 Apple Inc. and the container project authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //===----------------------------------------------------------------------===// import ContainerResource import ContainerXPC import ContainerizationError import ContainerizationExtras import Foundation /// A client for interacting with a single network. public struct NetworkClient: Sendable { static let label = "com.apple.container.network" public static func machServiceLabel(id: String, plugin: String) -> String { "\(Self.label).\(plugin).\(id)" } private var machServiceLabel: String { Self.machServiceLabel(id: id, plugin: plugin) } let id: String let plugin: String /// Create a client for a network. public init(id: String, plugin: String) { self.id = id self.plugin = plugin } } // Runtime Methods extension NetworkClient { /// Open a persistent connection to the network helper. /// /// The returned session should be reused for `allocate(on:)` calls. The /// network helper automatically releases all allocations made over this /// session when it closes. public func connect() -> XPCClientSession { createClient().openSession() } public func status() async throws -> NetworkStatus { let request = XPCMessage(route: NetworkRoutes.status.rawValue) let client = createClient() let response = try await client.send(request) let status = try response.status() return status } /// Allocate a network attachment over an existing session. /// /// Use `connect()` to obtain a session, then pass it here. The session /// must remain open for the lifetime of the allocation; closing it /// releases the allocation on the network helper automatically. public func allocate( hostname: String, macAddress: MACAddress? = nil, on session: XPCClientSession ) async throws -> (attachment: Attachment, additionalData: XPCMessage?) { let request = XPCMessage(route: NetworkRoutes.allocate.rawValue) request.set(key: NetworkKeys.hostname.rawValue, value: hostname) if let macAddress = macAddress { request.set(key: NetworkKeys.macAddress.rawValue, value: macAddress.description) } let response = try await session.send(request) let attachment = try response.attachment() let additionalData = response.additionalData() return (attachment, additionalData) } public func lookup(hostname: String) async throws -> Attachment? { let request = XPCMessage(route: NetworkRoutes.lookup.rawValue) request.set(key: NetworkKeys.hostname.rawValue, value: hostname) let client = createClient() let response = try await client.send(request) return try response.dataNoCopy(key: NetworkKeys.attachment.rawValue).map { try JSONDecoder().decode(Attachment.self, from: $0) } } private func createClient() -> XPCClient { XPCClient(service: machServiceLabel) } } extension XPCMessage { public func additionalData() -> XPCMessage? { guard let additionalData = xpc_dictionary_get_dictionary(self.underlying, NetworkKeys.additionalData.rawValue) else { return nil } return XPCMessage(object: additionalData) } public func attachment() throws -> Attachment { let data = self.dataNoCopy(key: NetworkKeys.attachment.rawValue) guard let data else { throw ContainerizationError(.invalidArgument, message: "no network attachment snapshot data in message") } return try JSONDecoder().decode(Attachment.self, from: data) } public func hostname() throws -> String { let hostname = self.string(key: NetworkKeys.hostname.rawValue) guard let hostname else { throw ContainerizationError(.invalidArgument, message: "no hostname data in message") } return hostname } public func status() throws -> NetworkStatus { let data = self.dataNoCopy(key: NetworkKeys.status.rawValue) guard let data else { throw ContainerizationError(.invalidArgument, message: "no network snapshot data in message") } return try JSONDecoder().decode(NetworkStatus.self, from: data) } }