From 5cf16c4bb917c6e789f0282520b0216a4b593927 Mon Sep 17 00:00:00 2001 From: John Logan Date: Sat, 20 Dec 2025 18:33:43 -0800 Subject: [PATCH 1/2] Adds network IPv6 configuration. - 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. --- Package.resolved | 6 +- Package.swift | 2 +- .../Network/NetworkCreate.swift | 10 ++- .../System/Property/PropertySet.swift | 5 ++ .../ContainerPersistence/DefaultsStore.swift | 10 ++- .../NetworkVmnetHelper+Start.swift | 17 +++-- .../Networks/NetworksService.swift | 22 ++++++- .../AllocationOnlyVmnetNetwork.swift | 7 ++- .../NetworkConfiguration.swift | 11 +++- .../NetworkState.swift | 33 +++------- .../ReservedVmnetNetwork.swift | 63 +++++++++++++++---- .../SandboxService.swift | 11 ++-- docs/command-reference.md | 4 +- docs/how-to.md | 29 ++++++++- 14 files changed, 171 insertions(+), 59 deletions(-) diff --git a/Package.resolved b/Package.resolved index 800a3c89..4f4d14ef 100644 --- a/Package.resolved +++ b/Package.resolved @@ -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" } }, { diff --git a/Package.swift b/Package.swift index 16f023b8..1d518e73 100644 --- a/Package.swift +++ b/Package.swift @@ -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", diff --git a/Sources/ContainerCommands/Network/NetworkCreate.swift b/Sources/ContainerCommands/Network/NetworkCreate.swift index 60c6a81e..6d557a0a 100644 --- a/Sources/ContainerCommands/Network/NetworkCreate.swift +++ b/Sources/ContainerCommands/Network/NetworkCreate.swift @@ -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) } diff --git a/Sources/ContainerCommands/System/Property/PropertySet.swift b/Sources/ContainerCommands/System/Property/PropertySet.swift index 3a8da98f..2266827c 100644 --- a/Sources/ContainerCommands/System/Property/PropertySet.swift +++ b/Sources/ContainerCommands/System/Property/PropertySet.swift @@ -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) } } } diff --git a/Sources/ContainerPersistence/DefaultsStore.swift b/Sources/ContainerPersistence/DefaultsStore.swift index e855b725..86d4490f 100644 --- a/Sources/ContainerPersistence/DefaultsStore.swift +++ b/Sources/ContainerPersistence/DefaultsStore.swift @@ -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" } diff --git a/Sources/Helpers/NetworkVmnet/NetworkVmnetHelper+Start.swift b/Sources/Helpers/NetworkVmnet/NetworkVmnetHelper+Start.swift index d2fcaa94..edde525a 100644 --- a/Sources/Helpers/NetworkVmnet/NetworkVmnetHelper+Start.swift +++ b/Sources/Helpers/NetworkVmnet/NetworkVmnetHelper+Start.swift @@ -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) diff --git a/Sources/Services/ContainerAPIService/Networks/NetworksService.swift b/Sources/Services/ContainerAPIService/Networks/NetworksService.swift index aaf66e28..41efc556 100644 --- a/Sources/Services/ContainerAPIService/Networks/NetworksService.swift +++ b/Sources/Services/ContainerAPIService/Networks/NetworksService.swift @@ -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), diff --git a/Sources/Services/ContainerNetworkService/AllocationOnlyVmnetNetwork.swift b/Sources/Services/ContainerNetworkService/AllocationOnlyVmnetNetwork.swift index ffacdb0e..befb1d0b 100644 --- a/Sources/Services/ContainerNetworkService/AllocationOnlyVmnetNetwork.swift +++ b/Sources/Services/ContainerNetworkService/AllocationOnlyVmnetNetwork.swift @@ -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: [ diff --git a/Sources/Services/ContainerNetworkService/NetworkConfiguration.swift b/Sources/Services/ContainerNetworkService/NetworkConfiguration.swift index 829637d6..dae355d7 100644 --- a/Sources/Services/ContainerNetworkService/NetworkConfiguration.swift +++ b/Sources/Services/ContainerNetworkService/NetworkConfiguration.swift @@ -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) } diff --git a/Sources/Services/ContainerNetworkService/NetworkState.swift b/Sources/Services/ContainerNetworkService/NetworkState.swift index a6f3730d..4ee90544 100644 --- a/Sources/Services/ContainerNetworkService/NetworkState.swift +++ b/Sources/Services/ContainerNetworkService/NetworkState.swift @@ -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. diff --git a/Sources/Services/ContainerNetworkService/ReservedVmnetNetwork.swift b/Sources/Services/ContainerNetworkService/ReservedVmnetNetwork.swift index 45832d11..61b80a3d 100644 --- a/Sources/Services/ContainerNetworkService/ReservedVmnetNetwork.swift +++ b/Sources/Services/ContainerNetworkService/ReservedVmnetNetwork.swift @@ -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 @@ -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,41 @@ 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)"] + 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)") + } + } + + 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 subnet", + 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 +169,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, + ) } } diff --git a/Sources/Services/ContainerSandboxService/SandboxService.swift b/Sources/Services/ContainerSandboxService/SandboxService.swift index f291d12a..7915726d 100644 --- a/Sources/Services/ContainerSandboxService/SandboxService.swift +++ b/Sources/Services/ContainerSandboxService/SandboxService.swift @@ -129,9 +129,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 @@ -857,17 +858,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( diff --git a/docs/command-reference.md b/docs/command-reference.md index 4bb9c16c..a4dfca8f 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -653,7 +653,7 @@ Creates a new network with the given name. **Usage** ```bash -container network create [--label