Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
4 changes: 2 additions & 2 deletions Sources/ContainerClient/Core/ContainerConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down
30 changes: 28 additions & 2 deletions Sources/ContainerClient/Flags.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,32 @@ 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 Global: ParsableArguments {
public init() {}
Expand Down Expand Up @@ -151,8 +177,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
Expand Down
15 changes: 9 additions & 6 deletions Sources/ContainerClient/Utility.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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(".") {
Expand All @@ -232,18 +232,21 @@ 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
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, options: AttachmentOptions(hostname: containerId))
return AttachmentConfiguration(network: item.element.networkId, options: AttachmentOptions(hostname: containerId, ip: item.element.ip))
}
return AttachmentConfiguration(network: item.element, options: AttachmentOptions(hostname: fqdn ?? containerId))
return AttachmentConfiguration(network: item.element.networkId, options: AttachmentOptions(hostname: fqdn ?? containerId, ip: item.element.ip))
}
}
// if no networks specified, attach to the default network
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

if let staticIndex {
try allocator.reserve(staticIndex)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When trying to reserve gateway ip 192.168.64.1:

Error: internalError: "failed to bootstrap container" (cause: "internalError: "failed to bootstrap container 66336145-44ba-4e26-8954-39eecd7f73fa (cause: "unknown: "cannot create index using address 3232251905"")"")

When trying to reserve already allocated ip 192.168.64.12:

Error: internalError: "failed to bootstrap container" (cause: "internalError: "failed to bootstrap container b7bd2b0f-00db-4cd3-8970-fd55864d6089 (cause: "unknown: "cannot choose already-allocated address 3232251916"")"")

hostnames[hostname] = staticIndex
return staticIndex
}

let index = try allocator.allocate()
hostnames[hostname] = index

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
13 changes: 12 additions & 1 deletion Sources/Services/ContainerNetworkService/NetworkClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions Sources/Services/ContainerNetworkService/NetworkKeys.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ public enum NetworkKeys: String {
case hostname
case network
case state
case ip
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ public actor SandboxService {
for index in 0..<config.networks.count {
let network = config.networks[index]
let client = NetworkClient(id: network.network)
let (attachment, additionalData) = try await client.allocate(hostname: network.options.hostname)
let (attachment, additionalData) = try await client.allocate(hostname: network.options.hostname, ip: network.options.ip)
attachments.append(attachment)

let interface = try self.interfaceStrategy.toInterface(
Expand Down