Files
container/Sources/Services/ContainerNetworkService/NetworkService.swift
T
Danish Singh Sethi c909cb3a27 Add --mac-address flag to set custom MAC addresses for containers (#753)
- Closes #752.
- Currently, there is no way to specify a custom MAC address for a
container's network interface and the MAC address is auto-generated by
the system.
- Use Cases
  - **Network Testing**: Developers testing network-dependent applications
that need predictable MAC addresses
  - **License Management**: Running containerized software with MAC-based
license keys
  - **Network Automation**: Scripts and tools that expect specific MAC
addresses for configuration
  - **Debugging**: Consistent MAC addresses across container restarts for
easier troubleshooting
2025-10-30 16:26:06 -07:00

160 lines
5.8 KiB
Swift

//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import ContainerXPC
import ContainerizationError
import ContainerizationExtras
import Foundation
import Logging
public actor NetworkService: Sendable {
private let network: any Network
private let log: Logger?
private var allocator: AttachmentAllocator
/// Set up a network service for the specified network.
public init(
network: any Network,
log: Logger? = nil
) async throws {
let state = await network.state
guard case .running(_, let status) = state else {
throw ContainerizationError(.invalidState, message: "invalid network state - network \(state.id) must be running")
}
let subnet = try CIDRAddress(status.address)
let size = Int(subnet.upper.value - subnet.lower.value - 3)
self.allocator = try AttachmentAllocator(lower: subnet.lower.value + 2, size: size)
self.network = network
self.log = log
}
@Sendable
public func state(_ message: XPCMessage) async throws -> XPCMessage {
let reply = message.reply()
let state = await network.state
try reply.setState(state)
return reply
}
@Sendable
public func allocate(_ message: XPCMessage) async throws -> XPCMessage {
let state = await network.state
guard case .running(_, let status) = state else {
throw ContainerizationError(.invalidState, message: "invalid network state - network \(state.id) must be running")
}
let hostname = try message.hostname()
let macAddress = message.string(key: NetworkKeys.macAddress.rawValue)
let index = try await allocator.allocate(hostname: hostname)
let subnet = try CIDRAddress(status.address)
let ip = IPv4Address(fromValue: index)
let attachment = Attachment(
network: state.id,
hostname: hostname,
address: try CIDRAddress(ip, prefixLength: subnet.prefixLength).description,
gateway: status.gateway,
macAddress: macAddress
)
log?.info(
"allocated attachment",
metadata: [
"hostname": "\(hostname)",
"address": "\(attachment.address)",
"gateway": "\(attachment.gateway)",
"macAddress": "\(macAddress ?? "auto")",
])
let reply = message.reply()
try reply.setAttachment(attachment)
try network.withAdditionalData {
if let additionalData = $0 {
try reply.setAdditionalData(additionalData.underlying)
}
}
return reply
}
@Sendable
public func deallocate(_ message: XPCMessage) async throws -> XPCMessage {
let hostname = try message.hostname()
try await allocator.deallocate(hostname: hostname)
log?.info("released attachments", metadata: ["hostname": "\(hostname)"])
return message.reply()
}
@Sendable
public func lookup(_ message: XPCMessage) async throws -> XPCMessage {
let state = await network.state
guard case .running(_, let status) = state else {
throw ContainerizationError(.invalidState, message: "invalid network state - network \(state.id) must be running")
}
let hostname = try message.hostname()
let index = try await allocator.lookup(hostname: hostname)
let reply = message.reply()
guard let index else {
return reply
}
let address = IPv4Address(fromValue: index)
let subnet = try CIDRAddress(status.address)
let attachment = Attachment(
network: state.id,
hostname: hostname,
address: try CIDRAddress(address, prefixLength: subnet.prefixLength).description,
gateway: status.gateway
)
log?.debug(
"lookup attachment",
metadata: [
"hostname": "\(hostname)",
"address": "\(address)",
])
try reply.setAttachment(attachment)
return reply
}
@Sendable
public func disableAllocator(_ message: XPCMessage) async throws -> XPCMessage {
let success = await allocator.disableAllocator()
log?.info("attempted allocator disable", metadata: ["success": "\(success)"])
let reply = message.reply()
reply.setAllocatorDisabled(success)
return reply
}
}
extension XPCMessage {
fileprivate func setAdditionalData(_ additionalData: xpc_object_t) throws {
xpc_dictionary_set_value(self.underlying, NetworkKeys.additionalData.rawValue, additionalData)
}
fileprivate func setAllocatorDisabled(_ allocatorDisabled: Bool) {
self.set(key: NetworkKeys.allocatorDisabled.rawValue, value: allocatorDisabled)
}
fileprivate func setAttachment(_ attachment: Attachment) throws {
let data = try JSONEncoder().encode(attachment)
self.set(key: NetworkKeys.attachment.rawValue, value: data)
}
fileprivate func setState(_ state: NetworkState) throws {
let data = try JSONEncoder().encode(state)
self.set(key: NetworkKeys.state.rawValue, value: data)
}
}