From f9899013fd43dd058fdf89709eed0b0861bfd931 Mon Sep 17 00:00:00 2001 From: Mike Drob Date: Thu, 30 Apr 2026 09:33:26 -0500 Subject: [PATCH] Merge commit from fork * Use DNSName for input * Use pqdn instead of fqdn. * Update Sources/DNSServer/Records/DNSName.swift Co-authored-by: J Logan * Simplify description * Add test * Make fmt --------- Co-authored-by: jwhur Co-authored-by: jwhur <57657645+JaewonHur@users.noreply.github.com> Co-authored-by: J Logan --- Package.swift | 1 + .../System/DNS/DNSCreate.swift | 7 +++++- .../System/DNS/DNSDelete.swift | 9 ++++++-- .../System/DNS/DNSList.swift | 11 +++++---- Sources/DNSServer/Records/DNSName.swift | 7 +++++- .../Client/HostDNSResolver.swift | 17 +++++++++----- .../Client/PacketFilter.swift | 9 ++++---- .../HostDNSResolverTest.swift | 23 ++++++++++--------- .../PacketFilterTest.swift | 9 ++++---- Tests/DNSServerTests/RecordsTests.swift | 7 ++++++ 10 files changed, 66 insertions(+), 34 deletions(-) diff --git a/Package.swift b/Package.swift index f4cad9e0..320e10d9 100644 --- a/Package.swift +++ b/Package.swift @@ -205,6 +205,7 @@ let package = Package( "ContainerPlugin", "ContainerResource", "ContainerXPC", + "DNSServer", "TerminalProgress", ], path: "Sources/Services/ContainerAPIService/Client" diff --git a/Sources/ContainerCommands/System/DNS/DNSCreate.swift b/Sources/ContainerCommands/System/DNS/DNSCreate.swift index 2fb9916d..0367b33c 100644 --- a/Sources/ContainerCommands/System/DNS/DNSCreate.swift +++ b/Sources/ContainerCommands/System/DNS/DNSCreate.swift @@ -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 { diff --git a/Sources/ContainerCommands/System/DNS/DNSDelete.swift b/Sources/ContainerCommands/System/DNS/DNSDelete.swift index ee801bf0..fc372e55 100644 --- a/Sources/ContainerCommands/System/DNS/DNSDelete.swift +++ b/Sources/ContainerCommands/System/DNS/DNSDelete.swift @@ -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) } } } diff --git a/Sources/ContainerCommands/System/DNS/DNSList.swift b/Sources/ContainerCommands/System/DNS/DNSList.swift index fa01654a..23c28bdb 100644 --- a/Sources/ContainerCommands/System/DNS/DNSList.swift +++ b/Sources/ContainerCommands/System/DNS/DNSList.swift @@ -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 } } diff --git a/Sources/DNSServer/Records/DNSName.swift b/Sources/DNSServer/Records/DNSName.swift index 32d277a3..164a5f18 100644 --- a/Sources/DNSServer/Records/DNSName.swift +++ b/Sources/DNSServer/Records/DNSName.swift @@ -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. diff --git a/Sources/Services/ContainerAPIService/Client/HostDNSResolver.swift b/Sources/Services/ContainerAPIService/Client/HostDNSResolver.swift index 5e5fb3b7..125c6f3c 100644 --- a/Sources/Services/ContainerAPIService/Client/HostDNSResolver.swift +++ b/Sources/Services/ContainerAPIService/Client/HostDNSResolver.swift @@ -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 diff --git a/Sources/Services/ContainerAPIService/Client/PacketFilter.swift b/Sources/Services/ContainerAPIService/Client/PacketFilter.swift index c53ef1d5..86feb1b0 100644 --- a/Sources/Services/ContainerAPIService/Client/PacketFilter.swift +++ b/Sources/Services/ContainerAPIService/Client/PacketFilter.swift @@ -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 diff --git a/Tests/ContainerAPIClientTests/HostDNSResolverTest.swift b/Tests/ContainerAPIClientTests/HostDNSResolverTest.swift index 15835b27..3ed751c7 100644 --- a/Tests/ContainerAPIClientTests/HostDNSResolverTest.swift +++ b/Tests/ContainerAPIClientTests/HostDNSResolverTest.swift @@ -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 diff --git a/Tests/ContainerAPIClientTests/PacketFilterTest.swift b/Tests/ContainerAPIClientTests/PacketFilterTest.swift index fdb63598..79dfe0a0 100644 --- a/Tests/ContainerAPIClientTests/PacketFilterTest.swift +++ b/Tests/ContainerAPIClientTests/PacketFilterTest.swift @@ -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) diff --git a/Tests/DNSServerTests/RecordsTests.swift b/Tests/DNSServerTests/RecordsTests.swift index 71e7726d..68a672de 100644 --- a/Tests/DNSServerTests/RecordsTests.swift +++ b/Tests/DNSServerTests/RecordsTests.swift @@ -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("")