Files
container/Sources/Helpers/APIServer/LocalhostDNSHandler.swift
T
J Logan 4a8a8a6f89 Use Sendable DNS types. (#1269)
- Closes #1268.
- The types we were using weren't very usable with Swift 6 structured
concurrency.
- Implements just the subset of records that we use.
- Use notImplemented instead of formatError for unknown record types.
- Use pure actor for LocalhostDNSHandler now that we have sendable
types.
- Use DNSName as key for table lookups in LocalhostDNSHandler and
HostTableResolver.
- Utilize dot-suffixed domain names everywhere in the lookup chain.
- Huge thanks to @manojmahapatra and @katiewasnothere for their
  diligent and patient review :D
2026-03-19 10:58:28 -07:00

112 lines
3.9 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 ContainerAPIClient
import ContainerOS
import ContainerPersistence
import ContainerizationError
import ContainerizationExtras
import DNSServer
import Foundation
import Logging
import Synchronization
actor LocalhostDNSHandler: DNSHandler {
private let ttl: UInt32
private let watcher: DirectoryWatcher
private var dns: [DNSName: IPv4Address]
public init(resolversURL: URL = HostDNSResolver.defaultConfigPath, ttl: UInt32 = 5, log: Logger) {
self.ttl = ttl
self.watcher = DirectoryWatcher(directoryURL: resolversURL, log: log)
self.dns = [DNSName: IPv4Address]()
}
public func monitorResolvers() async {
await self.watcher.startWatching { [weak self] fileURLs in
var dns: [DNSName: IPv4Address] = [:]
let regex = try Regex(HostDNSResolver.localhostOptionsRegex)
for file in fileURLs.filter({ $0.lastPathComponent.starts(with: HostDNSResolver.containerizationPrefix) }) {
let content = try String(contentsOf: file, encoding: .utf8)
if let match = content.firstMatch(of: regex),
let ipv4 = (match[1].substring.flatMap { try? IPv4Address(String($0)) })
{
let name = String(file.lastPathComponent.dropFirst(HostDNSResolver.containerizationPrefix.count))
guard let dnsName = try? DNSName(name) else {
continue
}
dns[dnsName] = ipv4
}
}
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
}
}