You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

向NEAppProxyUDPFlow写回数据时报Datagram过大错误的求助

使用DNSProxyProvider转发DNS响应时遭遇"The datagram was too large"错误

问题背景

我正在基于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数据报不能大于对应请求的原始数据报大小。

同时你的流程还存在两个潜在问题:

  1. 未修正DNS响应中的域名:服务器返回的响应是针对你修改后的域名(如827-example.com),直接转发会导致客户端无法识别,且额外的标识会增加数据报长度。
  2. processUDPFlow的递归调用可能引发内存泄漏,存在栈溢出风险。

具体修复步骤

  1. 还原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
            // ... 现有日志逻辑 ...
        }
    }
    
  2. 校验响应数据报长度
    在写回前对比原始请求和响应的长度,若响应仍超出限制,可考虑:

    • 过滤响应中不必要的记录(如额外的DNS扩展字段)
    • 启用DNS TCP fallback,在handleNewFlow中处理TCP类型的DNS请求,TCP不受UDP的单包尺寸限制
  3. 优化递归调用逻辑
    将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)
            }
        }
    }
    
  4. 验证DNS响应合法性
    用抓包工具(如Wireshark)对比原始请求、修改后请求、服务器响应的尺寸差异,确保响应是标准的DNS格式,无冗余数据。


内容的提问来源于stack exchange,提问作者VoidMain

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 21:07:33