//===----------------------------------------------------------------------===// // Copyright © 2026 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 ContainerNetworkClient import ContainerResource import ContainerXPC import ContainerizationError import ContainerizationExtras import DNSServer import Logging public actor DefaultNetworkService: NetworkService { private let network: any Network private let log: Logger private var allocator: AttachmentAllocator private var macAddresses: [UInt32: MACAddress] private var allocationsBySession: [XPCServerSession: [(hostname: String, index: UInt32)]] /// Set up a network service for the specified network. public init( network: any Network, log: Logger ) async throws { guard let status = await network.status else { throw ContainerizationError(.invalidState, message: "network \(network.id) must be running") } let subnet = status.ipv4Subnet let size = Int(subnet.upper.value - subnet.lower.value - 3) self.network = network self.log = log self.allocator = try AttachmentAllocator(lower: subnet.lower.value + 2, size: size) self.macAddresses = [:] self.allocationsBySession = [:] } @Sendable public func status() async throws -> NetworkStatus { guard let status = await network.status else { throw ContainerizationError(.invalidState, message: "network \(network.id) is not running") } return status } @Sendable public func allocate( hostname: String, macAddress: MACAddress?, aliases: [String], session: XPCServerSession ) async throws -> (attachment: Attachment, additionalData: XPCMessage?) { log.debug("enter", metadata: ["func": "\(#function)"]) defer { log.debug("exit", metadata: ["func": "\(#function)"]) } guard let status = await network.status else { throw ContainerizationError(.invalidState, message: "network \(network.id) must be running") } let macAddress = macAddress ?? MACAddress((UInt64.random(in: 0...UInt64.max) & 0x0cff_ffff_ffff) | 0xf200_0000_0000) // The aliases come over XPC, so check them against the same rules as the API server does, // instead of trusting that the caller did. let rules: [DNSMatchRule] do { let name = try? DNSName(AttachmentOptions.canonicalHostname(hostname)) rules = try AliasPolicy.validate(aliases: aliases, hostname: name, network: network.id) } catch { throw ContainerizationError(.invalidArgument, message: "invalid aliases for \(hostname): \(error)") } let index = try await allocator.allocate(hostname: hostname, aliases: rules) let ipv6Address = try status.ipv6Subnet .map { try CIDRv6(macAddress.ipv6Address(network: $0.lower), prefix: $0.prefix) } let ip = IPv4Address(index) let attachment = Attachment( network: network.id, hostname: hostname, ipv4Address: try CIDRv4(ip, prefix: status.ipv4Subnet.prefix), ipv4Gateway: status.ipv4Gateway, ipv6Address: ipv6Address, macAddress: macAddress, variant: network.variant ) log.info( "allocated attachment", metadata: [ "hostname": "\(hostname)", "ipv4Address": "\(attachment.ipv4Address)", "ipv4Gateway": "\(attachment.ipv4Gateway)", "ipv6Address": "\(attachment.ipv6Address?.description ?? "unavailable")", "macAddress": "\(attachment.macAddress?.description ?? "unspecified")", ]) var additionalData: XPCMessage? try network.withAdditionalData { additionalData = $0 } macAddresses[index] = macAddress let isNewSession = allocationsBySession[session] == nil allocationsBySession[session, default: []].append((hostname: hostname, index: index)) if isNewSession { await session.onDisconnect { [weak self] in await self?.releaseSession(session) } } return (attachment: attachment, additionalData: additionalData) } private func releaseSession(_ session: XPCServerSession) async { guard let allocations = allocationsBySession.removeValue(forKey: session) else { return } for allocation in allocations { _ = try? await allocator.deallocate(hostname: allocation.hostname) macAddresses.removeValue(forKey: allocation.index) } log.info("released session", metadata: ["allocations": "\(allocations.count)"]) } @Sendable public func lookup(name: String) async throws -> NetworkMatch? { log.debug("enter", metadata: ["func": "\(#function)"]) defer { log.debug("exit", metadata: ["func": "\(#function)"]) } guard let status = await network.status else { throw ContainerizationError(.invalidState, message: "network \(network.id) must be running") } guard let match = try await allocator.match(name: name) else { return nil } // Invariant: hostname -> index if and only if index -> MAC address guard let macAddress = macAddresses[match.index] else { return nil } let attachment = try makeAttachment(hostname: match.hostname, index: match.index, macAddress: macAddress, status: status) switch match.kind { case .hostname: return NetworkMatch(attachment: attachment, kind: .hostname) case .alias: return NetworkMatch(attachment: attachment, kind: .alias) case .subtree(let baseLabelCount): return NetworkMatch(attachment: attachment, kind: .subtree(baseLabelCount: baseLabelCount)) } } private func makeAttachment(hostname: String, index: UInt32, macAddress: MACAddress, status: NetworkStatus) throws -> Attachment { let address = IPv4Address(index) let subnet = status.ipv4Subnet let ipv4Address = try CIDRv4(address, prefix: subnet.prefix) let ipv6Address = try status.ipv6Subnet .map { try CIDRv6(macAddress.ipv6Address(network: $0.lower), prefix: $0.prefix) } let attachment = Attachment( network: network.id, hostname: hostname, ipv4Address: ipv4Address, ipv4Gateway: status.ipv4Gateway, ipv6Address: ipv6Address, macAddress: macAddress, variant: network.variant ) log.debug( "lookup attachment", metadata: [ "hostname": "\(hostname)", "address": "\(address)", ]) return attachment } }