Remove network variant computation from API server. (#1814)

- Closes #1812.
- The network plugin is the source of truth for the variant, if any,
that applies to the network. Resolving a missing variant configuration
option in the API server can create a situation where the variant the
runtime uses for interface selection is incorrect.
- Adds serial suites trait to tests to see whether it helps current CI
issues.

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

## Motivation and Context
Fixes a flaw in our interface strategy logic.

## Testing
- [x] Tested locally
- [ ] Added/updated tests
- [ ] Added/updated docs
This commit is contained in:
J Logan
2026-06-25 14:06:45 -07:00
committed by GitHub
parent 137b3bdaf0
commit 4a6084cc95
9 changed files with 33 additions and 25 deletions
@@ -33,6 +33,8 @@ public struct Attachment: Codable, Sendable {
public let macAddress: MACAddress?
/// The MTU for the network interface.
public let mtu: UInt32?
/// The network plugin variant, used by the runtime to select an interface strategy.
public let variant: String?
public init(
network: String,
@@ -41,7 +43,8 @@ public struct Attachment: Codable, Sendable {
ipv4Gateway: IPv4Address,
ipv6Address: CIDRv6?,
macAddress: MACAddress?,
mtu: UInt32? = nil
mtu: UInt32? = nil,
variant: String? = nil
) {
self.network = network
self.hostname = hostname
@@ -50,6 +53,7 @@ public struct Attachment: Codable, Sendable {
self.ipv6Address = ipv6Address
self.macAddress = macAddress
self.mtu = mtu
self.variant = variant
}
enum CodingKeys: String, CodingKey {
@@ -60,6 +64,7 @@ public struct Attachment: Codable, Sendable {
case ipv6Address
case macAddress
case mtu
case variant
// TODO: retain for deserialization compatibility for now, remove later
case address
case gateway
@@ -85,6 +90,7 @@ public struct Attachment: Codable, Sendable {
ipv6Address = try container.decodeIfPresent(CIDRv6.self, forKey: .ipv6Address)
macAddress = try container.decodeIfPresent(MACAddress.self, forKey: .macAddress)
mtu = try container.decodeIfPresent(UInt32.self, forKey: .mtu)
variant = try container.decodeIfPresent(String.self, forKey: .variant)
}
/// Encode the configuration to the supplied Encoder.
@@ -98,5 +104,6 @@ public struct Attachment: Codable, Sendable {
try container.encodeIfPresent(ipv6Address, forKey: .ipv6Address)
try container.encodeIfPresent(macAddress, forKey: .macAddress)
try container.encodeIfPresent(mtu, forKey: .mtu)
try container.encodeIfPresent(variant, forKey: .variant)
}
}
@@ -422,10 +422,10 @@ public actor ContainersService {
var networkBootstrapInfos = [NetworkBootstrapInfo]()
for n in config.networks {
guard let (plugin, options) = try await self.networksService?.pluginConfiguration(id: n.network) else {
throw ContainerizationError(.internalError, message: "failed to get plugin configuration for network \(n.network)")
guard let plugin = try await self.networksService?.plugin(for: n.network) else {
throw ContainerizationError(.internalError, message: "failed to get plugin for network \(n.network)")
}
networkBootstrapInfos.append(NetworkBootstrapInfo(plugin: plugin, options: options))
networkBootstrapInfos.append(NetworkBootstrapInfo(plugin: plugin))
}
do {
@@ -314,19 +314,11 @@ public actor NetworksService {
}
}
public func pluginConfiguration(id: String) throws -> (plugin: String, options: [String: String]) {
public func plugin(for id: String) throws -> String {
guard let serviceState = serviceStates[id] else {
throw ContainerizationError(.notFound, message: "no network for id \(id)")
}
var options = serviceState.configuration.options
if options["variant"] == nil {
if #available(macOS 26, *) {
options["variant"] = "reserved"
} else {
options["variant"] = "allocationOnly"
}
}
return (plugin: serviceState.configuration.plugin, options: options)
return serviceState.configuration.plugin
}
private static func getClient(configuration: NetworkConfiguration) throws -> ContainerNetworkClient.NetworkClient {
@@ -77,7 +77,8 @@ public actor DefaultNetworkService: NetworkService {
ipv4Address: try CIDRv4(ip, prefix: status.ipv4Subnet.prefix),
ipv4Gateway: status.ipv4Gateway,
ipv6Address: ipv6Address,
macAddress: macAddress
macAddress: macAddress,
variant: network.variant
)
log.info(
"allocated attachment",
@@ -146,7 +147,8 @@ public actor DefaultNetworkService: NetworkService {
ipv4Address: ipv4Address,
ipv4Gateway: status.ipv4Gateway,
ipv6Address: ipv6Address,
macAddress: macAddress
macAddress: macAddress,
variant: network.variant
)
log.debug(
"lookup attachment",
@@ -22,6 +22,12 @@ public protocol Network: Sendable {
/// The network's identifier.
var id: String { get }
/// An operational hint passed back to the runtime in the allocate response.
/// Together with the plugin name, the runtime uses this to select the appropriate
/// interface strategy for the sandbox. A `nil` value indicates that the plugin
/// has only a single, default variant.
nonisolated var variant: String? { get }
/// The network's runtime status. `nil` before ``start()`` completes.
var status: NetworkStatus? { get async }
@@ -50,6 +50,8 @@ public actor AllocationOnlyVmnetNetwork: Network {
public nonisolated var id: String { configuration.id }
public nonisolated var variant: String? { "allocationOnly" }
public var status: NetworkStatus? { _status }
public nonisolated func withAdditionalData(_ handler: (XPCMessage?) throws -> Void) throws {
@@ -63,6 +63,8 @@ public final class ReservedVmnetNetwork: ContainerNetworkServer.Network {
public nonisolated var id: String { configuration.id }
public nonisolated var variant: String? { "reserved" }
public var status: NetworkStatus? {
stateMutex.withLock { $0.status }
}
@@ -17,16 +17,12 @@
import ContainerResource
/// Plugin info passed from the API server in the sandbox bootstrap message so the
/// runtime can connect to the correct network helper and configure the interface.
/// runtime can connect to the correct network helper.
public struct NetworkBootstrapInfo: Codable, Sendable {
/// The network plugin name identifying which network helper to contact.
public let plugin: String
/// Plugin-specific options, including `variant` which selects the interface strategy.
public let options: [String: String]
public init(plugin: String, options: [String: String] = [:]) {
public init(plugin: String) {
self.plugin = plugin
self.options = options
}
}
@@ -193,13 +193,14 @@ public actor RuntimeService {
ipv4Gateway: attachment.ipv4Gateway,
ipv6Address: attachment.ipv6Address,
macAddress: attachment.macAddress,
mtu: mtu
mtu: mtu,
variant: attachment.variant
)
}
guard let iStrategy = self.interfaceStrategies[NetworkInterfaceKey(plugin: info.plugin, variant: info.options["variant"])] else {
guard let iStrategy = self.interfaceStrategies[NetworkInterfaceKey(plugin: info.plugin, variant: attachment.variant)] else {
throw ContainerizationError(
.internalError,
message: "no available interface strategy for network \(attachment.network), plugin=\(info.plugin) variant=\(info.options["variant"] ?? "nil")")
message: "no available interface strategy for network \(attachment.network), plugin=\(info.plugin) variant=\(attachment.variant ?? "nil")")
}
let interface = try iStrategy.toInterface(
attachment: attachment,