Hi Quinn,
Following up on your earlier advice about using NEAppProxyUDPFlowHandling and the async readDatagrams / writeDatagrams API.
I’ve updated my DNS proxy as follows:
I now implement NEAppProxyUDPFlowHandling and handle DNS over UDP via the async API.
For each flow, I store the Network.NWEndpoint that comes from readDatagrams() and then pass that same endpoint back into writeDatagrams.
I no longer use NWHostEndpoint / NetworkExtension.NWEndpoint anywhere in this path.
Relevant code (stripped down):
private func readFirstDatagrams(from udpFlow: NEAppProxyUDPFlow,
into flow: Flow,
channel: Channel) {
Task {
let (pairs, error) = await udpFlow.readDatagrams()
if let error = error {
// handle error...
return
}
guard let pairs = pairs, !pairs.isEmpty else {
// retry...
return
}
let (firstData, firstEndpoint) = pairs[0]
let size = firstData.count
MainLogger.shared.log(message:
"UDP recv \(pairs.count) datagrams, first size=\(size) from=\(firstEndpoint) type=\(type(of: firstEndpoint))"
)
// Force a hostPort endpoint
let hostPortEndpoint: Network.NWEndpoint
switch firstEndpoint {
case let .hostPort(host, port):
hostPortEndpoint = .hostPort(host: host, port: port)
default:
let desc = firstEndpoint.debugDescription // e.g. "192.0.2.1:53"
let parts = desc.components(separatedBy: ":")
let portStr = parts.last ?? "53"
let hostStr = parts.dropLast().joined(separator: ":")
let host = Network.NWEndpoint.Host(hostStr)
let port = Network.NWEndpoint.Port(portStr) ?? .init(rawValue: 53)!
hostPortEndpoint = .hostPort(host: host, port: port)
}
MainLogger.shared.log(message:
"FORCE endpoint hostPort=\(hostPortEndpoint) type=\(type(of: hostPortEndpoint))"
) flow.updateNetworkEndpoint(hostPortEndpoint)
// Validate and send to upstream (TCP via SwiftNIO)
// ...
}
}
// Flow.sendResponseToSystem
func sendResponseToSystem(_ data: Data) {
guard let udpFlow = udpFlow else { return }
guard let endpoint = networkEndpoint else { return }
MainLogger.shared.log(message:
"\(logPrefix) sendResponseToSystem: \(data.count) bytes → \(endpoint)"
)
let preview = data.prefix(4).map { String(format: "%02X", $0) }.joined(separator: " ")
MainLogger.shared.log(message:
"\(logPrefix) data preview: \(preview)"
)
MainLogger.shared.log(message:
"\(logPrefix) endpoint type=\(type(of: endpoint)) desc=\(endpoint)"
)
Task {
do {
try await udpFlow.writeDatagrams([(data, endpoint)])
MainLogger.shared.log(message: "\(logPrefix) writeDatagrams OK")
} catch {
MainLogger.shared.log(message:
"\(logPrefix) writeDatagrams error: \(error.localizedDescription)"
)
}
}
}
On macOS 26.x (DNS proxy system extension), with this code I still consistently hit The datagram was too large on a small, valid DNS response (67 bytes). Here is a complete log for a single dig example.com flow, with upstream details anonymised:
FLOW new from=com.apple.dig type=NEAppProxyUDPFlow remoteEndpoint=192.168.0.1:53
… upstream TCP connection to our DNS backend is established successfully …
UDP recv 1 datagrams, first size=39 from=192.168.0.1:53 type=NWEndpoint
✅ FORCE endpoint hostPort=192.168.0.1:53 type=NWEndpoint ✅
✅ Flow@… updateNetworkEndpoint=192.168.0.1:53 type=NWEndpoint ✅
DNS valid packet size=39, datagrams=1 → sendPackets
… TCP write of the DNS query over the upstream connection …
FlowChannelHandler: complete response 67 bytes → client ✅
✅ Flow@… sendResponseToSystem: 67 bytes → 192.168.0.1:53 ✅
⚠️ Flow@… data preview: 58 C8 81 80 ⚠️ // looks like a normal DNS answer
✅ Flow@… endpoint type=NWEndpoint desc=192.168.0.1:53
🆘 Flow@… writeDatagrams error: The datagram was too large 🆘
So, at this point:
I’m using the async readDatagrams / writeDatagrams API.
I store and reuse Network.NWEndpoint (a .hostPort endpoint), no NWHostEndpoint.
The response is small (67 bytes) and looks like a valid DNS answer.
Yet writeDatagrams([(data, endpoint)]) still fails with The datagram was too large.
Is there any known issue or additional requirement for NEAppProxyUDPFlow.writeDatagrams on macOS 26.x that could explain this error in this configuration? Or is this something you’d like me to file as a bug with a sysdiagnose?
Thanks in advance for any pointers.