mirror of
https://github.com/apple/container.git
synced 2026-10-25 08:08:29 +00:00
- Closes #346. - This PR enables connecting host's localhost ports from containers. - It adds an option `--localhost <localhost>` to DNS create command, after which the packets heading ip address in container are redirected to localhost in host machine. Packet filter rule is added and deleted along with the creation and deletion of localhost domain.
149 lines
5.6 KiB
Swift
149 lines
5.6 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 ContainerizationError
|
|
import ContainerizationExtras
|
|
import Foundation
|
|
|
|
/// Functions for managing local DNS domains for containers.
|
|
public struct HostDNSResolver {
|
|
public static let defaultConfigPath = URL(filePath: "/etc/resolver")
|
|
|
|
// prefix used to mark our files as /etc/resolver/{prefix}{domainName}
|
|
public static let containerizationPrefix = "containerization."
|
|
public static let localhostOptionsRegex = #"options localhost:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"#
|
|
|
|
private let configURL: URL
|
|
|
|
public init(configURL: URL = Self.defaultConfigPath) {
|
|
self.configURL = configURL
|
|
}
|
|
|
|
/// Creates a DNS resolver configuration file for domain resolved by the application.
|
|
public func createDomain(name: String, localhost: IPAddress? = nil) throws {
|
|
let path = self.configURL.appending(path: "\(Self.containerizationPrefix)\(name)").path
|
|
let fm: FileManager = FileManager.default
|
|
|
|
if fm.fileExists(atPath: self.configURL.path) {
|
|
guard let isDir = try self.configURL.resourceValues(forKeys: [.isDirectoryKey]).isDirectory, isDir else {
|
|
throw ContainerizationError(.invalidState, message: "expected \(self.configURL.path) to be a directory, but found a file")
|
|
}
|
|
} else {
|
|
try fm.createDirectory(at: self.configURL, withIntermediateDirectories: true)
|
|
}
|
|
|
|
guard !fm.fileExists(atPath: path) else {
|
|
throw ContainerizationError(.exists, message: "domain \(name) already exists")
|
|
}
|
|
|
|
let dnsPort = localhost == nil ? "2053" : "1053"
|
|
let options =
|
|
localhost == nil
|
|
? ""
|
|
: HostDNSResolver.localhostOptionsRegex.replacingOccurrences(
|
|
of: #"\((.*?)\)"#, with: localhost!.description, options: .regularExpression)
|
|
let resolverText = """
|
|
domain \(name)
|
|
search \(name)
|
|
nameserver 127.0.0.1
|
|
port \(dnsPort)
|
|
\(options)
|
|
"""
|
|
|
|
try resolverText.write(toFile: path, atomically: true, encoding: .utf8)
|
|
}
|
|
|
|
/// Removes a DNS resolver configuration file for domain resolved by the application.
|
|
public func deleteDomain(name: String) throws -> IPAddress? {
|
|
let path = self.configURL.appending(path: "\(Self.containerizationPrefix)\(name)").path
|
|
let fm = FileManager.default
|
|
guard fm.fileExists(atPath: path) else {
|
|
throw ContainerizationError(.notFound, message: "domain \(name) at \(path) not found")
|
|
}
|
|
|
|
var localhost: IPAddress?
|
|
let content = try String(contentsOfFile: path, encoding: .utf8)
|
|
if let match = content.firstMatch(of: try Regex(HostDNSResolver.localhostOptionsRegex)) {
|
|
localhost = try? IPAddress(String(match[1].substring ?? ""))
|
|
}
|
|
|
|
do {
|
|
try fm.removeItem(atPath: path)
|
|
} catch {
|
|
throw ContainerizationError(.invalidState, message: "cannot delete domain (try sudo?)")
|
|
}
|
|
|
|
return localhost
|
|
}
|
|
|
|
/// Lists application-created local DNS domains.
|
|
public func listDomains() -> [String] {
|
|
let fm: FileManager = FileManager.default
|
|
guard
|
|
let resolverPaths = try? fm.contentsOfDirectory(
|
|
at: self.configURL,
|
|
includingPropertiesForKeys: [.isDirectoryKey]
|
|
)
|
|
else {
|
|
return []
|
|
}
|
|
|
|
return
|
|
resolverPaths
|
|
.filter { $0.lastPathComponent.starts(with: Self.containerizationPrefix) }
|
|
.compactMap { try? getDomainFromResolver(url: $0) }
|
|
.sorted()
|
|
}
|
|
|
|
/// Reinitializes the macOS DNS daemon.
|
|
public static func reinitialize() throws {
|
|
do {
|
|
let kill = Foundation.Process()
|
|
kill.executableURL = URL(fileURLWithPath: "/usr/bin/killall")
|
|
kill.arguments = ["-HUP", "mDNSResponder"]
|
|
|
|
let null = FileHandle.nullDevice
|
|
kill.standardOutput = null
|
|
kill.standardError = null
|
|
|
|
try kill.run()
|
|
kill.waitUntilExit()
|
|
let status = kill.terminationStatus
|
|
guard status == 0 else {
|
|
throw ContainerizationError(.internalError, message: "mDNSResponder restart failed with status \(status)")
|
|
}
|
|
}
|
|
}
|
|
|
|
private func getDomainFromResolver(url: URL) throws -> String? {
|
|
let text = try String(contentsOf: url, encoding: .utf8)
|
|
for line in text.components(separatedBy: .newlines) {
|
|
let trimmed = line.trimmingCharacters(in: .whitespaces)
|
|
let components = trimmed.split(whereSeparator: { $0.isWhitespace })
|
|
guard components.count == 2 else {
|
|
continue
|
|
}
|
|
guard components[0] == "domain" else {
|
|
continue
|
|
}
|
|
|
|
return String(components[1])
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|