Skip to content
Open
6 changes: 4 additions & 2 deletions Sources/APIServer/ContainerDNSHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,12 @@ struct ContainerDNSHandler: DNSHandler {
}

private func answerHost(question: Question) async throws -> ResourceRecord? {
guard let ipAllocation = try await networkService.lookup(hostname: question.name) else {
guard let ipAllocation = try await networkService.lookup(hostname: question.name),
let ipv4Address = ipAllocation.ipv4Address
else {
return nil
}
let ipv4 = ipAllocation.ipv4Address.address.description
let ipv4 = ipv4Address.address.description
guard let ip = try? IPv4Address(ipv4) else {
throw DNSResolverError.serverError("failed to parse IP address: \(ipv4)")
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/ContainerCommands/Builder/BuilderStatus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ private struct PrintableBuilder: ListDisplayable {
snapshot.id,
snapshot.configuration.image.reference,
snapshot.status.rawValue,
snapshot.networks.map { $0.ipv4Address.description }.joined(separator: ","),
snapshot.networks.map { $0.ipv4Address?.description ?? "" }.joined(separator: ","),
"\(snapshot.configuration.resources.cpus)",
"\(snapshot.configuration.resources.memoryInBytes / (1024 * 1024)) MB",
]
Expand Down
2 changes: 1 addition & 1 deletion Sources/ContainerCommands/Container/ContainerList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ extension PrintableContainer: ListDisplayable {
self.configuration.platform.os,
self.configuration.platform.architecture,
self.status.rawValue,
self.networks.map { $0.ipv4Address.description }.joined(separator: ","),
self.networks.map { $0.ipv4Address?.description ?? "" }.joined(separator: ","),
"\(self.configuration.resources.cpus)",
"\(self.configuration.resources.memoryInBytes / (1024 * 1024)) MB",
self.startedDate?.ISO8601Format() ?? "",
Expand Down
30 changes: 14 additions & 16 deletions Sources/ContainerResource/Network/Attachment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ public struct Attachment: Codable, Sendable {
/// The hostname associated with the attachment.
public let hostname: String
/// The CIDR address describing the interface IPv4 address, with the prefix length of the subnet.
public let ipv4Address: CIDRv4
/// Nil for bridge-mode attachments where the address is assigned by DHCP at runtime.
public let ipv4Address: CIDRv4?
/// The IPv4 gateway address.
public let ipv4Gateway: IPv4Address
/// Nil for bridge-mode attachments where the gateway is discovered via DHCP at runtime.
public let ipv4Gateway: IPv4Address?
/// The CIDR address describing the interface IPv6 address, with the prefix length of the subnet.
/// The address is nil if the IPv6 subnet could not be determined at network creation time.
public let ipv6Address: CIDRv6?
Expand All @@ -37,8 +39,8 @@ public struct Attachment: Codable, Sendable {
public init(
network: String,
hostname: String,
ipv4Address: CIDRv4,
ipv4Gateway: IPv4Address,
ipv4Address: CIDRv4?,
ipv4Gateway: IPv4Address?,
ipv6Address: CIDRv6?,
macAddress: MACAddress?,
mtu: UInt32? = nil
Expand Down Expand Up @@ -72,16 +74,12 @@ public struct Attachment: Codable, Sendable {

network = try container.decode(String.self, forKey: .network)
hostname = try container.decode(String.self, forKey: .hostname)
if let address = try? container.decode(CIDRv4.self, forKey: .ipv4Address) {
ipv4Address = address
} else {
ipv4Address = try container.decode(CIDRv4.self, forKey: .address)
}
if let gateway = try? container.decode(IPv4Address.self, forKey: .ipv4Gateway) {
ipv4Gateway = gateway
} else {
ipv4Gateway = try container.decode(IPv4Address.self, forKey: .gateway)
}
ipv4Address =
try container.decodeIfPresent(CIDRv4.self, forKey: .ipv4Address)
?? container.decodeIfPresent(CIDRv4.self, forKey: .address)
ipv4Gateway =
try container.decodeIfPresent(IPv4Address.self, forKey: .ipv4Gateway)
?? container.decodeIfPresent(IPv4Address.self, forKey: .gateway)
ipv6Address = try container.decodeIfPresent(CIDRv6.self, forKey: .ipv6Address)
macAddress = try container.decodeIfPresent(MACAddress.self, forKey: .macAddress)
mtu = try container.decodeIfPresent(UInt32.self, forKey: .mtu)
Expand All @@ -93,8 +91,8 @@ public struct Attachment: Codable, Sendable {

try container.encode(network, forKey: .network)
try container.encode(hostname, forKey: .hostname)
try container.encode(ipv4Address, forKey: .ipv4Address)
try container.encode(ipv4Gateway, forKey: .ipv4Gateway)
try container.encodeIfPresent(ipv4Address, forKey: .ipv4Address)
try container.encodeIfPresent(ipv4Gateway, forKey: .ipv4Gateway)
try container.encodeIfPresent(ipv6Address, forKey: .ipv6Address)
try container.encodeIfPresent(macAddress, forKey: .macAddress)
try container.encodeIfPresent(mtu, forKey: .mtu)
Expand Down
6 changes: 6 additions & 0 deletions Sources/ContainerXPC/XPCMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,12 @@ extension XPCMessage {
}
}

public func xpcDictionary(key: String) -> xpc_object_t? {
lock.withLock {
xpc_dictionary_get_dictionary(self.object, key)
}
}

public func endpoint(key: String) -> xpc_endpoint_t? {
lock.withLock {
xpc_dictionary_get_value(self.object, key)
Expand Down
36 changes: 27 additions & 9 deletions Sources/Plugins/NetworkVmnet/NetworkVmnetHelper+Start.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,8 @@ import ContainerizationExtras
import Foundation
import Logging

enum Variant: String, ExpressibleByArgument {
case reserved
case allocationOnly
}

extension NetworkMode: ExpressibleByArgument {}
extension NetworkVariant: ExpressibleByArgument {}

extension NetworkVmnetHelper {
struct Start: AsyncParsableCommand {
Expand Down Expand Up @@ -60,17 +56,26 @@ extension NetworkVmnetHelper {
var ipv6Subnet: String?

@Option(name: .long, help: "Variant of the network helper to use.")
var variant: Variant = {
var variant: NetworkVariant = {
guard #available(macOS 26, *) else {
return .allocationOnly
}
return .reserved
}()

@Option(name: .customLong("option-json"), help: "UTF8-encoded JSON string that contains the configuration options for this network")
var stringifiedOptions: String = "{}"

var logRoot = LogRoot.path

func run() async throws {
let commandName = NetworkVmnetHelper._commandName
guard let encodedOptions = stringifiedOptions.data(using: .utf8),
let options = try? JSONSerialization.jsonObject(with: encodedOptions) as? [String: String]
else {
throw ContainerizationError(.invalidArgument, message: "failed to decode network configuration options from JSON string")
}

let logPath = logRoot.map { $0.appending("\(commandName)-\(id).log") }
let log = ServiceLogger.bootstrap(category: "NetworkVmnetHelper", metadata: ["id": "\(id)"], debug: debug, logPath: logPath)
log.info("starting helper", metadata: ["name": "\(commandName)"])
Expand All @@ -89,15 +94,17 @@ extension NetworkVmnetHelper {
ipv4Subnet: ipv4Subnet,
ipv6Subnet: ipv6Subnet,
plugin: NetworkVmnetHelper._commandName,
options: ["variant": self.variant.rawValue]
options: options.merging(
["variant": self.variant.rawValue],
uniquingKeysWith: { _, new in new })
)
let network = try Self.createNetwork(
configuration: configuration,
variant: self.variant,
log: log
)
try await network.start()
let service = try await DefaultNetworkService(network: network, log: log)
let service: NetworkService = try await Self.createNetworkService(network: network, variant: variant, log: log)
let harness = NetworkHarness(service: service)
let xpc = XPCServer(
identifier: serviceIdentifier,
Expand All @@ -122,7 +129,16 @@ extension NetworkVmnetHelper {
}
}

private static func createNetwork(configuration: NetworkConfiguration, variant: Variant, log: Logger) throws -> Network {
private static func createNetworkService(network: Network, variant: NetworkVariant, log: Logger) async throws -> NetworkService {
switch variant {
case .bridged, .bridgedViaHelper:
return try await BridgeNetworkService(network: network, variant: variant, log: log)
default:
return try await DefaultNetworkService(network: network, log: log)
}
}

private static func createNetwork(configuration: NetworkConfiguration, variant: NetworkVariant, log: Logger) throws -> Network {
switch variant {
case .allocationOnly:
return try AllocationOnlyVmnetNetwork(configuration: configuration, log: log)
Expand All @@ -134,6 +150,8 @@ extension NetworkVmnetHelper {
)
}
return try ReservedVmnetNetwork(configuration: configuration, log: log)
case .bridged, .bridgedViaHelper:
return try BridgedVmnetNetwork(configuration: configuration, log: log)
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/Plugins/RuntimeLinux/RuntimeLinuxHelper+Start.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ extension RuntimeLinuxHelper {
NetworkInterfaceKey(plugin: "container-network-vmnet", variant: "allocationOnly"): IsolatedInterfaceStrategy()
]
if #available(macOS 26, *) {
interfaceStrategies[NetworkInterfaceKey(plugin: "container-network-vmnet", variant: "bridged")] = BridgedInterfaceStrategy(log: log)
interfaceStrategies[NetworkInterfaceKey(plugin: "container-network-vmnet", variant: "bridgedViaHelper")] = BridgedInterfaceStrategy(log: log)
interfaceStrategies[NetworkInterfaceKey(plugin: "container-network-vmnet", variant: "reserved")] = NonisolatedInterfaceStrategy(log: log)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,17 @@ public actor NetworksService {
args += ["--variant", variant]
}

// TODO: variant could possibly stay inside the options and does not need to be a dedicated commandline argument?
let options = configuration.options.filter({ key, _ in key != "variant" })
if !options.isEmpty {
guard let encodedOptions = try? JSONSerialization.data(withJSONObject: options),
let stringifiedOptions = String(data: encodedOptions, encoding: .utf8)
else {
throw ContainerizationError(.internalError, message: "failed to encode network configuration options as json string")
}
args += ["--option-json", stringifiedOptions]
}

let entityPath = try store.entityPath(configuration.id)
try pluginLoader.registerWithLaunchd(
plugin: networkPlugin,
Expand Down
23 changes: 23 additions & 0 deletions Sources/Services/Network/Client/BridgeNetworkKeys.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//===----------------------------------------------------------------------===//
// 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.
//===----------------------------------------------------------------------===//

public enum BridgeNetworkKeys: String {
case hostInterface
case enableTso
case enableChecksumOffload
case bufferedPacketCount
case sandboxEndpoint
}
22 changes: 22 additions & 0 deletions Sources/Services/Network/Client/NetworkVariant.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//===----------------------------------------------------------------------===//
// 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.
//===----------------------------------------------------------------------===//

public enum NetworkVariant: String, Sendable {
case reserved
case allocationOnly
case bridged
case bridgedViaHelper
}
Loading