diff --git a/Sources/Containerization/ContainerManager.swift b/Sources/Containerization/ContainerManager.swift index bbdbd8c0..8ddb04f9 100644 --- a/Sources/Containerization/ContainerManager.swift +++ b/Sources/Containerization/ContainerManager.swift @@ -24,7 +24,6 @@ import Foundation import ContainerizationExtras import SystemPackage import Virtualization -import vmnet /// A manager for creating and running containers. /// Supports container networking options. @@ -37,184 +36,6 @@ public struct ContainerManager: Sendable { self.imageStore.path.appendingPathComponent("containers") } - /// A network that can allocate and release interfaces for use with containers. - public protocol Network: Sendable { - mutating func create(_ id: String) throws -> Interface? - mutating func release(_ id: String) throws - } - - /// A network backed by vmnet on macOS. - @available(macOS 26.0, *) - public struct VmnetNetwork: Network { - private var allocator: Allocator - // `reference` isn't used concurrently. - nonisolated(unsafe) private let reference: vmnet_network_ref - - /// The IPv4 subnet of this network. - public let subnet: CIDRv4 - - /// The IPv4 gateway address of this network. - public var ipv4Gateway: IPv4Address { - subnet.gateway - } - - struct Allocator: Sendable { - private let addressAllocator: any AddressAllocator - private let cidr: CIDRv4 - private var allocations: [String: UInt32] - - init(cidr: CIDRv4) throws { - self.cidr = cidr - self.allocations = .init() - let size = Int(cidr.upper.value - cidr.lower.value - 3) - self.addressAllocator = try UInt32.rotatingAllocator( - lower: cidr.lower.value + 2, - size: UInt32(size) - ) - } - - mutating func allocate(_ id: String) throws -> CIDRv4 { - if allocations[id] != nil { - throw ContainerizationError(.exists, message: "allocation with id \(id) already exists") - } - let index = try addressAllocator.allocate() - allocations[id] = index - let ip = IPv4Address(index) - return try CIDRv4(ip, prefix: cidr.prefix) - } - - mutating func release(_ id: String) throws { - if let index = self.allocations[id] { - try addressAllocator.release(index) - allocations.removeValue(forKey: id) - } - } - } - - /// A network interface supporting the vmnet_network_ref. - public struct Interface: Containerization.Interface, VZInterface, Sendable { - public let ipv4Address: CIDRv4 - public let ipv4Gateway: IPv4Address? - public let macAddress: MACAddress? - public let mtu: UInt32 - - // `reference` isn't used concurrently. - nonisolated(unsafe) private let reference: vmnet_network_ref - - public init( - reference: vmnet_network_ref, - ipv4Address: CIDRv4, - ipv4Gateway: IPv4Address, - macAddress: MACAddress? = nil, - mtu: UInt32 = 1500 - ) { - self.ipv4Address = ipv4Address - self.ipv4Gateway = ipv4Gateway - self.macAddress = macAddress - self.mtu = mtu - self.reference = reference - } - - /// Returns the underlying `VZVirtioNetworkDeviceConfiguration`. - public func device() throws -> VZVirtioNetworkDeviceConfiguration { - let config = VZVirtioNetworkDeviceConfiguration() - if let macAddress = self.macAddress { - guard let mac = VZMACAddress(string: macAddress.description) else { - throw ContainerizationError(.invalidArgument, message: "invalid mac address \(macAddress)") - } - config.macAddress = mac - } - config.attachment = VZVmnetNetworkDeviceAttachment(network: self.reference) - return config - } - } - - /// Creates a new network. - /// - Parameter subnet: The subnet to use for this network. - public init(subnet: CIDRv4? = nil) throws { - var status: vmnet_return_t = .VMNET_FAILURE - guard let config = vmnet_network_configuration_create(.VMNET_SHARED_MODE, &status) else { - throw ContainerizationError(.unsupported, message: "failed to create vmnet config with status \(status)") - } - - vmnet_network_configuration_disable_dhcp(config) - - if let subnet { - try Self.configureSubnet(config, subnet: subnet) - } - - guard let ref = vmnet_network_create(config, &status), status == .VMNET_SUCCESS else { - throw ContainerizationError(.unsupported, message: "failed to create vmnet network with status \(status)") - } - - let cidr = try Self.getSubnet(ref) - - self.allocator = try .init(cidr: cidr) - self.subnet = cidr - self.reference = ref - } - - /// Returns a new interface for use with a container. - /// - Parameter id: The container ID. - public mutating func create(_ id: String) throws -> Containerization.Interface? { - let ipv4Address = try allocator.allocate(id) - return Self.Interface( - reference: self.reference, - ipv4Address: ipv4Address, - ipv4Gateway: self.ipv4Gateway, - ) - } - - /// Returns a new interface for use with a container with a custom MTU. - /// - Parameters: - /// - id: The container ID. - /// - mtu: The MTU for the interface. - public mutating func create(_ id: String, mtu: UInt32) throws -> Containerization.Interface? { - let ipv4Address = try allocator.allocate(id) - return Self.Interface( - reference: self.reference, - ipv4Address: ipv4Address, - ipv4Gateway: self.ipv4Gateway, - mtu: mtu - ) - } - - /// Performs cleanup of an interface. - /// - Parameter id: The container ID. - public mutating func release(_ id: String) throws { - try allocator.release(id) - } - - private static func getSubnet(_ ref: vmnet_network_ref) throws -> CIDRv4 { - var subnet = in_addr() - var mask = in_addr() - vmnet_network_get_ipv4_subnet(ref, &subnet, &mask) - - let sa = UInt32(bigEndian: subnet.s_addr) - let mv = UInt32(bigEndian: mask.s_addr) - - let lower = IPv4Address(sa & mv) - let upper = IPv4Address(lower.value + ~mv) - - return try CIDRv4(lower: lower, upper: upper) - } - - private static func configureSubnet(_ config: vmnet_network_configuration_ref, subnet: CIDRv4) throws { - let gateway = subnet.gateway - - var ga = in_addr() - inet_pton(AF_INET, gateway.description, &ga) - - let mask = IPv4Address(subnet.prefix.prefixMask32) - var ma = in_addr() - inet_pton(AF_INET, mask.description, &ma) - - guard vmnet_network_configuration_set_ipv4_subnet(config, &ga, &ma) == .VMNET_SUCCESS else { - throw ContainerizationError(.internalError, message: "failed to set subnet \(subnet) for network") - } - } - } - /// Create a new manager with the provided kernel, initfs mount, image store /// and optional network implementation. This will use a Virtualization.framework /// backed VMM implicitly. diff --git a/Sources/Containerization/Network.swift b/Sources/Containerization/Network.swift new file mode 100644 index 00000000..dfa0cfb7 --- /dev/null +++ b/Sources/Containerization/Network.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization 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. +//===----------------------------------------------------------------------===// + +/// A network that can allocate and release interfaces for use with containers. +public protocol Network: Sendable { + mutating func create(_ id: String) throws -> Interface? + mutating func release(_ id: String) throws +} diff --git a/Sources/Containerization/VmnetNetwork.swift b/Sources/Containerization/VmnetNetwork.swift new file mode 100644 index 00000000..de14931f --- /dev/null +++ b/Sources/Containerization/VmnetNetwork.swift @@ -0,0 +1,209 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization 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. +//===----------------------------------------------------------------------===// + +#if os(macOS) + +import ContainerizationError +import ContainerizationExtras +import Virtualization +import vmnet + +/// A network backed by vmnet on macOS. +@available(macOS 26.0, *) +public struct VmnetNetwork: Network { + private var allocator: Allocator + // `reference` isn't used concurrently. + nonisolated(unsafe) private let reference: vmnet_network_ref + + /// The IPv4 subnet of this network. + public let subnet: CIDRv4 + + /// The IPv4 gateway address of this network. + public var ipv4Gateway: IPv4Address { + subnet.gateway + } + + struct Allocator: Sendable { + private let addressAllocator: any AddressAllocator + private let cidr: CIDRv4 + private var allocations: [String: UInt32] + + init(cidr: CIDRv4) throws { + self.cidr = cidr + self.allocations = .init() + let size = Int(cidr.upper.value - cidr.lower.value - 3) + self.addressAllocator = try UInt32.rotatingAllocator( + lower: cidr.lower.value + 2, + size: UInt32(size) + ) + } + + mutating func allocate(_ id: String) throws -> CIDRv4 { + if allocations[id] != nil { + throw ContainerizationError(.exists, message: "allocation with id \(id) already exists") + } + let index = try addressAllocator.allocate() + allocations[id] = index + let ip = IPv4Address(index) + return try CIDRv4(ip, prefix: cidr.prefix) + } + + mutating func release(_ id: String) throws { + if let index = self.allocations[id] { + try addressAllocator.release(index) + allocations.removeValue(forKey: id) + } + } + } + + /// A network interface supporting the vmnet_network_ref. + public struct Interface: Containerization.Interface, VZInterface, Sendable { + public let ipv4Address: CIDRv4 + public let ipv4Gateway: IPv4Address? + public let macAddress: MACAddress? + public let mtu: UInt32 + + // `reference` isn't used concurrently. + nonisolated(unsafe) private let reference: vmnet_network_ref + + public init( + reference: vmnet_network_ref, + ipv4Address: CIDRv4, + ipv4Gateway: IPv4Address? = nil, + macAddress: MACAddress? = nil, + mtu: UInt32 = 1500 + ) { + self.ipv4Address = ipv4Address + self.ipv4Gateway = ipv4Gateway + self.macAddress = macAddress + self.mtu = mtu + self.reference = reference + } + + /// Returns the underlying `VZVirtioNetworkDeviceConfiguration`. + public func device() throws -> VZVirtioNetworkDeviceConfiguration { + let config = VZVirtioNetworkDeviceConfiguration() + if let macAddress = self.macAddress { + guard let mac = VZMACAddress(string: macAddress.description) else { + throw ContainerizationError(.invalidArgument, message: "invalid mac address \(macAddress)") + } + config.macAddress = mac + } + config.attachment = VZVmnetNetworkDeviceAttachment(network: self.reference) + return config + } + } + + /// Creates a new network. + /// - Parameters: + /// - mode: The vmnet operating mode. Defaults to `.VMNET_SHARED_MODE`. + /// - subnet: The subnet to use for this network. + public init(mode: vmnet.operating_modes_t = .VMNET_SHARED_MODE, subnet: CIDRv4? = nil) throws { + var status: vmnet_return_t = .VMNET_FAILURE + guard let config = vmnet_network_configuration_create(mode, &status) else { + throw ContainerizationError(.unsupported, message: "failed to create vmnet config with status \(status)") + } + + vmnet_network_configuration_disable_dhcp(config) + + if let subnet { + try Self.configureSubnet(config, subnet: subnet) + } + + guard let ref = vmnet_network_create(config, &status), status == .VMNET_SUCCESS else { + throw ContainerizationError(.unsupported, message: "failed to create vmnet network with status \(status)") + } + + let cidr = try Self.getSubnet(ref) + + self.allocator = try .init(cidr: cidr) + self.subnet = cidr + self.reference = ref + } + + /// Returns a new interface for use with a container. + /// - Parameter id: The container ID. + public mutating func create(_ id: String) throws -> Containerization.Interface? { + let ipv4Address = try allocator.allocate(id) + return Self.Interface( + reference: self.reference, + ipv4Address: ipv4Address, + ipv4Gateway: self.ipv4Gateway, + ) + } + + /// Returns a new interface without a default gateway route. + /// Use this for secondary interfaces where another interface already provides the default route. + /// - Parameter id: The container ID. + public mutating func createWithoutGateway(_ id: String) throws -> Containerization.Interface? { + let ipv4Address = try allocator.allocate(id) + return Self.Interface( + reference: self.reference, + ipv4Address: ipv4Address, + ) + } + + /// Returns a new interface for use with a container with a custom MTU. + /// - Parameters: + /// - id: The container ID. + /// - mtu: The MTU for the interface. + public mutating func create(_ id: String, mtu: UInt32) throws -> Containerization.Interface? { + let ipv4Address = try allocator.allocate(id) + return Self.Interface( + reference: self.reference, + ipv4Address: ipv4Address, + ipv4Gateway: self.ipv4Gateway, + mtu: mtu + ) + } + + /// Performs cleanup of an interface. + /// - Parameter id: The container ID. + public mutating func release(_ id: String) throws { + try allocator.release(id) + } + + private static func getSubnet(_ ref: vmnet_network_ref) throws -> CIDRv4 { + var subnet = in_addr() + var mask = in_addr() + vmnet_network_get_ipv4_subnet(ref, &subnet, &mask) + + let sa = UInt32(bigEndian: subnet.s_addr) + let mv = UInt32(bigEndian: mask.s_addr) + + let lower = IPv4Address(sa & mv) + let upper = IPv4Address(lower.value + ~mv) + + return try CIDRv4(lower: lower, upper: upper) + } + + private static func configureSubnet(_ config: vmnet_network_configuration_ref, subnet: CIDRv4) throws { + let gateway = subnet.gateway + + var ga = in_addr() + inet_pton(AF_INET, gateway.description, &ga) + + let mask = IPv4Address(subnet.prefix.prefixMask32) + var ma = in_addr() + inet_pton(AF_INET, mask.description, &ma) + + guard vmnet_network_configuration_set_ipv4_subnet(config, &ga, &ma) == .VMNET_SUCCESS else { + throw ContainerizationError(.internalError, message: "failed to set subnet \(subnet) for network") + } + } +} + +#endif diff --git a/Sources/Integration/ContainerTests.swift b/Sources/Integration/ContainerTests.swift index 302d4507..3e45b1ef 100644 --- a/Sources/Integration/ContainerTests.swift +++ b/Sources/Integration/ContainerTests.swift @@ -2895,7 +2895,7 @@ extension IntegrationSuite { let bs = try await bootstrap(id) let customMTU: UInt32 = 1400 - var network = try ContainerManager.VmnetNetwork() + var network = try VmnetNetwork() defer { try? network.release(id) } @@ -3974,7 +3974,7 @@ extension IntegrationSuite { let id = "test-networking-disabled" let bs = try await bootstrap(id) - let network = try ContainerManager.VmnetNetwork() + let network = try VmnetNetwork() var manager = try ContainerManager(vmm: bs.vmm, network: network) defer { try? manager.delete(id) @@ -4027,7 +4027,7 @@ extension IntegrationSuite { let id = "test-networking-enabled" let bs = try await bootstrap(id) - let network = try ContainerManager.VmnetNetwork() + let network = try VmnetNetwork() var manager = try ContainerManager(vmm: bs.vmm, network: network) defer { try? manager.delete(id) diff --git a/Sources/cctl/RunCommand.swift b/Sources/cctl/RunCommand.swift index 8dba0901..3326a3d1 100644 --- a/Sources/cctl/RunCommand.swift +++ b/Sources/cctl/RunCommand.swift @@ -82,9 +82,9 @@ extension Application { ) // Choose network implementation based on macOS version - let network: ContainerManager.Network? + let network: Network? if #available(macOS 26, *) { - network = try ContainerManager.VmnetNetwork() + network = try VmnetNetwork() } else { network = nil } diff --git a/Tests/ContainerizationTests/ContainerManagerTests.swift b/Tests/ContainerizationTests/ContainerManagerTests.swift index 268e4db2..4d3645ec 100644 --- a/Tests/ContainerizationTests/ContainerManagerTests.swift +++ b/Tests/ContainerizationTests/ContainerManagerTests.swift @@ -33,7 +33,7 @@ private struct NilGatewayInterface: Interface { } } -private struct NilGatewayNetwork: ContainerManager.Network { +private struct NilGatewayNetwork: Network { mutating func create(_ id: String) throws -> Interface? { NilGatewayInterface() } diff --git a/examples/ctr-example/Package.resolved b/examples/ctr-example/Package.resolved index ba87c7c7..d50e3654 100644 --- a/examples/ctr-example/Package.resolved +++ b/examples/ctr-example/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "388030ec4369b8efc17c2c2d43edf28074bc35bba6354fef196e9830dc83c5c7", + "originHash" : "5de11e9b526f881c570e7b65cb339765f3aa79e8646a0c1289d36f224f9f8ca0", "pins" : [ { "identity" : "async-http-client", @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/containerization.git", "state" : { - "revision" : "185bc1ecd7b6d9ef1938da1846e620ab73db8950", - "version" : "0.26.3" + "revision" : "636eef0eff00e451de6d5d426e6a6785b90b44e2", + "version" : "0.26.5" } }, { diff --git a/examples/ctr-example/Sources/ctr-example/main.swift b/examples/ctr-example/Sources/ctr-example/main.swift index 0b1123ef..0ae8f651 100644 --- a/examples/ctr-example/Sources/ctr-example/main.swift +++ b/examples/ctr-example/Sources/ctr-example/main.swift @@ -35,7 +35,7 @@ struct CtrExample { var manager = try await ContainerManager( kernel: Kernel(path: URL(fileURLWithPath: kernelPath), platform: .linuxArm), initfsReference: initfsReference, - network: try ContainerManager.VmnetNetwork() + network: try VmnetNetwork() ) let containerId = "ctr-example"