Files
container/Sources/Services/Network/Client/NetworkClient.swift
T
J Logan b3eb70307a Add DNS aliases and subtree aliases for container network attachments (#2390)
- 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.
2026-10-14 10:11:00 -07:00

160 lines
5.8 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 ContainerResource
import ContainerXPC
import ContainerizationError
import ContainerizationExtras
import Foundation
/// A client for interacting with a single network.
public struct NetworkClient: Sendable {
static let label = "com.apple.container.network"
public static func machServiceLabel(id: String, plugin: String) -> String {
"\(Self.label).\(plugin).\(id)"
}
private var machServiceLabel: String {
Self.machServiceLabel(id: id, plugin: plugin)
}
let id: String
let plugin: String
/// Create a client for a network.
public init(id: String, plugin: String) {
self.id = id
self.plugin = plugin
}
}
// Runtime Methods
extension NetworkClient {
/// Open a persistent connection to the network helper.
///
/// The returned session should be reused for `allocate(on:)` calls. The
/// network helper automatically releases all allocations made over this
/// session when it closes.
public func connect() -> XPCClientSession {
createClient().openSession()
}
public func status() async throws -> NetworkStatus {
let request = XPCMessage(route: NetworkRoutes.status.rawValue)
let client = createClient()
let response = try await client.send(request)
let status = try response.status()
return status
}
/// Allocate a network attachment over an existing session.
///
/// Use `connect()` to obtain a session, then pass it here. The session
/// must remain open for the lifetime of the allocation; closing it
/// releases the allocation on the network helper automatically.
///
/// - Parameter aliases: Additional names that resolve to the attachment, in their normalized
/// string form.
public func allocate(
hostname: String,
macAddress: MACAddress? = nil,
aliases: [String] = [],
on session: XPCClientSession
) async throws -> (attachment: Attachment, additionalData: XPCMessage?) {
let request = XPCMessage(route: NetworkRoutes.allocate.rawValue)
request.set(key: NetworkKeys.hostname.rawValue, value: hostname)
if let macAddress = macAddress {
request.set(key: NetworkKeys.macAddress.rawValue, value: macAddress.description)
}
if !aliases.isEmpty {
try request.setAliases(aliases)
}
let response = try await session.send(request)
let attachment = try response.attachment()
let additionalData = response.additionalData()
return (attachment, additionalData)
}
/// Look up a name on the network.
///
/// The name can be the hostname of an attachment, an alias of it, or a name below a subtree alias.
///
/// - Returns: The attachment that the name resolves to and how the name matched, or nil if the
/// name does not resolve on this network.
public func lookup(name: String) async throws -> NetworkMatch? {
let request = XPCMessage(route: NetworkRoutes.lookup.rawValue)
request.set(key: NetworkKeys.name.rawValue, value: name)
let client = createClient()
let response = try await client.send(request)
guard let data = response.dataNoCopy(key: NetworkKeys.attachment.rawValue) else {
return nil
}
return NetworkMatch(
attachment: try JSONDecoder().decode(Attachment.self, from: data),
kind: try response.matchKind()
)
}
private func createClient() -> XPCClient {
XPCClient(service: machServiceLabel)
}
}
extension XPCMessage {
public func additionalData() -> XPCMessage? {
guard let additionalData = xpc_dictionary_get_dictionary(self.underlying, NetworkKeys.additionalData.rawValue) else {
return nil
}
return XPCMessage(object: additionalData)
}
public func attachment() throws -> Attachment {
let data = self.dataNoCopy(key: NetworkKeys.attachment.rawValue)
guard let data else {
throw ContainerizationError(.invalidArgument, message: "no network attachment snapshot data in message")
}
return try JSONDecoder().decode(Attachment.self, from: data)
}
public func hostname() throws -> String {
let hostname = self.string(key: NetworkKeys.hostname.rawValue)
guard let hostname else {
throw ContainerizationError(.invalidArgument, message: "no hostname data in message")
}
return hostname
}
public func name() throws -> String {
let name = self.string(key: NetworkKeys.name.rawValue)
guard let name else {
throw ContainerizationError(.invalidArgument, message: "no name data in message")
}
return name
}
public func status() throws -> NetworkStatus {
let data = self.dataNoCopy(key: NetworkKeys.status.rawValue)
guard let data else {
throw ContainerizationError(.invalidArgument, message: "no network snapshot data in message")
}
return try JSONDecoder().decode(NetworkStatus.self, from: data)
}
}