Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Sources/APIServer/ContainerDNSHandler.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -76,10 +76,12 @@ struct ContainerDNSHandler: DNSHandler {
}

privatefunc answerHost(question:Question)asyncthrows->ResourceRecord?{
guardlet ipAllocation =tryawait networkService.lookup(hostname: question.name)else{
guardlet ipAllocation =tryawait networkService.lookup(hostname: question.name),
let ipv4Address = ipAllocation.ipv4Address
else{
returnnil
}
letipv4=ipAllocation.ipv4Address.address.description
letipv4= ipv4Address.address.description
guardlet ip =try?IPv4Address(ipv4)else{
throwDNSResolverError.serverError("failed to parse IP address: \(ipv4)")
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/ContainerCommands/Builder/BuilderStatus.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -81,7 +81,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
38 changes: 38 additions & 0 deletions Sources/ContainerCommands/Container/ContainerList.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,3 +52,41 @@ extension Application {
}
}
}

extension PrintableContainer: ListDisplayable {
static var tableHeader: [String] {
["ID", "IMAGE", "OS", "ARCH", "STATE", "IP", "CPUS", "MEMORY", "STARTED"]
}

var tableRow: [String] {
[
self.configuration.id,
self.configuration.image.reference,
self.configuration.platform.os,
self.configuration.platform.architecture,
self.status.rawValue,
self.networks.map { $0.ipv4Address?.description ?? "" }.joined(separator: ","),
"\(self.configuration.resources.cpus)",
"\(self.configuration.resources.memoryInBytes / (1024 * 1024)) MB",
self.startedDate?.ISO8601Format() ?? "",
]
}

var quietValue: String {
self.configuration.id
}
}

struct PrintableContainer: Codable, Sendable {
let status: RuntimeStatus
let configuration: ContainerConfiguration
let networks: [Attachment]
let startedDate: Date?

init(_ container: ContainerSnapshot) {
self.status = container.status
self.configuration = container.configuration
self.networks = container.networks
self.startedDate = container.startedDate
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,7 @@ extension ManagedContainer: ListDisplayable {
configuration.platform.os,
configuration.platform.architecture,
status.state.rawValue,
status.networks.map { $0.ipv4Address.description }.joined(separator: ","),
status.networks.map { $0.ipv4Address?.description ?? "<none>" }.joined(separator: ","),
"\(configuration.resources.cpus)",
"\(configuration.resources.memoryInBytes / (1024 * 1024)) MB",
status.startedDate?.ISO8601Format() ?? "",
Expand Down
30 changes: 14 additions & 16 deletions Sources/ContainerResource/Network/Attachment.swift
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -403,6 +403,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
Original file line numberDiff line numberDiff line change
Expand Up@@ -162,7 +162,7 @@ public actor MachinesService {
self.log.warning("failed to fetch container addresses: \(error)")
}
let addressMap = (containers ?? []).reduce(into: [String: String]()) { result, c in
if let addr = c.networks.first?.ipv4Address.address.description {
if let addr = c.networks.first?.ipv4Address?.address.description {
result[c.id] = addr
}
}
Expand DownExpand Up@@ -536,7 +536,7 @@ public actor MachinesService {
if snapshot.status == .running, let cid = snapshot.containerId {
do {
let container = try await self.client.get(id: cid)
snapshot.ipAddress = container.networks.first?.ipv4Address.address.description
snapshot.ipAddress = container.networks.first?.ipv4Address?.address.description
} catch {
self.log.warning("failed to fetch container address for \(cid): \(error)")
}
Expand Down
23 changes: 23 additions & 0 deletions Sources/Services/Network/Client/BridgeNetworkKeys.swift
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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