向NEAppProxyUDPFlow写回数据时报Datagram过大错误的求助
问题背景
我正在基于DNSProxyProvider开发Network Extension,功能是拦截UDP协议的DNS流量,给域名附加自定义设备标识后发送至自定义DNS服务器,再将服务器响应转发回请求客户端。目前已成功完成请求域名的标识附加,且能正常获取服务器响应,但在将响应写回udpFlow时,控制台抛出如下错误:
Error Domain=NEAppProxyFlowErrorDomain Code=9 "The datagram was too large" UserInfo={NSLocalizedDescription=The datagram was too large}
已尝试方案
- 将数据报截断至10字节以内
- 使用虚拟Data对象执行写入操作
- 重新校验App及Network Extension的签名与权限配置
相关代码
handleNewFlow 方法
override func handleNewFlow(_ flow: NEAppProxyFlow) -> Bool { if flow is NEAppProxyTCPFlow { NSLog("BDDNSProxyProvider : Is TCP Flow...") } else if let udpFlow = flow as? NEAppProxyUDPFlow { NSLog("BDDNSProxyProvider: handleNewFlow : \(udpFlow)") processUDPFlow(udpFlow) // < -- } return true }
processUDPFlow 方法
// Read incoming DNS packets from the client private func processUDPFlow(_ udpFlow: NEAppProxyUDPFlow) { self.udpAppProxyFlow = udpFlow udpFlow.readDatagrams { datagrams, error in if let error = error { NSLog("Error reading datagrams: \(error.localizedDescription)") return } guard let datagrams = datagrams else { NSLog("No datagrams received.") return } // Forward each DNS packet to the custom DNS server for (index, packet) in datagrams.enumerated() { let dnsMessage = self.parseDNSMessage(from: packet.0) NSLog("tDatagram Header: \(dnsMessage.header)") for question in dnsMessage.questions { NSLog("tDatagram Question: \(question.name), Type: \(question.type), Class: \(question.klass)") } for answer in dnsMessage.answers { NSLog("tDatagram Answer: \(answer.name), Type: \(answer.type), Data: \(answer.data)") } let oldDomain = self.extractDomainName(from: packet.0)! let packetWithNewDomain = self.replaceDomainName(in: packet.0, with: "827-\(oldDomain)") // func to append device ID (827) NSLog("Packet's new domain \(self.extractDomainName(from: packetWithNewDomain ?? packet.0) ?? "Found nil")") self.sendToCustomDNSServer(packetWithNewDomain!) { responseDatagram in guard let responseDatagram = responseDatagram else { NSLog("Failed to get a response from the custom DNS server") return } let tDatagram = (responseDatagram, packet.1) udpFlow.writeDatagrams([tDatagram]) { error in if let error = error { NSLog("Failed to write DNS response back to client: \(error)") } else { NSLog("Successfully wrote DNS response back to client.") } } } } // Continue Reading Datagrams self.processUDPFlow(udpFlow) } }
域名提取方法
func extractDomainName(from datagram: Data) -> String? { // Ensure the datagram has enough data for a DNS header guard datagram.count > 12 else { return nil } // Start reading after the header (12 bytes) var offset = 12 var domainName = "" while offset < datagram.count { // Read the length of the next label let length = Int(datagram[offset]) offset += 1 // Check for the null terminator (end of domain name) if length == 0 { break } // Ensure there's enough data for the label guard offset + length <= datagram.count else { return nil } // Extract the label as a string if let label = String(data: datagram[offset..<offset + length], encoding: .utf8) { // Append the label to the domain name domainName += domainName.isEmpty ? label : "." + label } offset += length } return domainName.isEmpty ? nil : domainName }
解决方案
核心问题分析
这个错误的本质是:你写回客户端的DNS响应数据报长度,超过了客户端发起原始请求时的UDP payload尺寸限制。iOS/macOS的Network Extension对DNSProxy有隐含规则:响应的UDP数据报不能大于对应请求的原始数据报大小。
同时你的流程还存在两个潜在问题:
- 未修正DNS响应中的域名:服务器返回的响应是针对你修改后的域名(如
827-example.com),直接转发会导致客户端无法识别,且额外的标识会增加数据报长度。 processUDPFlow的递归调用可能引发内存泄漏,存在栈溢出风险。
具体修复步骤
还原DNS响应中的原始域名
实现与replaceDomainName对应的反向函数,将服务器响应中的827-xxx域名改回客户端最初请求的原始域名。这不仅能让客户端正确解析,还会直接缩短响应数据报长度,规避尺寸限制。示例反向替换逻辑:
func restoreOriginalDomain(in responseData: Data, originalDomain: String) -> Data? { var mutableData = responseData // 参考extractDomainName的解析逻辑,定位响应中的域名字段,替换为原始域名的DNS编码格式 // 注意处理DNS消息中的压缩指针(如果存在) return mutableData }在写回响应前调用该函数:
self.sendToCustomDNSServer(packetWithNewDomain!) { responseDatagram in guard let responseDatagram = responseDatagram else { NSLog("Failed to get a response from the custom DNS server") return } // 还原原始域名 guard let restoredResponse = self.restoreOriginalDomain(in: responseDatagram, originalDomain: oldDomain) else { NSLog("Failed to restore original domain in response") return } let tDatagram = (restoredResponse, packet.1) udpFlow.writeDatagrams([tDatagram]) { error in // ... 现有日志逻辑 ... } }校验响应数据报长度
在写回前对比原始请求和响应的长度,若响应仍超出限制,可考虑:- 过滤响应中不必要的记录(如额外的DNS扩展字段)
- 启用DNS TCP fallback,在
handleNewFlow中处理TCP类型的DNS请求,TCP不受UDP的单包尺寸限制
优化递归调用逻辑
将processUDPFlow的递归调用改为异步队列调用,避免栈溢出和循环引用:private func processUDPFlow(_ udpFlow: NEAppProxyUDPFlow) { udpFlow.readDatagrams { [weak self] datagrams, error in guard let self = self else { return } // ... 现有处理逻辑 ... // 异步调用避免递归栈溢出 DispatchQueue.global(qos: .background).async { self.processUDPFlow(udpFlow) } } }验证DNS响应合法性
用抓包工具(如Wireshark)对比原始请求、修改后请求、服务器响应的尺寸差异,确保响应是标准的DNS格式,无冗余数据。
内容的提问来源于stack exchange,提问作者VoidMain

