Reorganize Swift package targets for network plugin. (#1615)

- Part of #1404.
- Updates containerization to 0.33.2.
- Reorganizes network plugin targets into:
  - `ContainerNetworkClient` - network plugin client and default types
- `ContainerNetworkServer` - separate protocols for `Network` which
manages the underlying virtual network, `NetworkService`, which takes a
network and implements the API, and an actor `NetworkHarness` that
marshals between the API and the XPC protocol. The service-harness
separation will help us ensure XPC protocol compatibility in both
directions as we evolve the plugin APIs.
- Removes `disableAllocator()` which is no longer used since #1545
switched over to using XPC connections between runtime and network
plugin instances to track whether a network has attached containers.
This commit is contained in:
J Logan
2026-05-28 15:26:31 -07:00
committed by GitHub
parent 445c90f927
commit c5a8d7a802
18 changed files with 195 additions and 176 deletions
-1
View File
@@ -18,7 +18,6 @@ import ArgumentParser
import ContainerAPIClient
import ContainerAPIService
import ContainerLog
import ContainerNetworkService
import ContainerPersistence
import ContainerPlugin
import ContainerResource
@@ -16,8 +16,9 @@
import ArgumentParser
import ContainerLog
import ContainerNetworkService
import ContainerNetworkServiceClient
import ContainerNetworkClient
import ContainerNetworkServer
import ContainerNetworkVmnetServer
import ContainerPlugin
import ContainerResource
import ContainerXPC
@@ -99,14 +100,14 @@ extension NetworkVmnetHelper {
log: log
)
try await network.start()
let server = try await NetworkService(network: network, log: log)
let service = try await DefaultNetworkService(network: network, log: log)
let harness = NetworkHarness(service: service)
let xpc = XPCServer(
identifier: serviceIdentifier,
routes: [
NetworkRoutes.state.rawValue: XPCServer.route(server.state),
NetworkRoutes.allocate.rawValue: server.allocate,
NetworkRoutes.lookup.rawValue: XPCServer.route(server.lookup),
NetworkRoutes.disableAllocator.rawValue: XPCServer.route(server.disableAllocator),
NetworkRoutes.state.rawValue: XPCServer.route(harness.state),
NetworkRoutes.allocate.rawValue: harness.allocate,
NetworkRoutes.lookup.rawValue: XPCServer.route(harness.lookup),
],
log: log
)
@@ -15,7 +15,7 @@
//===----------------------------------------------------------------------===//
import ContainerAPIClient
import ContainerNetworkServiceClient
import ContainerNetworkClient
import ContainerPersistence
import ContainerPlugin
import ContainerResource
@@ -30,7 +30,7 @@ import SystemPackage
public actor NetworksService {
struct NetworkServiceState {
var networkState: NetworkState
var client: ContainerNetworkServiceClient.NetworkClient
var client: ContainerNetworkClient.NetworkClient
}
private let pluginLoader: PluginLoader
@@ -389,7 +389,7 @@ public actor NetworksService {
return pluginInfo
}
private static func getClient(configuration: NetworkConfiguration) throws -> ContainerNetworkServiceClient.NetworkClient {
private static func getClient(configuration: NetworkConfiguration) throws -> ContainerNetworkClient.NetworkClient {
guard let pluginInfo = configuration.pluginInfo else {
throw ContainerizationError(.internalError, message: "network \(configuration.id) missing plugin information")
}
@@ -113,15 +113,6 @@ extension NetworkClient {
}
}
public func disableAllocator() async throws -> Bool {
let request = XPCMessage(route: NetworkRoutes.disableAllocator.rawValue)
let client = createClient()
let response = try await client.send(request)
return try response.allocatorDisabled()
}
private func createClient() -> XPCClient {
XPCClient(service: machServiceLabel)
}
@@ -135,10 +126,6 @@ extension XPCMessage {
return XPCMessage(object: additionalData)
}
public func allocatorDisabled() throws -> Bool {
self.bool(key: NetworkKeys.allocatorDisabled.rawValue)
}
public func attachment() throws -> Attachment {
let data = self.dataNoCopy(key: NetworkKeys.attachment.rawValue)
guard let data else {
@@ -16,7 +16,6 @@
public enum NetworkKeys: String {
case additionalData
case allocatorDisabled
case attachment
case hostname
case macAddress
@@ -19,8 +19,6 @@ public enum NetworkRoutes: String {
case state = "com.apple.container.network/state"
/// Allocates parameters for attaching a sandbox to the network.
case allocate = "com.apple.container.network/allocate"
/// Disables the allocator if no sandboxes are attached.
case disableAllocator = "com.apple.container.network/disableAllocator"
/// Retrieves the allocation for a hostname.
case lookup = "com.apple.container.network/lookup"
}
@@ -52,11 +52,6 @@ actor AttachmentAllocator {
return index
}
/// If no addresses are allocated, prevent future allocations and return true.
func disableAllocator() async -> Bool {
allocator.disableAllocator()
}
/// Retrieve the allocator index for a hostname.
func lookup(hostname: String) async throws -> UInt32? {
hostnames[hostname]
@@ -14,15 +14,13 @@
// limitations under the License.
//===----------------------------------------------------------------------===//
import ContainerNetworkServiceClient
import ContainerResource
import ContainerXPC
import ContainerizationError
import ContainerizationExtras
import Foundation
import Logging
public actor NetworkService: Sendable {
public actor DefaultNetworkService: NetworkService {
private let network: any Network
private let log: Logger
private var allocator: AttachmentAllocator
@@ -50,15 +48,16 @@ public actor NetworkService: Sendable {
}
@Sendable
public func state(_ message: XPCMessage) async throws -> XPCMessage {
let reply = message.reply()
let state = await network.state
try reply.setState(state)
return reply
public func state() async throws -> NetworkState {
await network.state
}
@Sendable
public func allocate(_ message: XPCMessage, _ session: XPCServerSession) async throws -> XPCMessage {
public func allocate(
hostname: String,
macAddress: MACAddress?,
session: XPCServerSession
) async throws -> (attachment: Attachment, additionalData: XPCMessage?) {
log.debug("enter", metadata: ["func": "\(#function)"])
defer { log.debug("exit", metadata: ["func": "\(#function)"]) }
@@ -67,11 +66,7 @@ public actor NetworkService: Sendable {
throw ContainerizationError(.invalidState, message: "invalid network state - network \(state.id) must be running")
}
let hostname = try message.hostname()
let macAddress =
try message.string(key: NetworkKeys.macAddress.rawValue)
.map { try MACAddress($0) }
?? MACAddress((UInt64.random(in: 0...UInt64.max) & 0x0cff_ffff_ffff) | 0xf200_0000_0000)
let macAddress = macAddress ?? MACAddress((UInt64.random(in: 0...UInt64.max) & 0x0cff_ffff_ffff) | 0xf200_0000_0000)
let index = try await allocator.allocate(hostname: hostname)
let ipv6Address = try status.ipv6Subnet
.map { try CIDRv6(macAddress.ipv6Address(network: $0.lower), prefix: $0.prefix) }
@@ -93,12 +88,10 @@ public actor NetworkService: Sendable {
"ipv6Address": "\(attachment.ipv6Address?.description ?? "unavailable")",
"macAddress": "\(attachment.macAddress?.description ?? "unspecified")",
])
let reply = message.reply()
try reply.setAttachment(attachment)
var additionalData: XPCMessage?
try network.withAdditionalData {
if let additionalData = $0 {
try reply.setAdditionalData(additionalData.underlying)
}
additionalData = $0
}
macAddresses[index] = macAddress
@@ -110,7 +103,7 @@ public actor NetworkService: Sendable {
}
allocationsBySession[session]!.append((hostname: hostname, index: index))
return reply
return (attachment: attachment, additionalData: additionalData)
}
private func releaseSession(_ session: XPCServerSession) async {
@@ -125,7 +118,7 @@ public actor NetworkService: Sendable {
}
@Sendable
public func lookup(_ message: XPCMessage) async throws -> XPCMessage {
public func lookup(hostname: String) async throws -> Attachment? {
log.debug("enter", metadata: ["func": "\(#function)"])
defer { log.debug("exit", metadata: ["func": "\(#function)"]) }
@@ -134,15 +127,16 @@ public actor NetworkService: Sendable {
throw ContainerizationError(.invalidState, message: "invalid network state - network \(state.id) must be running")
}
let hostname = try message.hostname()
// Invariant: hostname -> index if and only if index -> MAC address
let index = try await allocator.lookup(hostname: hostname)
let reply = message.reply()
guard let index else {
return reply
return nil
}
guard let macAddress = macAddresses[index] else {
return reply
return nil
}
// populate attachment
let address = IPv4Address(index)
let subnet = status.ipv4Subnet
let ipv4Address = try CIDRv4(address, prefix: subnet.prefix)
@@ -162,39 +156,7 @@ public actor NetworkService: Sendable {
"hostname": "\(hostname)",
"address": "\(address)",
])
try reply.setAttachment(attachment)
return reply
}
@Sendable
public func disableAllocator(_ message: XPCMessage) async throws -> XPCMessage {
log.debug("enter", metadata: ["func": "\(#function)"])
defer { log.debug("exit", metadata: ["func": "\(#function)"]) }
let success = await allocator.disableAllocator()
log.info("attempted allocator disable", metadata: ["success": "\(success)"])
let reply = message.reply()
reply.setAllocatorDisabled(success)
return reply
}
}
extension XPCMessage {
fileprivate func setAdditionalData(_ additionalData: xpc_object_t) throws {
xpc_dictionary_set_value(self.underlying, NetworkKeys.additionalData.rawValue, additionalData)
}
fileprivate func setAllocatorDisabled(_ allocatorDisabled: Bool) {
self.set(key: NetworkKeys.allocatorDisabled.rawValue, value: allocatorDisabled)
}
fileprivate func setAttachment(_ attachment: Attachment) throws {
let data = try JSONEncoder().encode(attachment)
self.set(key: NetworkKeys.attachment.rawValue, value: data)
}
fileprivate func setState(_ state: NetworkState) throws {
let data = try JSONEncoder().encode(state)
self.set(key: NetworkKeys.state.rawValue, value: data)
return attachment
}
}
@@ -0,0 +1,87 @@
//===----------------------------------------------------------------------===//
// 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 ContainerNetworkClient
import ContainerResource
import ContainerXPC
import ContainerizationExtras
import Foundation
public actor NetworkHarness: Sendable {
private let service: NetworkService
public init(service: NetworkService) {
self.service = service
}
@Sendable
public func state(_ message: XPCMessage) async throws -> XPCMessage {
let reply = message.reply()
let state = try await service.state()
try reply.setState(state)
return reply
}
@Sendable
public func allocate(_ message: XPCMessage, _ session: XPCServerSession) async throws -> XPCMessage {
let hostname = try message.hostname()
let macAddress =
try message.string(key: NetworkKeys.macAddress.rawValue)
.map { try MACAddress($0) }
let (attachment:attachment, additionalData:additionalData) = try await service.allocate(
hostname: hostname,
macAddress: macAddress,
session: session
)
let reply = message.reply()
try reply.setAttachment(attachment)
if let additionalData {
try reply.setAdditionalData(additionalData.underlying)
}
return reply
}
@Sendable
public func lookup(_ message: XPCMessage) async throws -> XPCMessage {
let hostname = try message.hostname()
let reply = message.reply()
guard let attachment = try await service.lookup(hostname: hostname) else {
return reply
}
try reply.setAttachment(attachment)
return reply
}
}
extension XPCMessage {
fileprivate func setAdditionalData(_ additionalData: xpc_object_t) throws {
xpc_dictionary_set_value(self.underlying, NetworkKeys.additionalData.rawValue, additionalData)
}
fileprivate func setAttachment(_ attachment: Attachment) throws {
let data = try JSONEncoder().encode(attachment)
self.set(key: NetworkKeys.attachment.rawValue, value: data)
}
fileprivate func setState(_ state: NetworkState) throws {
let data = try JSONEncoder().encode(state)
self.set(key: NetworkKeys.state.rawValue, value: data)
}
}
@@ -0,0 +1,35 @@
//===----------------------------------------------------------------------===//
// 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 ContainerizationExtras
/// A network service
public protocol NetworkService: Sendable {
/// Gets the properties of the realized network.
func state() async throws -> NetworkState
/// Register a hostname and allocate associated addresses.
func allocate(
hostname: String,
macAddress: MACAddress?,
session: XPCServerSession
) async throws -> (attachment: Attachment, additionalData: XPCMessage?)
/// Return the attachment for a hostname if it is registered with the network.
func lookup(hostname: String) async throws -> Attachment?
}
@@ -14,11 +14,11 @@
// limitations under the License.
//===----------------------------------------------------------------------===//
import ContainerNetworkServer
import ContainerResource
import ContainerXPC
import ContainerizationError
import ContainerizationExtras
import Foundation
import Logging
public actor AllocationOnlyVmnetNetwork: Network {
@@ -14,22 +14,20 @@
// limitations under the License.
//===----------------------------------------------------------------------===//
import ContainerNetworkServer
import ContainerResource
import ContainerXPC
import Containerization
import ContainerizationError
import ContainerizationExtras
import Dispatch
import Foundation
import Logging
import Synchronization
import SystemConfiguration
import XPC
import vmnet
/// Creates a vmnet network with reservation APIs.
@available(macOS 26, *)
public final class ReservedVmnetNetwork: Network {
public final class ReservedVmnetNetwork: ContainerNetworkServer.Network {
private struct State {
var networkState: NetworkState
var network: vmnet_network_ref?
@@ -14,7 +14,7 @@
// limitations under the License.
//===----------------------------------------------------------------------===//
import ContainerNetworkServiceClient
import ContainerNetworkClient
import ContainerOS
import ContainerPersistence
import ContainerResource
@@ -177,7 +177,7 @@ public actor RuntimeService {
do {
for (index, info) in networkBootstrapInfos.enumerated() {
let attachmentConfig = config.networks[index]
let client = ContainerNetworkServiceClient.NetworkClient(id: attachmentConfig.network, plugin: info.pluginInfo.plugin)
let client = ContainerNetworkClient.NetworkClient(id: attachmentConfig.network, plugin: info.pluginInfo.plugin)
let session = client.connect()
sessions.append(session)
var (attachment, additionalData) = try await client.allocate(