mirror of
https://github.com/apple/container.git
synced 2026-10-17 11:39:14 +00:00
- Closes #457. - A container can now declare additional DNS names for a network attachment with a repeatable `alias` property on `--network`, including subtree aliases that match every name below a domain. - With the default domain `test`, this container answers to `gateway.test` by its name, to `api.test` by the exact alias, and to `web.gateway.test`, `a.b.gateway.test` and every other name below `gateway.test` by the subtree alias. The motivating case is a gateway container in front of a local Kubernetes cluster that answers for `*.gateway.test`, with an ingress controller routing on the `Host` header. - An alias is an exact name, or a name with a leading dot that matches every name below it. A name without a dot gets the default domain, as container names do. A subtree alias must be below a name the same attachment owns, and exact names are unique across all containers, so a container cannot claim another container's namespace. The rules for one attachment are checked when a container is created, again when a stored container starts, and again by the network helper when it registers the aliases. See `docs/architecture/dns.md` for the design, the invariants and the security considerations.
186 lines
7.4 KiB
Swift
186 lines
7.4 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// 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
|
|
}
|
|
}
|