I'm just following up on the issue I previously reported. I've simplified the implementation by removing most of the added logic, all DNS proxy functionality is now contained within a single class.
However, I'm still experiencing issues with NEDNSProxyProvider on iOS 18.4.1 and iOS 15.5 Beta. The same code runs without any problems on iOS 18.3.2 and earlier.
Please let me know if any additional information would help troubleshoot this further.
import NetworkExtension
import OSLog
private extension DispatchQueue {
static let datagramConnection = DispatchQueue(label: "mwe.dns-proxy.datagram-connection")
}
class DNSProxyProvider: NEDNSProxyProvider {
override func startProxy(options:[String: Any]? = nil, completionHandler: @escaping (Error?) -> Void) {
Logger.traffic.info("NEDNSProxyProvider started")
completionHandler(nil)
}
override func stopProxy(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
Logger.traffic.error("NEDNSProxyProvider stopped with reason: \(reason.rawValue, privacy: .public)")
completionHandler()
}
override func handleNewFlow(_ flow: NEAppProxyFlow) -> Bool {
Logger.statistics.info("[NEDNSProxyProvider] - Received \(flow.debugDescription, privacy: .public)")
switch flow {
case let udpFlow as NEAppProxyUDPFlow:
Task { [weak self] in
await self?.handleNewUDPFlow(udpFlow)
}
return true
default:
return false
}
}
private func handleNewUDPFlow(_ flow: NEAppProxyUDPFlow) async {
defer {
flow.close()
Logger.traffic.info("[UDP] Flow closed")
}
do {
try await flow.open(withLocalFlowEndpoint: flow.localFlowEndpoint)
Logger.traffic.info("[UDP] Opened \(flow, privacy: .public)")
} catch {
Logger.traffic.error("[UDP] Failed to open: \(error.localizedDescription, privacy: .public)")
return
}
let (datagrams, error) = await flow.readDatagrams()
if let error = error {
Logger.traffic.error("[UDP] Failed to read: \(error.localizedDescription, privacy: .public)")
return
}
guard let datagrams, !datagrams.isEmpty else {
Logger.traffic.info("[UDP] No datagrams")
return
}
do {
let resolvedDatagrams = try await datagrams.parallelMap(parallelism: datagrams.count) { [weak self] datagram in
guard let self = self else {
throw NSError(domain: "UDP", code: 1, userInfo: [NSLocalizedDescriptionKey: "Self is nil"])
}
let resolvedQuery: Data = await self.forwardDNSQuery(datagram.0) ?? datagram.0.nxdomainData
return (resolvedQuery, datagram.1)
}
try await flow.writeDatagrams(resolvedDatagrams)
} catch {
Logger.traffic.error("[UDP] Failed to write: \(error.localizedDescription, privacy: .public)")
return
}
}
func forwardDNSQuery(_ data: Data) async -> Data? {
await withCheckedContinuation { continuation in
let connection = NWConnection(host: "1.1.1.3", port: 53, using: .udp)
connection.start(queue: .global())
connection.send(content: data, completion: .contentProcessed { error in
if error != nil {
connection.cancel()
continuation.resume(returning: nil)
return
}
connection.receive(minimumIncompleteLength: 1, maximumLength: 512) { content, _, _, _ in
connection.cancel()
continuation.resume(returning: content)
}
})
}
}
}
extension NEAppProxyFlow {
public func close(_ error: Error? = nil) {
self.closeReadWithError(error)
self.closeWriteWithError(error)
}
}
extension Collection {
func parallelMap<T: Sendable>(
parallelism: Int = 2,
_ transform: @escaping (Element) async throws -> T
) async rethrows -> [T] {
guard !isEmpty else { return [] }
let count = count
return try await withThrowingTaskGroup(of: (Int, T?).self, returning: [T].self) { group in
var buffer: [T?] = [T?](repeating: nil, count: count)
var i = self.startIndex
var submitted = 0
func submitNext() async throws {
guard i != self.endIndex else { return }
group.addTask { [submitted, i] in
do {
let value = try await transform(self[i])
return (submitted, value)
} catch {
return (submitted, nil)
}
}
submitted += 1
formIndex(after: &i)
}
for _ in 0 ..< parallelism {
try await submitNext()
}
var completedCount = 0
while let (index, taskResult) = try await group.next() {
buffer[index] = taskResult
completedCount += 1
try Task.checkCancellation()
if completedCount < count {
try await submitNext()
}
}
let result = buffer.compactMap { $0 }
if result.count != count {
throw NSError(
domain: "ParallelismError", code: 5,
userInfo: [NSLocalizedDescriptionKey: "parallelMap transformation failed due to invalid result count"]
)
}
return result
}
}
}
extension Data {
var nxdomainData: Data {
let deafultPacket = Data(
[0x00, 0x00,
0x81, 0x83,
0x00, 0x01,
0x00, 0x00,
0x00, 0x00,
0x00, 0x00]
)
guard self.count >= 12 else { return deafultPacket }
var response = Data()
response.append(self.prefix(2))
response.append(contentsOf: [0x81, 0x83])
response.append(contentsOf: [0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
if let optStartIndex = self[12...].firstIndex(of: 0x00) {
response.append(self[12...optStartIndex + 4]) // Question section
} else {
response.append(self[12...]) // No OPT record
}
return response
}
}