Conversation
|
|
||
| /// The fully-qualified domain name with trailing dot. | ||
| public var description: String { | ||
| labels.isEmpty ? "." : labels.joined(separator: ".") + "." |
There was a problem hiding this comment.
Question: some lookup paths appear to use plain string (no normalization), will this change cause any issue? For example in AttachmentAllocator;
// store
func allocate(hostname: String)..
// lookup
func lookup(hostname: String) async throws -> UInt32? {
hostnames[hostname]
}
There was a problem hiding this comment.
@manojmahapatra Thanks for looking at this! I converted it to draft.
This one was done by telling Claude to create the types we needed using the bit-fiddling code that ContainerizationNetlink uses, and I pushed it primarily to back the change up and get something started for the issue.
I still need to review the work myself and sanity check it, but I'll bet I would have missed this!
I'll have a look through the relevant RFCs as I review this myself. This isn't an area I'm super expert in so if you've got experience and suggestions I'd welcome them!
There was a problem hiding this comment.
I'm not an expert in this area either, but I reviewed the PR in detail against current behavior. Overall this is moving in the right direction.
I see two gaps worth addressing here:
- Hostname normalization consistency (e.g.
foovsfoo.) across parse/store/lookup paths. (additional focused unit tests will be nice) - Compression-pointer safety in
DNSNamedecode to avoid malformed packet loops. Something like this maybe;
// Calculate pointer offset from message start
let pointerLocation = offset
let pointer = Int(length & 0x3F) << 8 | Int(buffer[offset + 1])
let pointerTarget = messageStart + pointer
guard pointerTarget < pointerLocation else {
throw DNSBindError.unmarshalFailure(type: "DNSName", field: "compression pointer not prior")
}
offset = pointerTarget0d90e59 to
32eac7e
Compare
- Closes apple#1268. - The types we were using weren't very usable with Swift 6 structured concurrency. - Use notImplemented instead of formatError for unknown record types. - Use pure actor for LocalhostDNSHandler now that we have sendable types.
- Require all input domain names to carry trailing dot.
- Fold labels to lowercase. - Reject compression pointer recursion.
|
@manojmahapatra I've addressed what you called out, and also we're now folding to lowercase when creating domain name records. The larger change (requiring a trailing dot on all the DNS APIs) is in the first follow-up commit, and the smaller one for case folding and compression pointer recursion preventions is in the second one. Finally got myself out from under some other obligations which've kept me busy elsewhere. I'll look over your apple/containerization#577 early tomorrow. Thank you again for your scrutiny on this PR! |
| /// | ||
| /// - Parameter dnsHostname: A DNS-format hostname, optionally with a trailing dot | ||
| /// (e.g. `"example.com."` or `"example.com"`). The trailing dot is stripped before lookup. | ||
| public func lookup(dnsHostname: String) async throws -> Attachment? { |
There was a problem hiding this comment.
Question: Looks like it's a public API break due to the function rename? Do we need a shim?
There was a problem hiding this comment.
Ah, my bad. With the docc description we don't really need to change the parameter name.
| /// Handler that uses table lookup to resolve hostnames. | ||
| /// | ||
| /// All keys in `hosts4` must be canonical DNS names — fully-qualified with a | ||
| /// trailing dot (e.g. `"example.com."`). This matches the canonical form used |
| public init(_ string: String) { | ||
| // Remove trailing dot if present, then split | ||
| let normalized = string.hasSuffix(".") ? String(string.dropLast()) : string | ||
| self.labels = normalized.isEmpty ? [] : normalized.split(separator: ".").map { String($0).lowercased() } |
There was a problem hiding this comment.
Question: So here labels are lowercases when parsing names. However, HostTableResolver does not normalize keys on init, which I think might be an issue. can you please confirm?
There was a problem hiding this comment.
Thanks for catching that. We're not using HostTableResolver yet, but it would have been broken. I reworked the case folding and the HostTableResolver a little to do things correctly, and a little more cleanly.
|
@jglogan overall the changes looks good, just two questions. |
| /// | ||
| /// - Parameter hosts4: A dictionary mapping fully-qualified domain names (with trailing dot) | ||
| /// to IPv4 addresses. Keys without a trailing dot will not match wire-decoded queries. | ||
| /// - Parameter ttl: The TTL in seconds to set on answer records. |
There was a problem hiding this comment.
nitpick: The TTL in seconds to set on answer records. Defaults to 300.
| let endOffset = try record.appendBuffer(&buffer, offset: 0) | ||
|
|
||
| // Should have written: name + type(2) + class(2) + ttl(4) + rdlen(2) + rdata(4) | ||
| #expect(endOffset > 0) |
There was a problem hiding this comment.
Can we check that the end offset is > 4 instead?
katiewasnothere
left a comment
There was a problem hiding this comment.
Couple test requests, otherwise LGTM
- Removes the Bindable prototype as we're not using it.
- Fix a double-optional expression, use flatMap. - Validate the DNS name extracted from the resolv.conf option, and then use the lowercased name as the DNS dict key. - Return NODATA on AAAA query if an A record exists, which matches HostTableResolver and ContainerDNSHandler behavior.
- Lookup now matches HostTableResolver.
| /// are case-insensitive and trailing dots are optional. | ||
| public struct HostTableResolver: DNSHandler { | ||
| public let hosts4: [String: IPv4] | ||
| public let hosts4: [DNSName: IPv4Address] |
There was a problem hiding this comment.
summary:
- Replace
IPv4with ourIPv4Addresseverywhere. - Use
DNSNamefor keys to validate and normalize hostnames for lookup.
|
|
||
| public func answer(query: Message) async throws -> Message? { | ||
| let question = query.questions[0] | ||
| guard let question = query.questions.first else { |
There was a problem hiding this comment.
summary: updated all handlers to ensure questions[0] is present, and if not, defer to the next handler
This would result in a NXDOMAIN when the last handler returns nil, when a .formatError would be more appropriate. The typical usage, though, is to put a StandardQueryValidator at the front of the chain which ensures that exactly one question is present and returns a .formatError otherwise.
| guard let question = query.questions.first else { | ||
| return nil | ||
| } | ||
| let n = question.name.hasSuffix(".") ? String(question.name.dropLast()) : question.name |
There was a problem hiding this comment.
summary: for the DNS names that come in on the wire, we don't perform hostname validation - we just split into labels and form a DNSName, and look them up. If they have weird characters or are otherwise invalid hostnames, they'll just miss as L35 ensures that our keys are always valid hostnames.
| } | ||
| // If hostname doesn't exist, return nil which will become NXDOMAIN | ||
| return nil | ||
| case ResourceRecordType.nameServer, |
There was a problem hiding this comment.
summary: all unhandled question types in handlers should return .notImplemented
| //===----------------------------------------------------------------------===// | ||
|
|
||
| /// Errors that can occur during DNS message serialization/deserialization. | ||
| public enum DNSBindError: Error, CustomStringConvertible { |
There was a problem hiding this comment.
summary: extracted types from Bindable.swift into individual files
| public var labels: [String] | ||
|
|
||
| /// Creates a DNS name representing the root (empty label list). | ||
| public init() { |
There was a problem hiding this comment.
summary: used to building DNS names label by label from wire data
- Ensures labels are normalized.
| // We can look at refining this later to see if we can use some common | ||
| // bit fiddling code everywhere. | ||
|
|
||
| extension [UInt8] { |
There was a problem hiding this comment.
summary: removed Bindable.swift, extracting types into individual files, this code is brought over from containerization
| // EDNS0 (RFC 6891), and this server only resolves host A/AAAA queries, so a | ||
| // legitimate query will never approach this limit. Reject oversized packets | ||
| // before reading to avoid allocating memory for malformed or malicious datagrams. | ||
| let maxPacketSize = 512 |
There was a problem hiding this comment.
summary: added for safety
|
|
||
| self.log?.debug("serializing response") | ||
| responseData = try response.serialize() | ||
| } catch let error as DNSBindError { |
There was a problem hiding this comment.
summary: we now differentiate between structural errors in DNS queries (.formatError) and unsupported query values (.notImplemented)
| responseData = try response.serialize() | ||
| } catch { | ||
| self.log?.error("error processing message from \(query): \(error)") | ||
| let rawId = data.count >= 2 ? data[0..<2].withUnsafeBytes { $0.load(as: UInt16.self) } : 0 |
There was a problem hiding this comment.
summary: our "500" path
| private let watcher: DirectoryWatcher | ||
|
|
||
| private let dns: Mutex<[String: IPv4]> | ||
| private var dns: [DNSName: IPv4Address] |
There was a problem hiding this comment.
summary: no need for Mutex, actor isolation is fine here, and use DNSName keys to ensure hostname validity
|
|
||
| if let match = content.firstMatch(of: regex), | ||
| let ipv4 = (match[1].substring.map { String($0) }) | ||
| let ipv4 = (match[1].substring.flatMap { try? IPv4Address(String($0)) }) |
There was a problem hiding this comment.
summary: flatMap should be used here since substring produces an optional
| guard let question = query.questions.first else { | ||
| return nil | ||
| } | ||
| let n = question.name.hasSuffix(".") ? String(question.name.dropLast()) : question.name |
There was a problem hiding this comment.
Same pattern as HostTableHandler, don't need to do hostname validation on the wire data.
| ? "" | ||
| : HostDNSResolver.localhostOptionsRegex.replacingOccurrences( | ||
| of: #"\((.*?)\)"#, with: localhost!.description, options: .regularExpression) | ||
| localhost.map { |
There was a problem hiding this comment.
summary: remove force-unwrap
Type of Change
Testing