//===----------------------------------------------------------------------===// // 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.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) } } }