chore: import upstream snapshot with attribution
container project - merge build / Invoke build (push) Successful in 1s
container project - merge build / Invoke build (push) Successful in 1s
This commit is contained in:
@@ -0,0 +1,398 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 ArgumentParser
|
||||
import ContainerAPIClient
|
||||
import ContainerAPIService
|
||||
import ContainerLog
|
||||
import ContainerPersistence
|
||||
import ContainerPlugin
|
||||
import ContainerResource
|
||||
import ContainerXPC
|
||||
import ContainerizationExtras
|
||||
import DNSServer
|
||||
import Foundation
|
||||
import Logging
|
||||
import SystemPackage
|
||||
|
||||
extension APIServer {
|
||||
struct Start: AsyncParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: "start",
|
||||
abstract: "Start helper for the API server"
|
||||
)
|
||||
|
||||
static let listenAddress = "127.0.0.1"
|
||||
static let localhostDNSPort = 1053
|
||||
static let dnsPort = 2053
|
||||
|
||||
@Flag(name: .long, help: "Enable debug logging")
|
||||
var debug = false
|
||||
|
||||
var appRoot = ApplicationRoot.path
|
||||
|
||||
var installRoot = InstallRoot.path
|
||||
|
||||
var logRoot = LogRoot.path
|
||||
|
||||
func run() async throws {
|
||||
let containerSystemConfig: ContainerSystemConfig = try await ConfigurationLoader.load()
|
||||
let commandName = APIServer._commandName
|
||||
let logPath = logRoot.map { $0.appending(FilePath.Component("\(commandName).log") ?? "unknown") }
|
||||
let log = ServiceLogger.bootstrap(category: "APIServer", debug: debug, logPath: logPath)
|
||||
log.info("starting helper", metadata: ["name": "\(commandName)"])
|
||||
defer {
|
||||
log.info("stopping helper", metadata: ["name": "\(commandName)"])
|
||||
}
|
||||
|
||||
do {
|
||||
log.info("configuring XPC server")
|
||||
var routes = [XPCRoute: XPCServer.RouteHandler]()
|
||||
let pluginLoader = try initializePluginLoader(log: log)
|
||||
|
||||
try await initializePlugins(pluginLoader: pluginLoader, log: log, routes: &routes, debug: debug)
|
||||
let containersService = try initializeContainersService(
|
||||
pluginLoader: pluginLoader,
|
||||
containerSystemConfig: containerSystemConfig,
|
||||
log: log,
|
||||
routes: &routes
|
||||
)
|
||||
let networkService = try await initializeNetworksService(
|
||||
pluginLoader: pluginLoader,
|
||||
containersService: containersService,
|
||||
containerSystemConfig: containerSystemConfig,
|
||||
log: log,
|
||||
routes: &routes
|
||||
)
|
||||
await containersService.setNetworksService(networkService)
|
||||
initializeHealthCheckService(log: log, routes: &routes)
|
||||
try initializeKernelService(log: log, routes: &routes)
|
||||
let volumesService = try await initializeVolumeService(containersService: containersService, log: log, routes: &routes)
|
||||
try initializeDiskUsageService(
|
||||
containersService: containersService,
|
||||
volumesService: volumesService,
|
||||
log: log,
|
||||
routes: &routes
|
||||
)
|
||||
|
||||
let server = XPCServer(
|
||||
identifier: "com.apple.container.apiserver",
|
||||
routes: routes.reduce(
|
||||
into: [String: XPCServer.RouteHandler](),
|
||||
{
|
||||
$0[$1.key.rawValue] = $1.value
|
||||
}), log: log)
|
||||
|
||||
await withTaskGroup(of: Result<Void, Error>.self) { group in
|
||||
group.addTask {
|
||||
log.info("starting XPC server")
|
||||
do {
|
||||
try await server.listen()
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
// start up host table DNS
|
||||
group.addTask {
|
||||
let hostsResolver = ContainerDNSHandler(networkService: networkService)
|
||||
let nxDomainResolver = NxDomainResolver()
|
||||
let compositeResolver = CompositeResolver(handlers: [hostsResolver, nxDomainResolver])
|
||||
let hostsQueryValidator = StandardQueryValidator(handler: compositeResolver)
|
||||
let dnsServer: DNSServer = DNSServer(handler: hostsQueryValidator, log: log)
|
||||
log.info(
|
||||
"starting DNS resolver for container hostnames",
|
||||
metadata: [
|
||||
"host": "\(Self.listenAddress)",
|
||||
"port": "\(Self.dnsPort)",
|
||||
]
|
||||
)
|
||||
do {
|
||||
try await dnsServer.run(host: Self.listenAddress, port: Self.dnsPort)
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// start up realhost DNS
|
||||
group.addTask {
|
||||
do {
|
||||
let localhostResolver = LocalhostDNSHandler(log: log)
|
||||
await localhostResolver.monitorResolvers()
|
||||
|
||||
let nxDomainResolver = NxDomainResolver()
|
||||
let compositeResolver = CompositeResolver(handlers: [localhostResolver, nxDomainResolver])
|
||||
let hostsQueryValidator = StandardQueryValidator(handler: compositeResolver)
|
||||
let dnsServer: DNSServer = DNSServer(handler: hostsQueryValidator, log: log)
|
||||
log.info(
|
||||
"starting DNS resolver for localhost",
|
||||
metadata: [
|
||||
"host": "\(Self.listenAddress)",
|
||||
"port": "\(Self.localhostDNSPort)",
|
||||
]
|
||||
)
|
||||
try await dnsServer.run(host: Self.listenAddress, port: Self.localhostDNSPort)
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
for await result in group {
|
||||
switch result {
|
||||
case .success():
|
||||
continue
|
||||
case .failure(let error):
|
||||
log.error("API server task failed: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
log.error(
|
||||
"helper failed",
|
||||
metadata: [
|
||||
"name": "\(commandName)",
|
||||
"error": "\(error)",
|
||||
])
|
||||
APIServer.exit(withError: error)
|
||||
}
|
||||
}
|
||||
|
||||
private func initializePluginLoader(log: Logger) throws -> PluginLoader {
|
||||
log.info(
|
||||
"initializing plugin loader",
|
||||
metadata: [
|
||||
"installRoot": "\(installRoot.string)"
|
||||
])
|
||||
|
||||
// TODO: Remove when we convert PluginLoader to FilePath
|
||||
let installRootURL = URL(fileURLWithPath: installRoot.string)
|
||||
let pluginsURL = PluginLoader.userPluginsDir(installRoot: installRootURL)
|
||||
log.info("detecting user plugins directory", metadata: ["path": "\(pluginsURL.path(percentEncoded: false))"])
|
||||
var directoryExists: ObjCBool = false
|
||||
_ = FileManager.default.fileExists(atPath: pluginsURL.path, isDirectory: &directoryExists)
|
||||
let userPluginsURL = directoryExists.boolValue ? pluginsURL : nil
|
||||
|
||||
// plugins built into the application installed as a Unix-like application
|
||||
let installRootPluginsPath =
|
||||
installRoot
|
||||
.appending(FilePath.Component("libexec"))
|
||||
.appending(FilePath.Component("container"))
|
||||
.appending(FilePath.Component("plugins"))
|
||||
let installRootPluginsURL = URL(fileURLWithPath: installRootPluginsPath.string)
|
||||
|
||||
let pluginDirectories = [
|
||||
userPluginsURL,
|
||||
installRootPluginsURL,
|
||||
].compactMap { $0 }
|
||||
|
||||
let pluginFactories: [PluginFactory] = [
|
||||
DefaultPluginFactory(logger: log),
|
||||
AppBundlePluginFactory(logger: log),
|
||||
]
|
||||
|
||||
for pluginDirectory in pluginDirectories {
|
||||
log.info("discovered plugin directory", metadata: ["path": "\(pluginDirectory.path(percentEncoded: false))"])
|
||||
}
|
||||
|
||||
let appRootURL = URL(fileURLWithPath: appRoot.string)
|
||||
return try PluginLoader(
|
||||
appRoot: appRootURL,
|
||||
installRoot: installRootURL,
|
||||
logRoot: logRoot,
|
||||
pluginDirectories: pluginDirectories,
|
||||
pluginFactories: pluginFactories,
|
||||
log: log
|
||||
)
|
||||
}
|
||||
|
||||
// First load all of the plugins we can find. Then just expose
|
||||
// the handlers for clients to do whatever they want.
|
||||
private func initializePlugins(
|
||||
pluginLoader: PluginLoader,
|
||||
log: Logger,
|
||||
routes: inout [XPCRoute: XPCServer.RouteHandler],
|
||||
debug: Bool = false
|
||||
) async throws {
|
||||
log.info("initializing plugins")
|
||||
|
||||
let bootPlugins = pluginLoader.findPlugins().filter { $0.shouldBoot }
|
||||
|
||||
let service = PluginsService(pluginLoader: pluginLoader, log: log)
|
||||
try await service.loadAll(bootPlugins, debug: debug)
|
||||
|
||||
let harness = PluginsHarness(service: service, log: log)
|
||||
routes[XPCRoute.pluginGet] = XPCServer.route(harness.get)
|
||||
routes[XPCRoute.pluginList] = XPCServer.route(harness.list)
|
||||
routes[XPCRoute.pluginLoad] = XPCServer.route(harness.load)
|
||||
routes[XPCRoute.pluginUnload] = XPCServer.route(harness.unload)
|
||||
routes[XPCRoute.pluginRestart] = XPCServer.route(harness.restart)
|
||||
}
|
||||
|
||||
private func initializeHealthCheckService(log: Logger, routes: inout [XPCRoute: XPCServer.RouteHandler]) {
|
||||
log.info("initializing health check service")
|
||||
|
||||
// TODO: Remove when we convert HealthCheckHarness to FilePath
|
||||
let installRootURL = URL(fileURLWithPath: installRoot.string)
|
||||
let appRootURL = URL(fileURLWithPath: appRoot.string)
|
||||
let svc = HealthCheckHarness(
|
||||
appRoot: appRootURL,
|
||||
installRoot: installRootURL,
|
||||
logRoot: logRoot,
|
||||
log: log
|
||||
)
|
||||
routes[XPCRoute.ping] = XPCServer.route(svc.ping)
|
||||
}
|
||||
|
||||
private func initializeKernelService(log: Logger, routes: inout [XPCRoute: XPCServer.RouteHandler]) throws {
|
||||
log.info("initializing kernel service")
|
||||
|
||||
// TODO: Remove when we convert KernelService to FilePath
|
||||
let appRootURL = URL(fileURLWithPath: appRoot.string)
|
||||
let svc = try KernelService(log: log, appRoot: appRootURL)
|
||||
let harness = KernelHarness(service: svc, log: log)
|
||||
routes[XPCRoute.installKernel] = XPCServer.route(harness.install)
|
||||
routes[XPCRoute.getDefaultKernel] = XPCServer.route(harness.getDefaultKernel)
|
||||
}
|
||||
|
||||
private func initializeContainersService(
|
||||
pluginLoader: PluginLoader,
|
||||
containerSystemConfig: ContainerSystemConfig,
|
||||
log: Logger,
|
||||
routes: inout [XPCRoute: XPCServer.RouteHandler]
|
||||
) throws -> ContainersService {
|
||||
log.info("initializing containers service")
|
||||
|
||||
// TODO: Remove when we convert ContainersService to FilePath
|
||||
let appRootURL = URL(fileURLWithPath: appRoot.string)
|
||||
let service = try ContainersService(
|
||||
appRoot: appRootURL,
|
||||
pluginLoader: pluginLoader,
|
||||
containerSystemConfig: containerSystemConfig,
|
||||
log: log,
|
||||
debugHelpers: debug
|
||||
)
|
||||
let harness = ContainersHarness(service: service, log: log)
|
||||
|
||||
routes[XPCRoute.containerList] = XPCServer.route(harness.list)
|
||||
routes[XPCRoute.containerCreate] = XPCServer.route(harness.create)
|
||||
routes[XPCRoute.containerDelete] = XPCServer.route(harness.delete)
|
||||
routes[XPCRoute.containerLogs] = XPCServer.route(harness.logs)
|
||||
routes[XPCRoute.containerBootstrap] = XPCServer.route(harness.bootstrap)
|
||||
routes[XPCRoute.containerDial] = XPCServer.route(harness.dial)
|
||||
routes[XPCRoute.containerStop] = XPCServer.route(harness.stop)
|
||||
routes[XPCRoute.containerStartProcess] = XPCServer.route(harness.startProcess)
|
||||
routes[XPCRoute.containerCreateProcess] = XPCServer.route(harness.createProcess)
|
||||
routes[XPCRoute.containerResize] = XPCServer.route(harness.resize)
|
||||
routes[XPCRoute.containerWait] = XPCServer.route(harness.wait)
|
||||
routes[XPCRoute.containerKill] = XPCServer.route(harness.kill)
|
||||
routes[XPCRoute.containerStats] = XPCServer.route(harness.stats)
|
||||
routes[XPCRoute.containerDiskUsage] = XPCServer.route(harness.diskUsage)
|
||||
routes[XPCRoute.containerCopyIn] = XPCServer.route(harness.copyIn)
|
||||
routes[XPCRoute.containerCopyOut] = XPCServer.route(harness.copyOut)
|
||||
routes[XPCRoute.containerExport] = XPCServer.route(harness.export)
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
private func initializeNetworksService(
|
||||
pluginLoader: PluginLoader,
|
||||
containersService: ContainersService,
|
||||
containerSystemConfig: ContainerSystemConfig,
|
||||
log: Logger,
|
||||
routes: inout [XPCRoute: XPCServer.RouteHandler]
|
||||
) async throws -> NetworksService {
|
||||
log.info("initializing networks service")
|
||||
|
||||
let resourceRoot = appRoot.appending(FilePath.Component("networks"))
|
||||
let defaultNetworkConfig = try NetworkConfiguration(
|
||||
name: NetworkClient.defaultNetworkName,
|
||||
mode: .nat,
|
||||
ipv4Subnet: containerSystemConfig.network.subnet,
|
||||
ipv6Subnet: containerSystemConfig.network.subnetv6,
|
||||
labels: try .init([ResourceLabelKeys.role: ResourceRoleValues.builtin]),
|
||||
plugin: "container-network-vmnet"
|
||||
)
|
||||
let service = try await NetworksService(
|
||||
pluginLoader: pluginLoader,
|
||||
resourceRoot: resourceRoot,
|
||||
containersService: containersService,
|
||||
defaultNetworkConfiguration: defaultNetworkConfig,
|
||||
log: log,
|
||||
debugHelpers: debug
|
||||
)
|
||||
|
||||
let defaultNetwork = try await service.list()
|
||||
.filter { $0.isBuiltin }
|
||||
.first
|
||||
if defaultNetwork == nil {
|
||||
// FIXME: default network should be configurable elsewhere
|
||||
_ = try await service.create(configuration: defaultNetworkConfig)
|
||||
}
|
||||
|
||||
let harness = NetworksHarness(service: service, log: log)
|
||||
|
||||
if #available(macOS 26, *) {
|
||||
routes[XPCRoute.networkCreate] = XPCServer.route(harness.create)
|
||||
}
|
||||
routes[XPCRoute.networkList] = XPCServer.route(harness.list)
|
||||
routes[XPCRoute.networkDelete] = XPCServer.route(harness.delete)
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
private func initializeVolumeService(
|
||||
containersService: ContainersService,
|
||||
log: Logger,
|
||||
routes: inout [XPCRoute: XPCServer.RouteHandler]
|
||||
) async throws -> VolumesService {
|
||||
log.info("initializing volume service")
|
||||
|
||||
let resourceRoot = appRoot.appending(FilePath.Component("volumes"))
|
||||
let service = try await VolumesService(resourceRoot: resourceRoot, containersService: containersService, log: log)
|
||||
let harness = VolumesHarness(service: service, log: log)
|
||||
|
||||
routes[XPCRoute.volumeCreate] = XPCServer.route(harness.create)
|
||||
routes[XPCRoute.volumeDelete] = XPCServer.route(harness.delete)
|
||||
routes[XPCRoute.volumeList] = XPCServer.route(harness.list)
|
||||
routes[XPCRoute.volumeInspect] = XPCServer.route(harness.inspect)
|
||||
routes[XPCRoute.volumeDiskUsage] = XPCServer.route(harness.diskUsage)
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
private func initializeDiskUsageService(
|
||||
containersService: ContainersService,
|
||||
volumesService: VolumesService,
|
||||
log: Logger,
|
||||
routes: inout [XPCRoute: XPCServer.RouteHandler]
|
||||
) throws {
|
||||
log.info("initializing disk usage service")
|
||||
|
||||
let service = DiskUsageService(
|
||||
containersService: containersService,
|
||||
volumesService: volumesService,
|
||||
log: log
|
||||
)
|
||||
let harness = DiskUsageHarness(service: service, log: log)
|
||||
|
||||
routes[XPCRoute.systemDiskUsage] = XPCServer.route(harness.get)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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 ArgumentParser
|
||||
import ContainerVersion
|
||||
|
||||
@main
|
||||
struct APIServer: AsyncParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: "container-apiserver",
|
||||
abstract: "Container management API server",
|
||||
version: ReleaseVersion.singleLine(appName: "container-apiserver"),
|
||||
subcommands: [Start.self],
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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 ContainerAPIService
|
||||
import ContainerizationExtras
|
||||
import DNSServer
|
||||
|
||||
/// Handler that uses table lookup to resolve hostnames.
|
||||
struct ContainerDNSHandler: DNSHandler {
|
||||
private let networkService: NetworksService
|
||||
private let ttl: UInt32
|
||||
|
||||
public init(networkService: NetworksService, ttl: UInt32 = 5) {
|
||||
self.networkService = networkService
|
||||
self.ttl = ttl
|
||||
}
|
||||
|
||||
public func answer(query: Message) async throws -> Message? {
|
||||
guard let question = query.questions.first else {
|
||||
return nil
|
||||
}
|
||||
let record: ResourceRecord?
|
||||
switch question.type {
|
||||
case ResourceRecordType.host:
|
||||
record = try await answerHost(question: question)
|
||||
case ResourceRecordType.host6:
|
||||
let result = try await answerHost6(question: question)
|
||||
if result.record == nil && result.hostnameExists {
|
||||
// Return NODATA (noError with empty answers) when hostname exists but has no IPv6.
|
||||
// This is required because musl libc has issues when A record exists but AAAA returns NXDOMAIN.
|
||||
// musl treats NXDOMAIN on AAAA as "domain doesn't exist" and fails DNS resolution entirely.
|
||||
// NODATA correctly indicates "no IPv6 address available, but domain exists".
|
||||
return Message(
|
||||
id: query.id,
|
||||
type: .response,
|
||||
returnCode: .noError,
|
||||
questions: query.questions,
|
||||
answers: []
|
||||
)
|
||||
}
|
||||
record = result.record
|
||||
default:
|
||||
return Message(
|
||||
id: query.id,
|
||||
type: .response,
|
||||
returnCode: .notImplemented,
|
||||
questions: query.questions,
|
||||
answers: []
|
||||
)
|
||||
}
|
||||
|
||||
guard let record else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Message(
|
||||
id: query.id,
|
||||
type: .response,
|
||||
returnCode: .noError,
|
||||
questions: query.questions,
|
||||
answers: [record]
|
||||
)
|
||||
}
|
||||
|
||||
private func answerHost(question: Question) async throws -> ResourceRecord? {
|
||||
guard let ipAllocation = try await networkService.lookup(hostname: question.name) else {
|
||||
return nil
|
||||
}
|
||||
let ipv4 = ipAllocation.ipv4Address.address.description
|
||||
guard let ip = try? IPv4Address(ipv4) else {
|
||||
throw DNSResolverError.serverError("failed to parse IP address: \(ipv4)")
|
||||
}
|
||||
|
||||
return HostRecord<IPv4Address>(name: question.name, ttl: ttl, ip: ip)
|
||||
}
|
||||
|
||||
private func answerHost6(question: Question) async throws -> (record: ResourceRecord?, hostnameExists: Bool) {
|
||||
guard let ipAllocation = try await networkService.lookup(hostname: question.name) else {
|
||||
return (nil, false)
|
||||
}
|
||||
guard let ipv6Address = ipAllocation.ipv6Address else {
|
||||
return (nil, true)
|
||||
}
|
||||
let ipv6 = ipv6Address.address.description
|
||||
guard let ip = try? IPv6Address(ipv6) else {
|
||||
throw DNSResolverError.serverError("failed to parse IPv6 address: \(ipv6)")
|
||||
}
|
||||
|
||||
return (HostRecord<IPv6Address>(name: question.name, ttl: ttl, ip: ip), true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 ContainerAPIClient
|
||||
import ContainerOS
|
||||
import ContainerPersistence
|
||||
import ContainerizationError
|
||||
import ContainerizationExtras
|
||||
import DNSServer
|
||||
import Foundation
|
||||
import Logging
|
||||
import Synchronization
|
||||
import SystemPackage
|
||||
|
||||
actor LocalhostDNSHandler: DNSHandler {
|
||||
private let ttl: UInt32
|
||||
private let watcher: DirectoryWatcher
|
||||
|
||||
private var dns: [DNSName: IPv4Address]
|
||||
|
||||
public init(configPath: FilePath = HostDNSResolver.defaultConfigPath, ttl: UInt32 = 5, log: Logger) {
|
||||
self.ttl = ttl
|
||||
|
||||
self.watcher = DirectoryWatcher(directoryPath: configPath, log: log)
|
||||
self.dns = [DNSName: IPv4Address]()
|
||||
}
|
||||
|
||||
public func monitorResolvers() async {
|
||||
await self.watcher.startWatching { [weak self] filePaths in
|
||||
var dns: [DNSName: IPv4Address] = [:]
|
||||
let regex = try Regex(HostDNSResolver.localhostOptionsRegex)
|
||||
|
||||
for file in filePaths.filter({
|
||||
$0.lastComponent?.string.starts(with: HostDNSResolver.containerizationPrefix) == true
|
||||
}) {
|
||||
let content = try String(contentsOfFile: file.string, encoding: .utf8)
|
||||
|
||||
if let match = content.firstMatch(of: regex),
|
||||
let ipv4 = (match[1].substring.flatMap { try? IPv4Address(String($0)) })
|
||||
{
|
||||
guard let lastName = file.lastComponent?.string else {
|
||||
continue
|
||||
}
|
||||
let name = String(lastName.dropFirst(HostDNSResolver.containerizationPrefix.count))
|
||||
guard let dnsName = try? DNSName(name) else {
|
||||
continue
|
||||
}
|
||||
dns[dnsName] = ipv4
|
||||
}
|
||||
}
|
||||
if let self {
|
||||
Task { await self.updateDNS(dns) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func answer(query: Message) async throws -> Message? {
|
||||
guard let question = query.questions.first else {
|
||||
return nil
|
||||
}
|
||||
let n = question.name.hasSuffix(".") ? String(question.name.dropLast()) : question.name
|
||||
let key = try DNSName(labels: n.isEmpty ? [] : n.split(separator: ".", omittingEmptySubsequences: false).map(String.init))
|
||||
var record: ResourceRecord?
|
||||
switch question.type {
|
||||
case ResourceRecordType.host:
|
||||
if let ip = dns[key] {
|
||||
record = HostRecord<IPv4Address>(name: question.name, ttl: ttl, ip: ip)
|
||||
}
|
||||
case ResourceRecordType.host6:
|
||||
guard dns[key] != nil else {
|
||||
return nil
|
||||
}
|
||||
return Message(
|
||||
id: query.id,
|
||||
type: .response,
|
||||
returnCode: .noError,
|
||||
questions: query.questions,
|
||||
answers: []
|
||||
)
|
||||
default:
|
||||
return Message(
|
||||
id: query.id,
|
||||
type: .response,
|
||||
returnCode: .notImplemented,
|
||||
questions: query.questions,
|
||||
answers: []
|
||||
)
|
||||
}
|
||||
|
||||
guard let record else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Message(
|
||||
id: query.id,
|
||||
type: .response,
|
||||
returnCode: .noError,
|
||||
questions: query.questions,
|
||||
answers: [record]
|
||||
)
|
||||
}
|
||||
|
||||
private func updateDNS(_ dns: [DNSName: IPv4Address]) {
|
||||
self.dns = dns
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user