680845cb1c
Linux build / Determine Swift version (push) Waiting to run
Linux build / Linux compile check (push) Blocked by required conditions
Build containerization / Verify commit signatures (push) Has been skipped
Build containerization / containerization (push) Successful in 0s
303 lines
11 KiB
Swift
303 lines
11 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// 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 IPv6 prefix of this network.
|
|
public let prefixV6: CIDRv6?
|
|
|
|
/// The IPv4 gateway address of this network.
|
|
public var ipv4Gateway: IPv4Address {
|
|
subnet.gateway
|
|
}
|
|
|
|
/// The IPv6 gateway address of this network, if a prefix exists.
|
|
public var ipv6Gateway: IPv6Address? {
|
|
prefixV6?.gateway
|
|
}
|
|
|
|
struct Allocator: Sendable {
|
|
private let indexAllocatorV4: any AddressAllocator<UInt32>
|
|
private let indexAllocatorV6: (any AddressAllocator<UInt32>)?
|
|
private let cidrV4: CIDRv4
|
|
private let cidrV6: CIDRv6?
|
|
private var allocations: [String: (v4: UInt32, v6: UInt32?)]
|
|
|
|
init(cidrV4: CIDRv4, cidrV6: CIDRv6?) throws {
|
|
self.cidrV4 = cidrV4
|
|
self.cidrV6 = cidrV6
|
|
self.allocations = .init()
|
|
let v4Size = Int(cidrV4.upper.value - cidrV4.lower.value - 3)
|
|
self.indexAllocatorV4 = try UInt32.rotatingAllocator(
|
|
lower: cidrV4.lower.value + 2,
|
|
size: UInt32(v4Size)
|
|
)
|
|
if cidrV6 != nil {
|
|
// Independent v6 allocator. The host portion is sourced from a
|
|
// UInt32 index regardless of prefix length, and we never need
|
|
// more v6 entries than v4 can serve.
|
|
self.indexAllocatorV6 = try UInt32.rotatingAllocator(
|
|
lower: 2,
|
|
size: UInt32(v4Size)
|
|
)
|
|
} else {
|
|
self.indexAllocatorV6 = nil
|
|
}
|
|
}
|
|
|
|
mutating func allocate(_ id: String) throws -> (CIDRv4, CIDRv6?) {
|
|
if allocations[id] != nil {
|
|
throw ContainerizationError(.exists, message: "allocation with id \(id) already exists")
|
|
}
|
|
let v4Index = try indexAllocatorV4.allocate()
|
|
let v4 = try CIDRv4(IPv4Address(v4Index), prefix: cidrV4.prefix)
|
|
|
|
var v6Index: UInt32? = nil
|
|
let v6: CIDRv6?
|
|
if let indexAllocatorV6, let cidrV6 {
|
|
do {
|
|
let idx = try indexAllocatorV6.allocate()
|
|
v6Index = idx
|
|
let v6Value = (cidrV6.address.value & cidrV6.prefix.prefixMask128) | UInt128(idx)
|
|
v6 = try CIDRv6(IPv6Address(v6Value), prefix: cidrV6.prefix)
|
|
} catch {
|
|
// Roll back v4 so the pair stays atomic.
|
|
try? indexAllocatorV4.release(v4Index)
|
|
throw error
|
|
}
|
|
} else {
|
|
v6 = nil
|
|
}
|
|
|
|
allocations[id] = (v4: v4Index, v6: v6Index)
|
|
return (v4, v6)
|
|
}
|
|
|
|
mutating func release(_ id: String) throws {
|
|
if let entry = self.allocations[id] {
|
|
try indexAllocatorV4.release(entry.v4)
|
|
if let v6Index = entry.v6 {
|
|
try indexAllocatorV6?.release(v6Index)
|
|
}
|
|
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 ipv6Address: CIDRv6?
|
|
public let ipv6Gateway: IPv6Address?
|
|
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,
|
|
ipv6Address: CIDRv6? = nil,
|
|
ipv6Gateway: IPv6Address? = nil,
|
|
macAddress: MACAddress? = nil,
|
|
mtu: UInt32 = 1500
|
|
) {
|
|
self.ipv4Address = ipv4Address
|
|
self.ipv4Gateway = ipv4Gateway
|
|
self.ipv6Address = ipv6Address
|
|
self.ipv6Gateway = ipv6Gateway
|
|
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`.
|
|
/// - subnetV4: The IPv4 subnet to use for this network.
|
|
/// - prefixV6: The IPv6 prefix to use for this network.
|
|
public init(
|
|
mode: vmnet.operating_modes_t = .VMNET_SHARED_MODE,
|
|
subnet: CIDRv4? = nil,
|
|
prefixV6: CIDRv6? = 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.configureSubnetV4(config, subnetV4: subnet)
|
|
}
|
|
if let prefixV6 {
|
|
try Self.configurePrefixV6(config, prefixV6: prefixV6)
|
|
}
|
|
|
|
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 cidrV4 = try Self.getSubnetV4(ref)
|
|
let cidrV6 = Self.getPrefixV6(ref)
|
|
|
|
self.allocator = try .init(cidrV4: cidrV4, cidrV6: cidrV6)
|
|
self.subnet = cidrV4
|
|
self.prefixV6 = cidrV6
|
|
self.reference = ref
|
|
}
|
|
|
|
/// Returns a new interface for use with a container. Allocates an IPv4
|
|
/// address from the network's subnet, and — when the network has an IPv6
|
|
/// prefix — an IPv6 address from that prefix. The two allocations are
|
|
/// independent.
|
|
/// - Parameter id: The container ID.
|
|
public mutating func createInterface(_ id: String) throws -> Containerization.Interface? {
|
|
let (v4, v6) = try allocator.allocate(id)
|
|
return Self.Interface(
|
|
reference: self.reference,
|
|
ipv4Address: v4,
|
|
ipv4Gateway: self.ipv4Gateway,
|
|
ipv6Address: v6,
|
|
ipv6Gateway: self.ipv6Gateway
|
|
)
|
|
}
|
|
|
|
/// 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 createInterface(_ id: String, mtu: UInt32) throws -> Containerization.Interface? {
|
|
let (v4, v6) = try allocator.allocate(id)
|
|
return Self.Interface(
|
|
reference: self.reference,
|
|
ipv4Address: v4,
|
|
ipv4Gateway: self.ipv4Gateway,
|
|
ipv6Address: v6,
|
|
ipv6Gateway: self.ipv6Gateway,
|
|
mtu: mtu
|
|
)
|
|
}
|
|
|
|
/// Returns a new interface without a default gateway route. Useful for
|
|
/// secondary interfaces where another interface already provides the
|
|
/// default route.
|
|
/// - Parameter id: The container ID.
|
|
public mutating func createInterfaceWithoutGateway(_ id: String) throws -> Containerization.Interface? {
|
|
let (v4, v6) = try allocator.allocate(id)
|
|
return Self.Interface(
|
|
reference: self.reference,
|
|
ipv4Address: v4,
|
|
ipv6Address: v6
|
|
)
|
|
}
|
|
|
|
/// Performs cleanup of an interface.
|
|
/// - Parameter id: The container ID.
|
|
public mutating func releaseInterface(_ id: String) throws {
|
|
try allocator.release(id)
|
|
}
|
|
|
|
private static func getSubnetV4(_ 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 configureSubnetV4(_ config: vmnet_network_configuration_ref, subnetV4: CIDRv4) throws {
|
|
let gateway = subnetV4.gateway
|
|
|
|
var ga = in_addr()
|
|
inet_pton(AF_INET, gateway.description, &ga)
|
|
|
|
let mask = IPv4Address(subnetV4.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 IPv4 subnet \(subnetV4) for network")
|
|
}
|
|
}
|
|
|
|
private static func getPrefixV6(_ ref: vmnet_network_ref) -> CIDRv6? {
|
|
var p = in6_addr()
|
|
var len: UInt8 = 0
|
|
vmnet_network_get_ipv6_prefix(ref, &p, &len)
|
|
|
|
guard len > 0, let prefix = Prefix.ipv6(len) else {
|
|
return nil
|
|
}
|
|
|
|
let bytes: [UInt8] = withUnsafeBytes(of: p) { Array($0) }
|
|
guard let address = try? IPv6Address(bytes) else {
|
|
return nil
|
|
}
|
|
return try? CIDRv6(address, prefix: prefix)
|
|
}
|
|
|
|
private static func configurePrefixV6(_ config: vmnet_network_configuration_ref, prefixV6: CIDRv6) throws {
|
|
var p = in6_addr()
|
|
inet_pton(AF_INET6, prefixV6.lower.description, &p)
|
|
|
|
guard vmnet_network_configuration_set_ipv6_prefix(config, &p, prefixV6.prefix.length) == .VMNET_SUCCESS else {
|
|
throw ContainerizationError(.internalError, message: "failed to set IPv6 prefix \(prefixV6) for network")
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif
|