Merge commit from fork

* Use DNSName for input

* Use pqdn instead of fqdn.

* Update Sources/DNSServer/Records/DNSName.swift

Co-authored-by: J Logan <john_logan@apple.com>

* Simplify description

* Add test

* Make fmt

---------

Co-authored-by: jwhur <jaewon_hur@apple.com>
Co-authored-by: jwhur <57657645+JaewonHur@users.noreply.github.com>
Co-authored-by: J Logan <john_logan@apple.com>
This commit is contained in:
Mike Drob
2026-04-30 09:33:26 -05:00
committed by GitHub
co-authored by jwhur jwhur J Logan
parent fd2de35440
commit f9899013fd
10 changed files with 66 additions and 34 deletions
+1
View File
@@ -205,6 +205,7 @@ let package = Package(
"ContainerPlugin",
"ContainerResource",
"ContainerXPC",
"DNSServer",
"TerminalProgress",
],
path: "Sources/Services/ContainerAPIService/Client"
@@ -19,6 +19,7 @@ import ContainerAPIClient
import ContainerPersistence
import ContainerizationError
import ContainerizationExtras
import DNSServer
import Foundation
extension Application {
@@ -48,6 +49,10 @@ extension Application {
}
}
guard let domainName = try? DNSName(domainName) else {
throw ContainerizationError(.invalidArgument, message: "invalid domain name: \(domainName)")
}
let resolver: HostDNSResolver = HostDNSResolver()
do {
try resolver.createDomain(name: domainName, localhost: localhostIP)
@@ -67,7 +72,7 @@ extension Application {
throw error
}
}
print(domainName)
print(domainName.pqdn)
if localhostIP != nil {
do {
@@ -18,6 +18,7 @@ import ArgumentParser
import ContainerAPIClient
import ContainerizationError
import ContainerizationExtras
import DNSServer
import Foundation
extension Application {
@@ -37,6 +38,10 @@ extension Application {
public init() {}
public func run() async throws {
guard let domainName = try? DNSName(domainName) else {
throw ContainerizationError(.invalidArgument, message: "invalid domain name: \(domainName)")
}
let resolver = HostDNSResolver()
var localhostIP: IPAddress?
do {
@@ -52,7 +57,7 @@ extension Application {
}
guard let localhostIP else {
print(domainName)
print(domainName.pqdn)
return
}
@@ -66,7 +71,7 @@ extension Application {
} catch {
throw ContainerizationError(.invalidState, message: "failed loading pf rules")
}
print(domainName)
print(domainName.pqdn)
}
}
}
@@ -16,6 +16,7 @@
import ArgumentParser
import ContainerAPIClient
import DNSServer
import Foundation
extension Application {
@@ -42,7 +43,7 @@ extension Application {
let domains = resolver.listDomains()
try Output.render(
json: domains,
json: domains.map { $0.pqdn },
display: domains.map { PrintableDomain($0) },
format: format, quiet: quiet
)
@@ -51,9 +52,9 @@ extension Application {
}
private struct PrintableDomain: ListDisplayable {
let domain: String
let domain: DNSName
init(_ domain: String) {
init(_ domain: DNSName) {
self.domain = domain
}
@@ -62,10 +63,10 @@ private struct PrintableDomain: ListDisplayable {
}
var tableRow: [String] {
[domain]
[domain.pqdn]
}
var quietValue: String {
domain
domain.pqdn
}
}
+6 -1
View File
@@ -91,7 +91,12 @@ public struct DNSName: Sendable, Hashable, CustomStringConvertible {
/// The fully-qualified domain name with trailing dot.
public var description: String {
labels.isEmpty ? "." : labels.joined(separator: ".") + "."
labels.joined(separator: ".") + "."
}
/// The partially-qualified domain name, which is the FQDN less the trailing dot.
public var pqdn: String {
labels.joined(separator: ".")
}
/// Serialize this name into the buffer at the given offset.
@@ -16,6 +16,7 @@
import ContainerizationError
import ContainerizationExtras
import DNSServer
import Foundation
/// Functions for managing local DNS domains for containers.
@@ -33,7 +34,9 @@ public struct HostDNSResolver {
}
/// Creates a DNS resolver configuration file for domain resolved by the application.
public func createDomain(name: String, localhost: IPAddress? = nil) throws {
public func createDomain(name: DNSName, localhost: IPAddress? = nil) throws {
let name = name.pqdn
let path = self.configURL.appending(path: "\(Self.containerizationPrefix)\(name)").path
let fm: FileManager = FileManager.default
@@ -67,7 +70,9 @@ public struct HostDNSResolver {
}
/// Removes a DNS resolver configuration file for domain resolved by the application.
public func deleteDomain(name: String) throws -> IPAddress? {
public func deleteDomain(name: DNSName) throws -> IPAddress? {
let name = name.pqdn
let path = self.configURL.appending(path: "\(Self.containerizationPrefix)\(name)").path
let fm = FileManager.default
guard fm.fileExists(atPath: path) else {
@@ -90,7 +95,7 @@ public struct HostDNSResolver {
}
/// Lists application-created local DNS domains.
public func listDomains() -> [String] {
public func listDomains() -> [DNSName] {
let fm: FileManager = FileManager.default
guard
let resolverPaths = try? fm.contentsOfDirectory(
@@ -105,7 +110,7 @@ public struct HostDNSResolver {
resolverPaths
.filter { $0.lastPathComponent.starts(with: Self.containerizationPrefix) }
.compactMap { try? getDomainFromResolver(url: $0) }
.sorted()
.sorted { a, b in a.pqdn < b.pqdn }
}
/// Reinitializes the macOS DNS daemon.
@@ -128,7 +133,7 @@ public struct HostDNSResolver {
}
}
private func getDomainFromResolver(url: URL) throws -> String? {
private func getDomainFromResolver(url: URL) throws -> DNSName? {
let text = try String(contentsOf: url, encoding: .utf8)
for line in text.components(separatedBy: .newlines) {
let trimmed = line.trimmingCharacters(in: .whitespaces)
@@ -140,7 +145,7 @@ public struct HostDNSResolver {
continue
}
return String(components[1])
return try? DNSName(String(components[1]))
}
return nil
@@ -16,6 +16,7 @@
import ContainerizationError
import ContainerizationExtras
import DNSServer
import Foundation
public struct PacketFilter {
@@ -31,7 +32,7 @@ public struct PacketFilter {
self.anchorsURL = anchorsURL
}
public func createRedirectRule(from: IPAddress, to: IPAddress, domain: String) throws {
public func createRedirectRule(from: IPAddress, to: IPAddress, domain: DNSName) throws {
guard type(of: from) == type(of: to) else {
throw ContainerizationError(.invalidArgument, message: "protocol does not match: \(from) vs. \(to)")
}
@@ -45,7 +46,7 @@ public struct PacketFilter {
case .v4: inet = "inet"
case .v6: inet = "inet6"
}
let redirectRule = "rdr \(inet) from any to \(from.description) -> \(to.description) # \(domain)"
let redirectRule = "rdr \(inet) from any to \(from.description) -> \(to.description) # \(domain.pqdn)"
var content = ""
if fm.fileExists(atPath: anchorURL.path) {
@@ -62,7 +63,7 @@ public struct PacketFilter {
try lines.joined(separator: "\n").write(toFile: anchorURL.path, atomically: true, encoding: .utf8)
}
public func removeRedirectRule(from: IPAddress, to: IPAddress, domain: String) throws {
public func removeRedirectRule(from: IPAddress, to: IPAddress, domain: DNSName) throws {
guard type(of: from) == type(of: to) else {
throw ContainerizationError(.invalidArgument, message: "protocol does not match: \(from) vs. \(to)")
}
@@ -76,7 +77,7 @@ public struct PacketFilter {
case .v4: inet = "inet"
case .v6: inet = "inet6"
}
let redirectRule = "rdr \(inet) from any to \(from.description) -> \(to.description) # \(domain)"
let redirectRule = "rdr \(inet) from any to \(from.description) -> \(to.description) # \(domain.pqdn)"
guard fm.fileExists(atPath: anchorURL.path) else {
return
@@ -16,6 +16,7 @@
import ContainerizationError
import ContainerizationExtras
import DNSServer
import Foundation
import Testing
@@ -34,7 +35,7 @@ struct HostDNSResolverTest {
defer { try? FileManager.default.removeItem(at: tempURL) }
let resolver = HostDNSResolver(configURL: tempURL)
try resolver.createDomain(name: "foo.bar")
try resolver.createDomain(name: try! DNSName("foo.bar"))
let resolverConfigURL = tempURL.appending(path: "containerization.foo.bar")
let actualText = try String(contentsOf: resolverConfigURL, encoding: .utf8)
let expectedText = """
@@ -47,9 +48,9 @@ struct HostDNSResolverTest {
#expect(actualText == expectedText)
try resolver.createDomain(name: "bar.foo")
try resolver.createDomain(name: try! DNSName("bar.foo"))
let domains = resolver.listDomains()
#expect(domains == ["bar.foo", "foo.bar"])
#expect(domains.map { $0.pqdn } == ["bar.foo", "foo.bar"])
}
@Test
@@ -64,9 +65,9 @@ struct HostDNSResolverTest {
defer { try? FileManager.default.removeItem(at: tempURL) }
let resolver = HostDNSResolver(configURL: tempURL)
try resolver.createDomain(name: "foo.bar")
try resolver.createDomain(name: try! DNSName("foo.bar"))
#expect {
try resolver.createDomain(name: "foo.bar")
try resolver.createDomain(name: try! DNSName("foo.bar"))
} throws: { error in
guard let error = error as? ContainerizationError, error.code == .exists else {
return false
@@ -87,12 +88,12 @@ struct HostDNSResolverTest {
defer { try? FileManager.default.removeItem(at: tempURL) }
let resolver = HostDNSResolver(configURL: tempURL)
try resolver.createDomain(name: "foo.bar")
_ = try resolver.deleteDomain(name: "foo.bar")
try resolver.createDomain(name: try! DNSName("foo.bar"))
_ = try resolver.deleteDomain(name: try! DNSName("foo.bar"))
let localhost = try! IPAddress("127.0.0.1")
try resolver.createDomain(name: "bar.baz", localhost: localhost)
let deletedLocalhost = try resolver.deleteDomain(name: "bar.baz")
try resolver.createDomain(name: try! DNSName("bar.baz"), localhost: localhost)
let deletedLocalhost = try resolver.deleteDomain(name: try! DNSName("bar.baz"))
#expect(localhost == deletedLocalhost)
let domains = resolver.listDomains()
@@ -111,9 +112,9 @@ struct HostDNSResolverTest {
defer { try? FileManager.default.removeItem(at: tempURL) }
let resolver = HostDNSResolver(configURL: tempURL)
try resolver.createDomain(name: "foo.bar")
try resolver.createDomain(name: try! DNSName("foo.bar"))
#expect {
_ = try resolver.deleteDomain(name: "bar.foo")
_ = try resolver.deleteDomain(name: try! DNSName("bar.foo"))
} throws: { error in
guard let error = error as? ContainerizationError, error.code == .notFound else {
return false
@@ -16,6 +16,7 @@
import ContainerizationError
import ContainerizationExtras
import DNSServer
import Foundation
import Testing
@@ -36,25 +37,25 @@ struct PacketFilterTest {
let pf = PacketFilter(configURL: configURL, anchorsURL: tempURL)
let from1 = try! IPAddress("203.0.113.113")
let domain1 = "aaa.com"
let domain1 = try! DNSName("aaa.com")
let to = try! IPAddress("127.0.0.1")
try pf.createRedirectRule(from: from1, to: to, domain: domain1)
let anchorURL = tempURL.appending(path: "com.apple.container")
var actualAnchorText = try String(contentsOf: anchorURL, encoding: .utf8)
var expectedAnchorTest = """
rdr inet from any to \(from1) -> \(to) # \(domain1)\n
rdr inet from any to \(from1) -> \(to) # \(domain1.pqdn)\n
"""
#expect(actualAnchorText == expectedAnchorTest)
let from2 = try! IPAddress("172.31.72.1")
let domain2 = "bbb.com"
let domain2 = try! DNSName("bbb.com")
try pf.createRedirectRule(from: from2, to: to, domain: domain2)
actualAnchorText = try String(contentsOf: anchorURL, encoding: .utf8)
expectedAnchorTest += """
rdr inet from any to \(from2) -> \(to) # \(domain2)\n
rdr inet from any to \(from2) -> \(to) # \(domain2.pqdn)\n
"""
#expect(actualAnchorText == expectedAnchorTest)
+7
View File
@@ -45,6 +45,13 @@ struct RecordsTests {
#expect(name.description == "example.com.")
}
@Test("DNS name with newline should throw")
func DNSNameWithNewLine() throws {
#expect(throws: DNSBindError.self) {
_ = try DNSName("foo.com\n")
}
}
@Test("Root domain")
func rootDomain() throws {
let name = try DNSName("")