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
+3 -3
View File
@@ -1,5 +1,5 @@
{
"originHash" : "d9d032f8b1ad94cf9006bd4b441ce1dad52802b04b1d7f7b30baedd32cee0e8b",
"originHash" : "f3e6e0a1ee627e1f396b1565e72a50b179394e7011667ec4569dc53455ee06ed",
"pins" : [
{
"identity" : "async-http-client",
@@ -15,8 +15,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/containerization.git",
"state" : {
"revision" : "9205a766302a18e06a0ce43f8b7e483625e3e50f",
"version" : "0.33.1"
"revision" : "2550dd49f1890702f6fe0171212050bbce9d3825",
"version" : "0.33.2"
}
},
{
+30 -24
View File
@@ -23,7 +23,7 @@ import PackageDescription
let releaseVersion = ProcessInfo.processInfo.environment["RELEASE_VERSION"] ?? "0.0.0"
let gitCommit = ProcessInfo.processInfo.environment["GIT_COMMIT"] ?? "unspecified"
let builderShimVersion = "0.12.0"
let scVersion = "0.33.1"
let scVersion = "0.33.2"
let package = Package(
name: "container",
@@ -34,7 +34,9 @@ let package = Package(
.library(name: "ContainerAPIService", targets: ["ContainerAPIService"]),
.library(name: "ContainerAPIClient", targets: ["ContainerAPIClient"]),
.library(name: "ContainerImagesService", targets: ["ContainerImagesService", "ContainerImagesServiceClient"]),
.library(name: "ContainerNetworkService", targets: ["ContainerNetworkService", "ContainerNetworkServiceClient"]),
.library(name: "ContainerNetworkClient", targets: ["ContainerNetworkClient"]),
.library(name: "ContainerNetworkServer", targets: ["ContainerNetworkServer"]),
.library(name: "ContainerNetworkVmnetServer", targets: ["ContainerNetworkVmnetServer"]),
.library(name: "ContainerResource", targets: ["ContainerResource"]),
.library(name: "ContainerLog", targets: ["ContainerLog"]),
.library(name: "ContainerPersistence", targets: ["ContainerPersistence"]),
@@ -106,7 +108,6 @@ let package = Package(
"ContainerBuild",
"ContainerAPIClient",
"ContainerLog",
"ContainerNetworkService",
"ContainerPersistence",
"ContainerPlugin",
"ContainerResource",
@@ -166,7 +167,7 @@ let package = Package(
"ContainerAPIService",
"ContainerAPIClient",
"ContainerLog",
"ContainerNetworkService",
"ContainerNetworkClient",
"ContainerPersistence",
"ContainerPlugin",
"ContainerResource",
@@ -188,7 +189,7 @@ let package = Package(
.product(name: "SystemPackage", package: "swift-system"),
"CVersion",
"ContainerAPIClient",
"ContainerNetworkServiceClient",
"ContainerNetworkClient",
"ContainerPersistence",
"ContainerPlugin",
"ContainerResource",
@@ -291,13 +292,12 @@ let package = Package(
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Logging", package: "swift-log"),
.product(name: "Containerization", package: "containerization"),
.product(name: "ContainerizationExtras", package: "containerization"),
.product(name: "ContainerizationIO", package: "containerization"),
.product(name: "ContainerizationOS", package: "containerization"),
"ContainerLog",
"ContainerNetworkService",
"ContainerNetworkServiceClient",
"ContainerNetworkClient",
"ContainerNetworkServer",
"ContainerNetworkVmnetServer",
"ContainerPersistence",
"ContainerPlugin",
"ContainerResource",
@@ -308,36 +308,42 @@ let package = Package(
exclude: ["config.toml"]
),
.target(
name: "ContainerNetworkService",
name: "ContainerNetworkClient",
dependencies: [
.product(name: "Logging", package: "swift-log"),
.product(name: "Containerization", package: "containerization"),
.product(name: "ContainerizationOS", package: "containerization"),
"ContainerNetworkServiceClient",
"ContainerPersistence",
.product(name: "ContainerizationExtras", package: "containerization"),
"ContainerResource",
"ContainerXPC",
],
path: "Sources/Services/ContainerNetworkService/Server"
path: "Sources/Services/Network/Client"
),
.target(
name: "ContainerNetworkServer",
dependencies: [
.product(name: "Logging", package: "swift-log"),
.product(name: "ContainerizationExtras", package: "containerization"),
"ContainerNetworkClient",
"ContainerResource",
"ContainerXPC",
],
path: "Sources/Services/Network/Server"
),
.testTarget(
name: "ContainerNetworkServiceTests",
name: "ContainerNetworkServerTests",
dependencies: [
.product(name: "Containerization", package: "containerization"),
.product(name: "ContainerizationExtras", package: "containerization"),
"ContainerNetworkService",
"ContainerNetworkServer",
]
),
.target(
name: "ContainerNetworkServiceClient",
name: "ContainerNetworkVmnetServer",
dependencies: [
.product(name: "Logging", package: "swift-log"),
.product(name: "Containerization", package: "containerization"),
"ContainerLog",
.product(name: "ContainerizationExtras", package: "containerization"),
"ContainerNetworkServer",
"ContainerResource",
"ContainerXPC",
],
path: "Sources/Services/ContainerNetworkService/Client"
path: "Sources/Services/NetworkVmnet/Server"
),
.target(
name: "ContainerRuntimeLinuxClient",
@@ -371,7 +377,7 @@ let package = Package(
.product(name: "ContainerizationOS", package: "containerization"),
.product(name: "ArgumentParser", package: "swift-argument-parser"),
"ContainerAPIClient",
"ContainerNetworkServiceClient",
"ContainerNetworkClient",
"ContainerOS",
"ContainerPersistence",
"ContainerResource",
-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(
@@ -14,11 +14,9 @@
// limitations under the License.
//===----------------------------------------------------------------------===//
import ContainerizationError
import ContainerizationExtras
import Testing
@testable import ContainerNetworkService
@testable import ContainerNetworkServer
struct AttachmentAllocatorTest {
@Test func testAllocateSingleHostname() async throws {
@@ -145,53 +143,6 @@ struct AttachmentAllocatorTest {
#expect(finalAddress4 == newAddress)
}
@Test func testDisableAllocatorWhenEmpty() async throws {
let allocator = try AttachmentAllocator(lower: 100, size: 10)
let disabled = await allocator.disableAllocator()
#expect(disabled == true)
// After disabling, allocation should fail
await #expect(throws: Error.self) {
try await allocator.allocate(hostname: "test-host")
}
}
@Test func testDisableAllocatorWhenNotEmpty() async throws {
let allocator = try AttachmentAllocator(lower: 100, size: 10)
_ = try await allocator.allocate(hostname: "test-host")
let disabled = await allocator.disableAllocator()
#expect(disabled == false)
// Since disable failed, should still be able to allocate
let address = try await allocator.allocate(hostname: "another-host")
#expect(address >= 100)
#expect(address < 110)
}
@Test func testDisableAfterDeallocatingAll() async throws {
let allocator = try AttachmentAllocator(lower: 100, size: 10)
_ = try await allocator.allocate(hostname: "host1")
_ = try await allocator.allocate(hostname: "host2")
try await allocator.deallocate(hostname: "host1")
try await allocator.deallocate(hostname: "host2")
let disabled = await allocator.disableAllocator()
#expect(disabled == true)
// After disabling, allocation should fail
await #expect(throws: Error.self) {
try await allocator.allocate(hostname: "test-host")
}
}
@Test func testMultipleDeallocationsOfSameHostname() async throws {
let allocator = try AttachmentAllocator(lower: 100, size: 10)
+3 -2
View File
@@ -21,8 +21,9 @@ opts+=("--target" "ContainerAPIClient")
opts+=("--target" "ContainerRuntimeClient")
opts+=("--target" "ContainerRuntimeLinuxClient")
opts+=("--target" "ContainerRuntimeLinuxServer")
opts+=("--target" "ContainerNetworkService")
opts+=("--target" "ContainerNetworkServiceClient")
opts+=("--target" "ContainerNetworkClient")
opts+=("--target" "ContainerNetworkServer")
opts+=("--target" "ContainerNetworkVmnetServer")
opts+=("--target" "ContainerImagesService")
opts+=("--target" "ContainerImagesServiceClient")
opts+=("--target" "ContainerResource")