Adds network IPv6 configuration. (#975)

- Part of work for #460.
- Enable set/get of IPv6 network prefix in ReservedVmnetNetwork.
- Show IPv6 prefix in `network list` full output.
- Option for setting IPv6 prefix when creating a network.
- System property for default IPv6 prefix.

## Type of Change
- [ ] Bug fix
- [x] New feature  
- [ ] Breaking change
- [x] Documentation update

## Motivation and Context
See #460.

## Testing
- [x] Tested locally
- [ ] Added/updated tests
- [x] Added/updated docs
This commit is contained in:
J Logan
2025-12-22 10:16:14 -08:00
committed by GitHub
parent 9c239aa36c
commit 5064b0ffd5
14 changed files with 173 additions and 60 deletions
+3 -3
View File
@@ -1,5 +1,5 @@
{
"originHash" : "64cb422fa3914611343af4301e317573002890fea7d174e550cc937a76571515",
"originHash" : "928d12d151bf6f1a66dad38525ddf6aecbc8390889e72345fe353b89fc304830",
"pins" : [
{
"identity" : "async-http-client",
@@ -15,8 +15,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/containerization.git",
"state" : {
"revision" : "9ba8267afbdff66e5ddce180312abdb41395292f",
"version" : "0.17.0"
"revision" : "dcbc7bf71da8fe993f6d19214adb8179eec7d02a",
"version" : "0.18.0"
}
},
{
+1 -1
View File
@@ -23,7 +23,7 @@ import PackageDescription
let releaseVersion = ProcessInfo.processInfo.environment["RELEASE_VERSION"] ?? "0.0.0"
let gitCommit = ProcessInfo.processInfo.environment["GIT_COMMIT"] ?? "unspecified"
let builderShimVersion = "0.7.0"
let scVersion = "0.17.0"
let scVersion = "0.18.0"
let package = Package(
name: "container",
@@ -32,7 +32,10 @@ extension Application {
var labels: [String] = []
@Option(name: .customLong("subnet"), help: "Set subnet for a network")
var subnet: String? = nil
var ipv4Subnet: String? = nil
@Option(name: .customLong("subnet-v6"), help: "Set the IPv6 prefix for a network")
var ipv6Subnet: String? = nil
@OptionGroup
var global: Flags.Global
@@ -44,8 +47,9 @@ extension Application {
public func run() async throws {
let parsedLabels = Utility.parseKeyValuePairs(labels)
let ipv4Subnet = try subnet.map { try CIDRv4($0) }
let config = try NetworkConfiguration(id: self.name, mode: .nat, ipv4Subnet: ipv4Subnet, labels: parsedLabels)
let ipv4Subnet = try ipv4Subnet.map { try CIDRv4($0) }
let ipv6Subnet = try ipv6Subnet.map { try CIDRv6($0) }
let config = try NetworkConfiguration(id: self.name, mode: .nat, ipv4Subnet: ipv4Subnet, ipv6Subnet: ipv6Subnet, labels: parsedLabels)
let state = try await ClientNetwork.create(configuration: config)
print(state.id)
}
@@ -74,6 +74,11 @@ extension Application {
throw ContainerizationError(.invalidArgument, message: "invalid CIDRv4 address: \(value)")
}
DefaultsStore.set(value: value, key: key)
case .defaultIPv6Subnet:
guard (try? CIDRv6(value)) != nil else {
throw ContainerizationError(.invalidArgument, message: "invalid CIDRv6 address: \(value)")
}
DefaultsStore.set(value: value, key: key)
}
}
}
@@ -30,6 +30,7 @@ public enum DefaultsStore {
case defaultKernelBinaryPath = "kernel.binaryPath"
case defaultKernelURL = "kernel.url"
case defaultSubnet = "network.subnet"
case defaultIPv6Subnet = "network.subnetv6"
case defaultRegistryDomain = "registry.domain"
}
@@ -73,6 +74,7 @@ public enum DefaultsStore {
(.defaultKernelBinaryPath, { Self.get(key: $0) }),
(.defaultKernelURL, { Self.get(key: $0) }),
(.defaultSubnet, { Self.getOptional(key: $0) }),
(.defaultIPv6Subnet, { Self.getOptional(key: $0) }),
(.defaultDNSDomain, { Self.getOptional(key: $0) }),
(.defaultRegistryDomain, { Self.get(key: $0) }),
]
@@ -131,7 +133,9 @@ extension DefaultsStore.Keys {
case .defaultKernelURL:
return "The URL for the kernel file to install, or the URL for an archive containing the kernel file."
case .defaultSubnet:
return "Default subnet for IP allocation (used on macOS 15 only)."
return "Default subnet for IPv4 allocation."
case .defaultIPv6Subnet:
return "Default IPv6 network prefix."
case .defaultRegistryDomain:
return "The default registry to use for image references that do not specify a registry."
}
@@ -153,6 +157,8 @@ extension DefaultsStore.Keys {
return String.self
case .defaultSubnet:
return String.self
case .defaultIPv6Subnet:
return String.self
case .defaultRegistryDomain:
return String.self
}
@@ -180,6 +186,8 @@ extension DefaultsStore.Keys {
return "https://github.com/kata-containers/kata-containers/releases/download/3.17.0/kata-static-3.17.0-arm64.tar.xz"
case .defaultSubnet:
return "192.168.64.1/24"
case .defaultIPv6Subnet:
return "fd00::/64"
case .defaultRegistryDomain:
return "docker.io"
}
@@ -37,8 +37,11 @@ extension NetworkVmnetHelper {
@Option(name: .shortAndLong, help: "Network identifier")
var id: String
@Option(name: .shortAndLong, help: "CIDR address for the subnet")
var subnet: String?
@Option(name: .customLong("subnet"), help: "CIDR address for the IPv4 subnet")
var ipv4Subnet: String?
@Option(name: .customLong("subnet-v6"), help: "CIDR address for the IPv6 prefix")
var ipv6Subnet: String?
func run() async throws {
let commandName = NetworkVmnetHelper._commandName
@@ -50,8 +53,14 @@ extension NetworkVmnetHelper {
do {
log.info("configuring XPC server")
let ipv4Subnet = try self.subnet.map { try CIDRv4($0) }
let configuration = try NetworkConfiguration(id: id, mode: .nat, ipv4Subnet: ipv4Subnet)
let ipv4Subnet = try self.ipv4Subnet.map { try CIDRv4($0) }
let ipv6Subnet = try self.ipv6Subnet.map { try CIDRv6($0) }
let configuration = try NetworkConfiguration(
id: id,
mode: .nat,
ipv4Subnet: ipv4Subnet,
ipv6Subnet: ipv6Subnet,
)
let network = try Self.createNetwork(configuration: configuration, log: log)
try await network.start()
let server = try await NetworkService(network: network, log: log)
@@ -283,7 +283,7 @@ public actor NetworksService {
serviceIdentifier,
]
if let ipv4Subnet = (configuration.ipv4Subnet.map { $0 }) {
if let ipv4Subnet = configuration.ipv4Subnet {
var existingCidrs: [CIDRv4] = []
for networkState in networkStates.values {
if case .running(_, let status) = networkState {
@@ -303,6 +303,26 @@ public actor NetworksService {
args += ["--subnet", ipv4Subnet.description]
}
if let ipv6Subnet = configuration.ipv6Subnet {
var existingCidrs: [CIDRv6] = []
for networkState in networkStates.values {
if case .running(_, let status) = networkState, let otherIPv6Subnet = status.ipv6Subnet {
existingCidrs.append(otherIPv6Subnet)
}
}
let overlap = existingCidrs.first {
$0.contains(ipv6Subnet.lower)
|| $0.contains(ipv6Subnet.upper)
|| ipv6Subnet.contains($0.lower)
|| ipv6Subnet.contains($0.upper)
}
if let overlap {
throw ContainerizationError(.exists, message: "IPv6 subnet \(ipv6Subnet) overlaps an existing network with subnet \(overlap)")
}
args += ["--subnet-v6", ipv6Subnet.description]
}
try await pluginLoader.registerWithLaunchd(
plugin: networkPlugin,
pluginStateRoot: store.entityUrl(configuration.id),
@@ -67,7 +67,12 @@ public actor AllocationOnlyVmnetNetwork: Network {
let subnet = DefaultsStore.get(key: .defaultSubnet)
let subnetCIDR = try CIDRv4(subnet)
let gateway = IPv4Address(subnetCIDR.lower.value + 1)
self._state = .running(configuration, NetworkStatus(ipv4Subnet: subnetCIDR, ipv4Gateway: gateway))
let status = NetworkStatus(
ipv4Subnet: subnetCIDR,
ipv4Gateway: gateway,
ipv6Subnet: nil,
)
self._state = .running(configuration, status)
log.info(
"started allocation-only network",
metadata: [
@@ -32,6 +32,9 @@ public struct NetworkConfiguration: Codable, Sendable, Identifiable {
/// The preferred CIDR address for the IPv4 subnet, if specified
public let ipv4Subnet: CIDRv4?
/// The preferred CIDR address for the IPv6 subnet, if specified
public let ipv6Subnet: CIDRv6?
/// Key-value labels for the network.
public var labels: [String: String] = [:]
@@ -40,12 +43,14 @@ public struct NetworkConfiguration: Codable, Sendable, Identifiable {
id: String,
mode: NetworkMode,
ipv4Subnet: CIDRv4? = nil,
ipv6Subnet: CIDRv6? = nil,
labels: [String: String] = [:]
) throws {
self.id = id
self.creationDate = Date()
self.mode = mode
self.ipv4Subnet = ipv4Subnet
self.ipv6Subnet = ipv6Subnet
self.labels = labels
try validate()
}
@@ -55,6 +60,7 @@ public struct NetworkConfiguration: Codable, Sendable, Identifiable {
case creationDate
case mode
case ipv4Subnet
case ipv6Subnet
case labels
// TODO: retain for deserialization compatability for now, remove later
case subnet
@@ -72,6 +78,8 @@ public struct NetworkConfiguration: Codable, Sendable, Identifiable {
try container.decodeIfPresent(String.self, forKey: .ipv4Subnet)
?? container.decodeIfPresent(String.self, forKey: .subnet)
ipv4Subnet = try subnetText.map { try CIDRv4($0) }
ipv6Subnet = try container.decodeIfPresent(String.self, forKey: .ipv6Subnet)
.map { try CIDRv6($0) }
labels = try container.decodeIfPresent([String: String].self, forKey: .labels) ?? [:]
try validate()
}
@@ -83,7 +91,8 @@ public struct NetworkConfiguration: Codable, Sendable, Identifiable {
try container.encode(id, forKey: .id)
try container.encode(creationDate, forKey: .creationDate)
try container.encode(mode, forKey: .mode)
try container.encodeIfPresent(ipv4Subnet?.description, forKey: .ipv4Subnet)
try container.encodeIfPresent(ipv4Subnet, forKey: .ipv4Subnet)
try container.encodeIfPresent(ipv6Subnet, forKey: .ipv6Subnet)
try container.encode(labels, forKey: .labels)
}
@@ -21,40 +21,23 @@ public struct NetworkStatus: Codable, Sendable {
/// The address allocated for the network if no subnet was specified at
/// creation time; otherwise, the subnet from the configuration.
public let ipv4Subnet: CIDRv4
/// The gateway IPv4 address.
public let ipv4Gateway: IPv4Address
/// The address allocated for the IPv6 network if no subnet was specified at
/// creation time; otherwise, the IPv6 subnet from the configuration.
public let ipv6Subnet: CIDRv6?
public init(
ipv4Subnet: CIDRv4,
ipv4Gateway: IPv4Address
ipv4Gateway: IPv4Address,
ipv6Subnet: CIDRv6?,
) {
self.ipv4Subnet = ipv4Subnet
self.ipv4Gateway = ipv4Gateway
self.ipv6Subnet = ipv6Subnet
}
enum CodingKeys: String, CodingKey {
case ipv4Subnet
case ipv4Gateway
}
/// Create a network status from the supplied Decoder.
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let addressText = try container.decode(String.self, forKey: .ipv4Subnet)
ipv4Subnet = try CIDRv4(addressText)
let gatewayText = try container.decode(String.self, forKey: .ipv4Gateway)
ipv4Gateway = try IPv4Address(gatewayText)
}
/// Encode the network status to the supplied Encoder.
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(ipv4Subnet.description, forKey: .ipv4Subnet)
try container.encode(ipv4Gateway.description, forKey: .ipv4Gateway)
}
}
/// The configuration and runtime attributes for a network.
@@ -39,6 +39,7 @@ public final class ReservedVmnetNetwork: Network {
let network: vmnet_network_ref
let ipv4Subnet: CIDRv4
let ipv4Gateway: IPv4Address
let ipv6Subnet: CIDRv6
}
private let stateMutex: Mutex<State>
@@ -79,7 +80,11 @@ public final class ReservedVmnetNetwork: Network {
let networkInfo = try startNetwork(configuration: configuration, log: log)
let networkStatus = NetworkStatus(ipv4Subnet: networkInfo.ipv4Subnet, ipv4Gateway: networkInfo.ipv4Gateway)
let networkStatus = NetworkStatus(
ipv4Subnet: networkInfo.ipv4Subnet,
ipv4Gateway: networkInfo.ipv4Gateway,
ipv6Subnet: networkInfo.ipv6Subnet,
)
state.networkState = NetworkState.running(configuration, networkStatus)
state.network = networkInfo.network
}
@@ -102,10 +107,6 @@ public final class ReservedVmnetNetwork: Network {
]
)
// with the reservation API, subnet priority is CLI argument, UserDefault, auto
let defaultSubnet = try DefaultsStore.getOptional(key: .defaultSubnet).map { try CIDRv4($0) }
let subnet = configuration.ipv4Subnet ?? defaultSubnet
// set up the vmnet configuration
var status: vmnet_return_t = .VMNET_SUCCESS
guard let vmnetConfiguration = vmnet_network_configuration_create(vmnet.operating_modes_t.VMNET_SHARED_MODE, &status), status == .VMNET_SUCCESS else {
@@ -114,21 +115,42 @@ public final class ReservedVmnetNetwork: Network {
vmnet_network_configuration_disable_dhcp(vmnetConfiguration)
// set the subnet if the caller provided one
if let subnet {
let gateway = IPv4Address(subnet.lower.value + 1)
// subnet priority is CLI argument, UserDefault, auto
let defaultIpv4Subnet = try DefaultsStore.getOptional(key: .defaultSubnet).map { try CIDRv4($0) }
let ipv4Subnet = configuration.ipv4Subnet ?? defaultIpv4Subnet
let defaultIpv6Subnet = try DefaultsStore.getOptional(key: .defaultIPv6Subnet).map { try CIDRv6($0) }
let ipv6Subnet = configuration.ipv6Subnet ?? defaultIpv6Subnet
// set the IPv4 subnet if the caller provided one
if let ipv4Subnet {
let gateway = IPv4Address(ipv4Subnet.lower.value + 1)
var gatewayAddr = in_addr()
inet_pton(AF_INET, gateway.description, &gatewayAddr)
let mask = IPv4Address(subnet.prefix.prefixMask32)
let mask = IPv4Address(ipv4Subnet.prefix.prefixMask32)
var maskAddr = in_addr()
inet_pton(AF_INET, mask.description, &maskAddr)
log.info(
"configuring vmnet subnet",
metadata: ["cidr": "\(subnet)"]
"configuring vmnet IPv4 subnet",
metadata: ["cidr": "\(ipv4Subnet)"]
)
let status = vmnet_network_configuration_set_ipv4_subnet(vmnetConfiguration, &gatewayAddr, &maskAddr)
guard status == .VMNET_SUCCESS else {
throw ContainerizationError(.internalError, message: "failed to set subnet \(subnet) for network \(configuration.id)")
throw ContainerizationError(.internalError, message: "failed to set subnet \(ipv4Subnet) for IPv4 network \(configuration.id)")
}
}
// set the IPv6 network prefix if the caller provided one
if let ipv6Subnet {
let gateway = IPv6Address(ipv6Subnet.lower.value + 1)
var gatewayAddr = in6_addr()
inet_pton(AF_INET6, gateway.description, &gatewayAddr)
log.info(
"configuring vmnet IPv6 prefix",
metadata: ["cidr": "\(ipv6Subnet)"]
)
let status = vmnet_network_configuration_set_ipv6_prefix(vmnetConfiguration, &gatewayAddr, ipv6Subnet.prefix.length)
guard status == .VMNET_SUCCESS else {
throw ContainerizationError(.internalError, message: "failed to set prefix \(ipv6Subnet) for IPv6 network \(configuration.id)")
}
}
@@ -148,15 +170,33 @@ public final class ReservedVmnetNetwork: Network {
let runningSubnet = try CIDRv4(lower: lower, upper: upper)
let runningGateway = IPv4Address(runningSubnet.lower.value + 1)
var prefixAddr = in6_addr()
var prefixLength = UInt8(0)
vmnet_network_get_ipv6_prefix(network, &prefixAddr, &prefixLength)
guard let prefix = Prefix(length: prefixLength) else {
throw ContainerizationError(.internalError, message: "invalid IPv6 prefix length \(prefixLength) for network \(configuration.id)")
}
let prefixIpv6Bytes = withUnsafeBytes(of: prefixAddr.__u6_addr.__u6_addr8) {
Array($0)
}
let prefixIpv6Addr = IPv6Address(prefixIpv6Bytes)
let runningV6Subnet = try CIDRv6(prefixIpv6Addr, prefix: prefix)
log.info(
"started vmnet network",
metadata: [
"id": "\(configuration.id)",
"mode": "\(configuration.mode)",
"cidr": "\(runningSubnet)",
"cidrv6": "\(runningV6Subnet)",
]
)
return NetworkInfo(network: network, ipv4Subnet: runningSubnet, ipv4Gateway: runningGateway)
return NetworkInfo(
network: network,
ipv4Subnet: runningSubnet,
ipv4Gateway: runningGateway,
ipv6Subnet: runningV6Subnet,
)
}
}
@@ -131,9 +131,10 @@ public actor SandboxService {
// Dynamically configure the DNS nameserver from a network if no explicit configuration
if let dns = config.dns, dns.nameservers.isEmpty {
if let nameserver = try await self.getDefaultNameserver(attachmentConfigurations: config.networks) {
let defaultNameservers = try await self.getDefaultNameservers(attachmentConfigurations: config.networks)
if !defaultNameservers.isEmpty {
config.dns = ContainerConfiguration.DNSConfiguration(
nameservers: [nameserver],
nameservers: defaultNameservers,
domain: dns.domain,
searchDomains: dns.searchDomains,
options: dns.options
@@ -859,17 +860,17 @@ public actor SandboxService {
Self.configureInitialProcess(czConfig: &czConfig, config: config)
}
private func getDefaultNameserver(attachmentConfigurations: [AttachmentConfiguration]) async throws -> String? {
private func getDefaultNameservers(attachmentConfigurations: [AttachmentConfiguration]) async throws -> [String] {
for attachmentConfiguration in attachmentConfigurations {
let client = NetworkClient(id: attachmentConfiguration.network)
let state = try await client.state()
guard case .running(_, let status) = state else {
continue
}
return status.ipv4Gateway.description
return [status.ipv4Gateway.description]
}
return nil
return []
}
private static func configureInitialProcess(
+3 -1
View File
@@ -653,7 +653,7 @@ Creates a new network with the given name.
**Usage**
```bash
container network create [--label <label> ...] [--debug] <name>
container network create [--label <label> ...] [--subnet <subnet>] [--subnet-v6 <subnet-v6>] [--debug] <name>
```
**Arguments**
@@ -663,6 +663,8 @@ container network create [--label <label> ...] [--debug] <name>
**Options**
* `--label <label>`: Set metadata for a network
* `--subnet <subnet>`: Set the IPv4 subnet for a network (CIDR format, e.g., 192.168.100.0/24)
* `--subnet-v6 <subnet-v6>`: Set the IPv6 prefix for a network (CIDR format, e.g., fd00:1234::/64)
### `container network delete (rm)`
+28 -1
View File
@@ -286,6 +286,12 @@ This command creates a network named `foo`:
container network create foo
```
You can also specify custom IPv4 and IPv6 subnets when creating a network:
```bash
container network create foo --subnet 192.168.100.0/24 --subnet-v6 fd00:1234::/64
```
The `foo` network, the default network, and any other networks you create are isolated from one another. A container on one network has no connectivity to containers on other networks.
Run `container network list` to see the networks that exist:
@@ -319,6 +325,26 @@ container stop my-web-server
container network delete foo
```
Networks support both IPv4 and IPv6. When creating a network without explicit subnet options, the system uses default values if configured via system properties (see below), or automatically allocates subnets. The system validates that custom subnets don't overlap with existing networks.
## Configure default network subnets
You can customize the default IPv4 and IPv6 subnets used for new networks using system properties.
### Set default IPv4 subnet
```bash
container system property set network.subnet 192.168.100.1/24
```
### Set default IPv6 prefix
```bash
container system property set network.subnetv6 fd00:abcd::/64
```
These settings apply to networks created without explicit `--subnet` or `--subnet-v6` options.
## View container logs
The `container logs` command displays the output from your containerized application:
@@ -460,7 +486,8 @@ image.builder String ghcr.io/apple/container-builder-shim/... The image r
image.init String ghcr.io/apple/containerization/vminit... The image reference for the default initial filesystem image.
kernel.binaryPath String opt/kata/share/kata-containers/vmlinu... If the kernel URL is for an archive, the archive member pathname for the kernel file.
kernel.url String https://github.com/kata-containers/ka... The URL for the kernel file to install, or the URL for an archive containing the kernel file.
network.subnet String *undefined* Default subnet for IP allocation (used on macOS 15 only).
network.subnet String *undefined* Default subnet for IPv4 allocation.
network.subnetv6 String *undefined* Default IPv6 network prefix.
```
### Example: Disable Rosetta for builds