From d249605bb1b7a511a667e46f60d88bff065d4f4c Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sat, 11 Oct 2025 20:44:20 +0300 Subject: [PATCH 1/7] Specify ip in --network --- .../Core/ContainerConfiguration.swift | 4 +-- Sources/ContainerClient/Flags.swift | 31 +++++++++++++++++-- Sources/ContainerClient/Utility.swift | 20 +++++++----- .../AttachmentAllocator.swift | 8 ++++- .../AttachmentConfiguration.swift | 5 ++- .../NetworkClient.swift | 13 +++++++- .../ContainerNetworkService/NetworkKeys.swift | 1 + .../NetworkService.swift | 8 ++++- .../SandboxService.swift | 2 +- 9 files changed, 76 insertions(+), 16 deletions(-) diff --git a/Sources/ContainerClient/Core/ContainerConfiguration.swift b/Sources/ContainerClient/Core/ContainerConfiguration.swift index d4de4d43..10e1ae9d 100644 --- a/Sources/ContainerClient/Core/ContainerConfiguration.swift +++ b/Sources/ContainerClient/Core/ContainerConfiguration.swift @@ -88,8 +88,8 @@ public struct ContainerConfiguration: Sendable, Codable { do { networks = try container.decode([AttachmentConfiguration].self, forKey: .networks) } catch { - let networkIds = try container.decode([String].self, forKey: .networks) - networks = try Utility.getAttachmentConfigurations(containerId: id, networkIds: networkIds) + let networkArgs = try container.decode([NetworkArg].self, forKey: .networks) + networks = try Utility.getAttachmentConfigurations(containerId: id, networks: networkArgs) } } else { networks = [] diff --git a/Sources/ContainerClient/Flags.swift b/Sources/ContainerClient/Flags.swift index 3a01a2a3..cd5a9808 100644 --- a/Sources/ContainerClient/Flags.swift +++ b/Sources/ContainerClient/Flags.swift @@ -18,6 +18,33 @@ import ArgumentParser import ContainerizationError import Foundation +public struct NetworkArg: ExpressibleByArgument, Decodable { + var networkId: String + var ip: String? + // TODO throw error on invalid args + var invalidArgs: [String] = [] + + public init?(argument: String) { + let networkParts = argument.split(separator: ":", maxSplits: 1) + self.networkId = String(networkParts[0]) + if networkParts.count == 2 { + let args = networkParts[1].split(separator: ",") + for arg in args { + let parts = arg.split(separator: "=", maxSplits: 1) + guard parts.count == 2 else { + self.invalidArgs.append(String(arg)) + continue + } + if parts[0] == "ip" { + self.ip = String(parts[1]) + } else { + self.invalidArgs.append(String(arg)) + } + } + } + } +} + public struct Flags { public struct Global: ParsableArguments { public init() {} @@ -151,8 +178,8 @@ public struct Flags { @Option(name: .long, help: "Use the specified name as the container ID") public var name: String? - @Option(name: [.customLong("network")], help: "Attach the container to a network") - public var networks: [String] = [] + @Option(name: [.customLong("network")], help: "Attach the container to a network (format: network_id[:ip=<>])") + public var networks: [NetworkArg] = [] @Flag(name: [.customLong("no-dns")], help: "Do not configure DNS in the container") public var dnsDisabled = false diff --git a/Sources/ContainerClient/Utility.swift b/Sources/ContainerClient/Utility.swift index 383021d0..3e05a167 100644 --- a/Sources/ContainerClient/Utility.swift +++ b/Sources/ContainerClient/Utility.swift @@ -180,7 +180,7 @@ public struct Utility { config.virtualization = management.virtualization - config.networks = try getAttachmentConfigurations(containerId: config.id, networkIds: management.networks) + config.networks = try getAttachmentConfigurations(containerId: config.id, networks: management.networks) for attachmentConfiguration in config.networks { let network: NetworkState = try await ClientNetwork.get(id: attachmentConfiguration.network) guard case .running(_, _) = network else { @@ -217,7 +217,7 @@ public struct Utility { return (config, kernel) } - static func getAttachmentConfigurations(containerId: String, networkIds: [String]) throws -> [AttachmentConfiguration] { + static func getAttachmentConfigurations(containerId: String, networks: [NetworkArg]) throws -> [AttachmentConfiguration] { // make an FQDN for the first interface let fqdn: String? if !containerId.contains(".") { @@ -232,18 +232,24 @@ public struct Utility { fqdn = "\(containerId)." } - guard networkIds.isEmpty else { + guard networks.isEmpty else { // networks may only be specified for macOS 26+ guard #available(macOS 26, *) else { throw ContainerizationError(.invalidArgument, message: "non-default network configuration requires macOS 26 or newer") } // attach the first network using the fqdn, and the rest using just the container ID - return networkIds.enumerated().map { item in - guard item.offset == 0 else { - return AttachmentConfiguration(network: item.element, options: AttachmentOptions(hostname: containerId)) + return networks.enumerated().map { item in + if item.offset == 0 { + return AttachmentConfiguration( + network: item.element.networkId, + options: AttachmentOptions(hostname: fqdn ?? containerId, ip: item.element.ip) + ) } - return AttachmentConfiguration(network: item.element, options: AttachmentOptions(hostname: fqdn ?? containerId)) + return AttachmentConfiguration( + network: item.element.networkId, + options: AttachmentOptions(hostname: containerId, ip: item.element.ip) + ) } } // if no networks specified, attach to the default network diff --git a/Sources/Services/ContainerNetworkService/AttachmentAllocator.swift b/Sources/Services/ContainerNetworkService/AttachmentAllocator.swift index ede611b2..613de02c 100644 --- a/Sources/Services/ContainerNetworkService/AttachmentAllocator.swift +++ b/Sources/Services/ContainerNetworkService/AttachmentAllocator.swift @@ -29,12 +29,18 @@ actor AttachmentAllocator { } /// Allocate a network address for a host. - func allocate(hostname: String) async throws -> UInt32 { + func allocate(hostname: String, staticIndex: UInt32? = nil) async throws -> UInt32 { // Client is responsible for ensuring two containers don't use same hostname, so provide existing IP if hostname exists if let index = hostnames[hostname] { return index } + // TODO: Implement static index allocation properly + if let staticIndex { + hostnames[hostname] = staticIndex + return staticIndex + } + let index = try allocator.allocate() hostnames[hostname] = index diff --git a/Sources/Services/ContainerNetworkService/AttachmentConfiguration.swift b/Sources/Services/ContainerNetworkService/AttachmentConfiguration.swift index bf9f1dd9..6f03145c 100644 --- a/Sources/Services/ContainerNetworkService/AttachmentConfiguration.swift +++ b/Sources/Services/ContainerNetworkService/AttachmentConfiguration.swift @@ -33,7 +33,10 @@ public struct AttachmentOptions: Codable, Sendable { /// The hostname associated with the attachment. public let hostname: String - public init(hostname: String) { + public let ip: String? + + public init(hostname: String, ip: String? = nil) { self.hostname = hostname + self.ip = ip } } diff --git a/Sources/Services/ContainerNetworkService/NetworkClient.swift b/Sources/Services/ContainerNetworkService/NetworkClient.swift index cde0b154..b10fd638 100644 --- a/Sources/Services/ContainerNetworkService/NetworkClient.swift +++ b/Sources/Services/ContainerNetworkService/NetworkClient.swift @@ -46,9 +46,12 @@ extension NetworkClient { return state } - public func allocate(hostname: String) async throws -> (attachment: Attachment, additionalData: XPCMessage?) { + public func allocate(hostname: String, ip: String?) async throws -> (attachment: Attachment, additionalData: XPCMessage?) { let request = XPCMessage(route: NetworkRoutes.allocate.rawValue) request.set(key: NetworkKeys.hostname.rawValue, value: hostname) + if let ip { + request.set(key: NetworkKeys.ip.rawValue, value: ip) + } let client = createClient() @@ -120,6 +123,14 @@ extension XPCMessage { return hostname } + func ip() throws -> String? { + let ip = self.string(key: NetworkKeys.ip.rawValue) + guard let ip else { + return nil + } + return ip + } + func state() throws -> NetworkState { let data = self.dataNoCopy(key: NetworkKeys.state.rawValue) guard let data else { diff --git a/Sources/Services/ContainerNetworkService/NetworkKeys.swift b/Sources/Services/ContainerNetworkService/NetworkKeys.swift index ccbeb8db..f86efbac 100644 --- a/Sources/Services/ContainerNetworkService/NetworkKeys.swift +++ b/Sources/Services/ContainerNetworkService/NetworkKeys.swift @@ -21,4 +21,5 @@ public enum NetworkKeys: String { case hostname case network case state + case ip } diff --git a/Sources/Services/ContainerNetworkService/NetworkService.swift b/Sources/Services/ContainerNetworkService/NetworkService.swift index 9267dfe8..757920fb 100644 --- a/Sources/Services/ContainerNetworkService/NetworkService.swift +++ b/Sources/Services/ContainerNetworkService/NetworkService.swift @@ -59,7 +59,13 @@ public actor NetworkService: Sendable { } let hostname = try message.hostname() - let index = try await allocator.allocate(hostname: hostname) + var index: UInt32 + if let staticIpString = try message.ip() { + let staticIp = try IPv4Address(staticIpString) + index = try await allocator.allocate(hostname: hostname, staticIndex: staticIp.value) + } else { + index = try await allocator.allocate(hostname: hostname) + } let subnet = try CIDRAddress(status.address) let ip = IPv4Address(fromValue: index) let attachment = Attachment( diff --git a/Sources/Services/ContainerSandboxService/SandboxService.swift b/Sources/Services/ContainerSandboxService/SandboxService.swift index 35d8a1c8..103f07b0 100644 --- a/Sources/Services/ContainerSandboxService/SandboxService.swift +++ b/Sources/Services/ContainerSandboxService/SandboxService.swift @@ -142,7 +142,7 @@ public actor SandboxService { for index in 0.. Date: Sun, 12 Oct 2025 11:18:58 +0300 Subject: [PATCH 2/7] Validate NetworkArg unhandled arguments --- Sources/ContainerClient/Flags.swift | 1 - Sources/ContainerClient/Utility.swift | 17 +++++++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/Sources/ContainerClient/Flags.swift b/Sources/ContainerClient/Flags.swift index cd5a9808..55a9888d 100644 --- a/Sources/ContainerClient/Flags.swift +++ b/Sources/ContainerClient/Flags.swift @@ -21,7 +21,6 @@ import Foundation public struct NetworkArg: ExpressibleByArgument, Decodable { var networkId: String var ip: String? - // TODO throw error on invalid args var invalidArgs: [String] = [] public init?(argument: String) { diff --git a/Sources/ContainerClient/Utility.swift b/Sources/ContainerClient/Utility.swift index 3e05a167..b1a6acd5 100644 --- a/Sources/ContainerClient/Utility.swift +++ b/Sources/ContainerClient/Utility.swift @@ -239,17 +239,14 @@ public struct Utility { } // attach the first network using the fqdn, and the rest using just the container ID - return networks.enumerated().map { item in - if item.offset == 0 { - return AttachmentConfiguration( - network: item.element.networkId, - options: AttachmentOptions(hostname: fqdn ?? containerId, ip: item.element.ip) - ) + return try networks.enumerated().map { item in + guard item.element.invalidArgs.isEmpty else { + throw ContainerizationError(.invalidArgument, message: "invalid network arguments \(item.element.networkId): \(item.element.invalidArgs.joined(separator: ", "))") + } + guard item.offset == 0 else { + return AttachmentConfiguration(network: item.element.networkId, options: AttachmentOptions(hostname: containerId, ip: item.element.ip)) } - return AttachmentConfiguration( - network: item.element.networkId, - options: AttachmentOptions(hostname: containerId, ip: item.element.ip) - ) + return AttachmentConfiguration(network: item.element.networkId, options: AttachmentOptions(hostname: fqdn ?? containerId, ip: item.element.ip)) } } // if no networks specified, attach to the default network From 462598c5ad51a14d61e5e86715dcb5f10220e872 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sun, 12 Oct 2025 11:40:02 +0300 Subject: [PATCH 3/7] Reserve static ip from allocator --- .../Services/ContainerNetworkService/AttachmentAllocator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Services/ContainerNetworkService/AttachmentAllocator.swift b/Sources/Services/ContainerNetworkService/AttachmentAllocator.swift index 613de02c..ad6476d5 100644 --- a/Sources/Services/ContainerNetworkService/AttachmentAllocator.swift +++ b/Sources/Services/ContainerNetworkService/AttachmentAllocator.swift @@ -35,8 +35,8 @@ actor AttachmentAllocator { return index } - // TODO: Implement static index allocation properly if let staticIndex { + try allocator.reserve(staticIndex) hostnames[hostname] = staticIndex return staticIndex } From 1893a7b290cf75286bb8782183afcdbcf8570878 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sun, 12 Oct 2025 12:06:53 +0300 Subject: [PATCH 4/7] NetworkArg inside Flags --- .../Core/ContainerConfiguration.swift | 2 +- Sources/ContainerClient/Flags.swift | 45 ++++++++++--------- Sources/ContainerClient/Utility.swift | 2 +- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/Sources/ContainerClient/Core/ContainerConfiguration.swift b/Sources/ContainerClient/Core/ContainerConfiguration.swift index 10e1ae9d..975d3544 100644 --- a/Sources/ContainerClient/Core/ContainerConfiguration.swift +++ b/Sources/ContainerClient/Core/ContainerConfiguration.swift @@ -88,7 +88,7 @@ public struct ContainerConfiguration: Sendable, Codable { do { networks = try container.decode([AttachmentConfiguration].self, forKey: .networks) } catch { - let networkArgs = try container.decode([NetworkArg].self, forKey: .networks) + let networkArgs = try container.decode([Flags.NetworkArg].self, forKey: .networks) networks = try Utility.getAttachmentConfigurations(containerId: id, networks: networkArgs) } } else { diff --git a/Sources/ContainerClient/Flags.swift b/Sources/ContainerClient/Flags.swift index 55a9888d..1ebe2e33 100644 --- a/Sources/ContainerClient/Flags.swift +++ b/Sources/ContainerClient/Flags.swift @@ -18,33 +18,34 @@ import ArgumentParser import ContainerizationError import Foundation -public struct NetworkArg: ExpressibleByArgument, Decodable { - var networkId: String - var ip: String? - var invalidArgs: [String] = [] - - public init?(argument: String) { - let networkParts = argument.split(separator: ":", maxSplits: 1) - self.networkId = String(networkParts[0]) - if networkParts.count == 2 { - let args = networkParts[1].split(separator: ",") - for arg in args { - let parts = arg.split(separator: "=", maxSplits: 1) - guard parts.count == 2 else { - self.invalidArgs.append(String(arg)) - continue - } - if parts[0] == "ip" { - self.ip = String(parts[1]) - } else { - self.invalidArgs.append(String(arg)) + +public struct Flags { + public struct NetworkArg: ExpressibleByArgument, Decodable { + var networkId: String + var ip: String? + var invalidArgs: [String] = [] + + public init?(argument: String) { + let networkParts = argument.split(separator: ":", maxSplits: 1) + self.networkId = String(networkParts[0]) + if networkParts.count == 2 { + let args = networkParts[1].split(separator: ",") + for arg in args { + let parts = arg.split(separator: "=", maxSplits: 1) + guard parts.count == 2 else { + self.invalidArgs.append(String(arg)) + continue + } + if parts[0] == "ip" { + self.ip = String(parts[1]) + } else { + self.invalidArgs.append(String(arg)) + } } } } } -} -public struct Flags { public struct Global: ParsableArguments { public init() {} diff --git a/Sources/ContainerClient/Utility.swift b/Sources/ContainerClient/Utility.swift index b1a6acd5..5ee61d7b 100644 --- a/Sources/ContainerClient/Utility.swift +++ b/Sources/ContainerClient/Utility.swift @@ -217,7 +217,7 @@ public struct Utility { return (config, kernel) } - static func getAttachmentConfigurations(containerId: String, networks: [NetworkArg]) throws -> [AttachmentConfiguration] { + static func getAttachmentConfigurations(containerId: String, networks: [Flags.NetworkArg]) throws -> [AttachmentConfiguration] { // make an FQDN for the first interface let fqdn: String? if !containerId.contains(".") { From 9371caa068fc5dd67dd1c47d5522e0544e4a98ee Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sun, 12 Oct 2025 12:17:05 +0300 Subject: [PATCH 5/7] Update command reference --- docs/command-reference.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/command-reference.md b/docs/command-reference.md index 0631c215..1826c311 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -41,7 +41,7 @@ container run [OPTIONS] IMAGE [COMMAND] [ARG...] * `-l, --label