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
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ $(STAGING_DIR):
@mkdir -p "$(join $(STAGING_DIR), bin)"
@mkdir -p "$(join $(STAGING_DIR), libexec/container/plugins/container-runtime-linux/bin)"
@mkdir -p "$(join $(STAGING_DIR), libexec/container/plugins/container-network-vmnet/bin)"
@mkdir -p "$(join $(STAGING_DIR), libexec/container/plugins/container-network-vmnet-helper/bin)"
@mkdir -p "$(join $(STAGING_DIR), libexec/container/plugins/container-core-images/bin)"

@install "$(BUILD_BIN_DIR)/container" "$(join $(STAGING_DIR), bin/container)"
Expand All @@ -107,6 +108,8 @@ $(STAGING_DIR):
@install Sources/Plugins/RuntimeLinux/config.toml "$(join $(STAGING_DIR), libexec/container/plugins/container-runtime-linux/config.toml)"
@install "$(BUILD_BIN_DIR)/container-network-vmnet" "$(join $(STAGING_DIR), libexec/container/plugins/container-network-vmnet/bin/container-network-vmnet)"
@install Sources/Plugins/NetworkVmnet/config.toml "$(join $(STAGING_DIR), libexec/container/plugins/container-network-vmnet/config.toml)"
@install "$(BUILD_BIN_DIR)/container-network-vmnet-helper" "$(join $(STAGING_DIR), libexec/container/plugins/container-network-vmnet-helper/bin/container-network-vmnet-helper)"
@install Sources/Plugins/NetworkVmnetHelper/config.toml "$(join $(STAGING_DIR), libexec/container/plugins/container-network-vmnet-helper/config.toml)"
@install "$(BUILD_BIN_DIR)/container-core-images" "$(join $(STAGING_DIR), libexec/container/plugins/container-core-images/bin/container-core-images)"
@install Sources/Plugins/CoreImages/config.toml "$(join $(STAGING_DIR), libexec/container/plugins/container-core-images/config.toml)"

Expand All @@ -123,6 +126,7 @@ installer-pkg: $(STAGING_DIR)
@codesign $(CODESIGN_OPTS) --prefix=com.apple.container. "$(join $(STAGING_DIR), libexec/container/plugins/container-core-images/bin/container-core-images)"
@codesign $(CODESIGN_OPTS) --prefix=com.apple.container. --entitlements=signing/container-runtime-linux.entitlements "$(join $(STAGING_DIR), libexec/container/plugins/container-runtime-linux/bin/container-runtime-linux)"
@codesign $(CODESIGN_OPTS) --prefix=com.apple.container. --entitlements=signing/container-network-vmnet.entitlements "$(join $(STAGING_DIR), libexec/container/plugins/container-network-vmnet/bin/container-network-vmnet)"
@codesign $(CODESIGN_OPTS) --prefix=com.apple.container. "$(join $(STAGING_DIR), libexec/container/plugins/container-network-vmnet-helper/bin/container-network-vmnet-helper)"

@echo Creating application installer
@pkgbuild --root "$(STAGING_DIR)" --identifier com.apple.container-installer --install-location /usr/local --version ${RELEASE_VERSION} $(PKG_PATH)
Expand All @@ -135,6 +139,7 @@ dsym:
@mkdir -p "$(DSYM_DIR)"
@cp -a "$(BUILD_BIN_DIR)/container-runtime-linux.dSYM" "$(DSYM_DIR)"
@cp -a "$(BUILD_BIN_DIR)/container-network-vmnet.dSYM" "$(DSYM_DIR)"
@cp -a "$(BUILD_BIN_DIR)/container-network-vmnet-helper.dSYM" "$(DSYM_DIR)"
@cp -a "$(BUILD_BIN_DIR)/container-core-images.dSYM" "$(DSYM_DIR)"
@cp -a "$(BUILD_BIN_DIR)/container-apiserver.dSYM" "$(DSYM_DIR)"
@cp -a "$(BUILD_BIN_DIR)/container.dSYM" "$(DSYM_DIR)"
Expand Down
30 changes: 30 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ let package = Package(
.library(name: "ContainerImagesService", targets: ["ContainerImagesService", "ContainerImagesServiceClient"]),
.library(name: "ContainerNetworkClient", targets: ["ContainerNetworkClient"]),
.library(name: "ContainerNetworkServer", targets: ["ContainerNetworkServer"]),
.library(name: "ContainerNetworkVmnetHelperServer", targets: ["ContainerNetworkVmnetHelperServer"]),
.library(name: "ContainerNetworkVmnetServer", targets: ["ContainerNetworkVmnetServer"]),
.library(name: "ContainerResource", targets: ["ContainerResource"]),
.library(name: "ContainerLog", targets: ["ContainerLog"]),
Expand Down Expand Up @@ -307,6 +308,24 @@ let package = Package(
path: "Sources/Plugins/NetworkVmnet",
exclude: ["config.toml"]
),
.executableTarget(
name: "container-network-vmnet-helper",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Logging", package: "swift-log"),
.product(name: "ContainerizationExtras", package: "containerization"),
"ContainerLog",
"ContainerNetworkVmnetHelperServer",
"ContainerNetworkClient",
"ContainerNetworkServer",
"ContainerPlugin",
"ContainerResource",
"ContainerVersion",
"ContainerXPC",
],
path: "Sources/Plugins/NetworkVmnetHelper",
exclude: ["config.toml"]
),
.target(
name: "ContainerNetworkClient",
dependencies: [
Expand Down Expand Up @@ -345,6 +364,17 @@ let package = Package(
],
path: "Sources/Services/NetworkVmnet/Server"
),
.target(
name: "ContainerNetworkVmnetHelperServer",
dependencies: [
.product(name: "Logging", package: "swift-log"),
.product(name: "ContainerizationExtras", package: "containerization"),
"ContainerNetworkServer",
"ContainerResource",
"ContainerXPC",
],
path: "Sources/Services/NetworkVmnetHelper/Server"
),
.target(
name: "ContainerRuntimeLinuxClient",
dependencies: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,14 @@ public struct AttachmentOptions: Codable, Sendable {
/// The MTU for the network interface.
public let mtu: UInt32?

public init(hostname: String, macAddress: MACAddress? = nil, mtu: UInt32? = nil) {
/// A specific IPv4 address requested for the attachment (optional).
/// Only supported by network plugins that allow static address assignment.
public let ip: IPv4Address?

public init(hostname: String, macAddress: MACAddress? = nil, mtu: UInt32? = nil, ip: IPv4Address? = nil) {
self.hostname = hostname
self.macAddress = macAddress
self.mtu = mtu
self.ip = ip
}
}
44 changes: 44 additions & 0 deletions Sources/ContainerResource/Network/VmnetHelperNetworkKeys.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//===----------------------------------------------------------------------===//
// 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.
//===----------------------------------------------------------------------===//

/// Constants shared between the bridged network plugin and the runtime
/// interface strategy.
public enum VmnetHelperNetwork {
/// The name of the bridged network plugin.
public static let pluginName = "container-network-vmnet-helper"

/// Network creation options (`container network create --option key=value`).
public enum Option: String {
/// The physical host interface to bridge (e.g. `en0`). Required.
case interface
/// The IPv4 gateway address on the bridged subnet. Required.
case gateway
/// An optional pool for automatic allocation, formatted as
/// `START-END` (e.g. `192.168.1.200-192.168.1.220`).
case pool
/// An optional override for the vmnet-helper binary path.
case helperPath = "helper-path"
}

/// Keys for the additional data message returned with each
/// allocation.
public enum AdditionalDataKey: String {
/// The physical host interface to bridge.
case interface
/// The vmnet-helper binary path override, if configured.
case helperPath
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
//===----------------------------------------------------------------------===//
// 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 ArgumentParser
import ContainerLog
import ContainerNetworkClient
import ContainerNetworkServer
import ContainerNetworkVmnetHelperServer
import ContainerPlugin
import ContainerResource
import ContainerXPC
import ContainerizationError
import ContainerizationExtras
import Foundation
import Logging

extension NetworkVmnetHelperPlugin {
struct Start: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "start",
abstract: "Starts the bridged network plugin"
)

@Flag(name: .long, help: "Enable debug logging")
var debug = false

@Option(name: .long, help: "XPC service identifier")
var serviceIdentifier: String

@Option(name: .shortAndLong, help: "Network identifier")
var id: String

// Bridged networks define their own semantics, so the mode
// from the API server is accepted and ignored.
@Option(name: .long, help: "Network mode (ignored)")
var mode: String = "nat"

@Option(name: .customLong("subnet"), help: "CIDR address of the physical IPv4 subnet")
var ipv4Subnet: String?

@Option(
name: .customLong("option"),
help: "Plugin option as key=value (interface=NAME, gateway=ADDR, pool=START-END, helper-path=PATH)"
)
var options: [String] = []

var logRoot = LogRoot.path

func run() async throws {
let commandName = NetworkVmnetHelperPlugin._commandName
let logPath = logRoot.map { $0.appending("\(commandName)-\(id).log") }
let log = ServiceLogger.bootstrap(category: "NetworkVmnetHelperPlugin", metadata: ["id": "\(id)"], debug: debug, logPath: logPath)
log.info("starting helper", metadata: ["name": "\(commandName)"])
defer {
log.info("stopping helper", metadata: ["name": "\(commandName)"])
}

do {
let configuration = try parseConfiguration()
let network = VmnetHelperPhysicalNetwork(configuration: configuration, log: log)
try await network.start()
let service = VmnetHelperNetworkService(network: network, log: log)
let harness = NetworkHarness(service: service)
let xpc = XPCServer(
identifier: serviceIdentifier,
routes: [
NetworkRoutes.status.rawValue: XPCServer.route(harness.status),
NetworkRoutes.allocate.rawValue: harness.allocate,
NetworkRoutes.lookup.rawValue: XPCServer.route(harness.lookup),
],
log: log
)

log.info("starting XPC server")
try await xpc.listen()
} catch {
log.error(
"helper failed",
metadata: [
"name": "\(commandName)",
"error": "\(error)",
])
NetworkVmnetHelperPlugin.exit(withError: error)
}
}

private func parseConfiguration() throws -> VmnetHelperPhysicalNetwork.Configuration {
guard let subnetText = ipv4Subnet else {
throw ContainerizationError(
.invalidArgument,
message: "bridged networks require the physical subnet, create the network with --subnet"
)
}
let subnet = try CIDRv4(subnetText)

var optionValues: [String: String] = [:]
for option in options {
let keyVal = option.split(separator: "=", maxSplits: 1, omittingEmptySubsequences: false)
guard keyVal.count == 2, !keyVal[0].isEmpty else {
throw ContainerizationError(.invalidArgument, message: "invalid option format '\(option)': expected key=value")
}
optionValues[String(keyVal[0])] = String(keyVal[1])
}

guard let hostInterface = optionValues[VmnetHelperNetwork.Option.interface.rawValue], !hostInterface.isEmpty else {
throw ContainerizationError(
.invalidArgument,
message: "bridged networks require the host interface, create the network with --option interface=NAME"
)
}
guard let gatewayText = optionValues[VmnetHelperNetwork.Option.gateway.rawValue] else {
throw ContainerizationError(
.invalidArgument,
message: "bridged networks require the gateway address, create the network with --option gateway=ADDR"
)
}
let gateway = try IPv4Address(gatewayText)

var pool: ClosedRange<UInt32>?
if let poolText = optionValues[VmnetHelperNetwork.Option.pool.rawValue] {
let bounds = poolText.split(separator: "-", omittingEmptySubsequences: false)
guard bounds.count == 2,
let lower = try? IPv4Address(String(bounds[0])),
let upper = try? IPv4Address(String(bounds[1])),
lower.value <= upper.value
else {
throw ContainerizationError(.invalidArgument, message: "invalid pool '\(poolText)': expected START-END IPv4 addresses")
}
pool = lower.value...upper.value
}

return try VmnetHelperPhysicalNetwork.Configuration(
id: id,
ipv4Subnet: subnet,
ipv4Gateway: gateway,
hostInterface: hostInterface,
pool: pool,
helperPath: optionValues[VmnetHelperNetwork.Option.helperPath.rawValue]
)
}
}
}
30 changes: 30 additions & 0 deletions Sources/Plugins/NetworkVmnetHelper/NetworkVmnetHelperPlugin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//===----------------------------------------------------------------------===//
// 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 ArgumentParser
import ContainerVersion

@main
struct NetworkVmnetHelperPlugin: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "container-network-vmnet-helper",
abstract: "XPC service for managing a bridged physical network",
version: ReleaseVersion.singleLine(appName: "container-network-vmnet-helper"),
subcommands: [
Start.self
]
)
}
11 changes: 11 additions & 0 deletions Sources/Plugins/NetworkVmnetHelper/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
abstract = "bridged physical network management plugin"
author = "Apple"
version = 0.1

[servicesConfig]
loadAtBoot = false
runAtLoad = true
defaultArguments = []

[[servicesConfig.services]]
type = "network"
3 changes: 2 additions & 1 deletion Sources/Plugins/RuntimeLinux/RuntimeLinuxHelper+Start.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ extension RuntimeLinuxHelper {

// FIXME: The network plugins that the runtime supports should be configurable elsewhere
var interfaceStrategies: [NetworkInterfaceKey: InterfaceStrategy] = [
NetworkInterfaceKey(plugin: "container-network-vmnet", variant: "allocationOnly"): IsolatedInterfaceStrategy()
NetworkInterfaceKey(plugin: "container-network-vmnet", variant: "allocationOnly"): IsolatedInterfaceStrategy(),
NetworkInterfaceKey(plugin: VmnetHelperNetwork.pluginName, variant: nil): VmnetHelperInterfaceStrategy(log: log),
]
if #available(macOS 26, *) {
interfaceStrategies[NetworkInterfaceKey(plugin: "container-network-vmnet", variant: "reserved")] = NonisolatedInterfaceStrategy(log: log)
Expand Down
Loading