How to send DNS data packets to a custom DNS server using NEDNSProxyProvider on a supervised device

Currently my NEDNSProxyProvider class is set up like this:

Code Block swift
import NetworkExtension
class DNSProxyProvider: NEDNSProxyProvider {
   
  private var proxyFlow: NEAppProxyUDPFlow?
   
  override init() {
    super.init()
  }
  override func startProxy(options:[String: Any]? = nil, completionHandler: @escaping (Error?) -> Void) {
    NSLog("DNSProxyProvider: startProxy")
    completionHandler(nil)
  }
  override func stopProxy(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
    NSLog("DNSProxyProvider: stopProxy")
    completionHandler()
  }
  override func sleep(completionHandler: @escaping () -> Void) {
    NSLog("DNSProxyProvider: sleep")
    completionHandler()
  }
  override func wake() {
    NSLog("DNSProxyProvider: wake")
  }
  override func handleNewFlow(_ flow: NEAppProxyFlow) -> Bool {
    NSLog("DNSProxyProvider: handleFlow")
     
    if let udpFlow = flow as? NEAppProxyUDPFlow {
      let localHost = (udpFlow.localEndpoint as! NWHostEndpoint).hostname
      let localPort = (udpFlow.localEndpoint as! NWHostEndpoint).port
       
      NSLog("DNSProxyProvider UDP HOST : \(localHost)")
      NSLog("DNSProxyProvider UDP PORT : \(localPort)")
       
      proxyFlow = udpFlow
      open()
    }
     
    return true
  }
   
  private func open() {
     
    guard let flow = proxyFlow else { return }
    guard let endPoint = flow.localEndpoint as? NWHostEndpoint else { return }
     
    flow.open(withLocalEndpoint: endPoint) { (error) in
      if (error != nil) {
        NSLog("DNSProxyProvider UDP Open flow Error : \(error.debugDescription)")
      } else {
        NSLog("DNSProxyProvider UDP Open flow Success")
        self.handleData()
      }
    }
  }
   
  private func handleData() {
    proxyFlow?.readDatagrams(completionHandler: { (data, endpoint, error) in
      if let error = error {
        NSLog("DNSProxyProvider UDP read data Error : \(error.localizedDescription)")
        return
      }
       // modify EDNS
      // Send data to custom DNS server to resolve
      // Write the response back to flow
    })
  }
}

I am unsure of how to send the DNS request to my own DNS server. Should I use the NWConnection class for this ? Or is there any other method which can be used for this ? Thanks!
Answered by Systems Engineer in 671531022

I am unsure of how to send the DNS request to my own DNS server. Should I use the NWConnection class for this ?

I wrote an article that describes the flow copying process here. Having said that, the general process for UDP flow copying is not the same as TCP, which is outlined in the referenced article. UDP is different as it could deal with many datagrams on a single flow.

First I noticed that you are opening the local flow with the local endpoint, so you are off to a good start to this process. From there you need to build your outbound copier because your local flow is being opened first. So this is the copier reading/writing from the local flow. In this read you will get data for 1 or more outgoing datagrams that you will need to build and manage NWConnection's for. I would recommend even a separate outbound datagram class for each of these NWConnection's that is being opened from your flow.

Next is the inbound copier for data being read from the remote NWConnection. This is very similar to how TCP was done, but now you need to define an inbound copier for each one of your datagram connections so each one of these connections can receive data and write it to the local flow.

Lastly, you would close each of the datagram connections on finish or error just as you normally would with cancel, closeReadWithError, and closeWriteWithError.

There is no sample for this but please let me know if you have questions.


Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com
Accepted Answer

I am unsure of how to send the DNS request to my own DNS server. Should I use the NWConnection class for this ?

I wrote an article that describes the flow copying process here. Having said that, the general process for UDP flow copying is not the same as TCP, which is outlined in the referenced article. UDP is different as it could deal with many datagrams on a single flow.

First I noticed that you are opening the local flow with the local endpoint, so you are off to a good start to this process. From there you need to build your outbound copier because your local flow is being opened first. So this is the copier reading/writing from the local flow. In this read you will get data for 1 or more outgoing datagrams that you will need to build and manage NWConnection's for. I would recommend even a separate outbound datagram class for each of these NWConnection's that is being opened from your flow.

Next is the inbound copier for data being read from the remote NWConnection. This is very similar to how TCP was done, but now you need to define an inbound copier for each one of your datagram connections so each one of these connections can receive data and write it to the local flow.

Lastly, you would close each of the datagram connections on finish or error just as you normally would with cancel, closeReadWithError, and closeWriteWithError.

There is no sample for this but please let me know if you have questions.


Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com
@Matt Thanks for that.


I would recommend even a separate outbound datagram class for each of these NWConnection's that is being opened from your flow.

Yep, I've implemented this, creating a new NWConnection for each datagram received when reading the flow, but it still doesn't seem to be working. I seem to be getting a lot of (3303290176): Closing reads, not closed by plugin and (3303290176): Closing writes, not sending close error messages.


Currently this is what I have implemented. The NEDNSProxyProvider class after handleNewFlow and flow.open is called:

Code Block swift
  private func handleData(for flow: NEAppProxyUDPFlow) {
    flow.readDatagrams(completionHandler: { (data, endpoint, error) in
      if let error = error {
        NSLog("DNSProxyProvider UDP read data Error : \(error.localizedDescription)")
        return
      } else {
        if let datagrams = data, let _ = endpoint, !datagrams.isEmpty {
          self.outBoundCopier(flow: flow, datagrams: datagrams)
        }
      }
    })
  }
   
  private func outBoundCopier(flow: NEAppProxyUDPFlow, datagrams: [Data]) {
    for data in datagrams {
      NSLog("DNSProxyProvider starting connection")
             
      let dnsServer = DNSServerConnection(flow: flow, data: data)
      dnsServer.start()
       
      dnsServer.onDataReceived = { (flow, data, isComplete, error, endpoint) in
        self.inBoundCopier(flow: flow, data: data, isComplete: isComplete, error: error, endPoint: endpoint)
      }
       
    }
  }
   
  private func inBoundCopier(flow: NEAppProxyUDPFlow, data: Data?, isComplete: Bool?, error: NWError?, endPoint: NWHostEndpoint) {
    switch(data, isComplete, error) {
    case (let data?, _ , _):
      flow.writeDatagrams([data], sentBy: [endPoint], completionHandler: { (error) in
        if let error = error {
          NSLog("DNSProxyProvider UDP write Error : \(error.localizedDescription)")
        }
        NSLog("DNSProxyProvider UDP write completed")
      })
    case(_, true, _):
      flow.closeReadWithError(error)
      flow.closeWriteWithError(error)
      NSLog("DNSProxyProvider inbound copier completed")
    case (_, _, let error?):
      NSLog("DNSProxyProvider inbound copier Error : \(error.localizedDescription)")
    default: NSLog("DNSProxyProvider inbound copier error")
    }
     
  }


And the DNSServerConnection class which handles all the NWConnection's

Code Block swift
class DNSServerConnection {
  private let connection: NWConnection
  private let data: Data
  private let flow: NEAppProxyUDPFlow
   
  private let endPoint = NWHostEndpoint(hostname: CUSTOM_DNS_IP, port: CUSTOM_DNS_PORT)
   
  var onDataReceived: ((NEAppProxyUDPFlow, Data?, Bool?, NWError?, NWHostEndpoint) -> ())?
   
  init(flow: NEAppProxyUDPFlow, data: Data) {
    self.flow = flow
    self.data = data
    connection = NWConnection(host: CUSTOM_DNS_IP, port: CUSTOM_DNS_PORT, using: .udp)
  }
   
  func start() {
    connection.stateUpdateHandler = connectionStateChanged(to:)
    connection.start(queue: .main)
  }
   
  private func connectionStateChanged(to state: NWConnection.State) {
    switch state {
    case .waiting(let error):
      NSLog("DNSProxyProvider Error opening connection: \(error.localizedDescription)")
     
    case .ready:
      NSLog("DNSProxyProvider connection ready")
      sendDataToServer()
     
    case .failed(let error):
      connection.cancel()
      NSLog("DNSProxyProvider Error opening connection: \(error.localizedDescription)")
     
    default: break
     
    }
  }
   
  private func sendDataToServer() {
     
    connection.send(content: data, completion: .contentProcessed({ (error) in
      if let error = error {
        NSLog("DNSProxyProvider UDP outbound Error : \(error.localizedDescription)")
      }
       
      NSLog("DNSProxyProvider UDP outbound Success")
      self.receiveData()
    }))
  }
   
  private func receiveData() {
    connection.receive(minimumIncompleteLength: 1, maximumLength: 2048, completion: { [self] (data, _, isComplete, error) in
       
      if (isComplete) {
        NSLog("DNSProxyProvider UDP outbound completed")
         
        self.connection.stateUpdateHandler = nil
        self.connection.cancel()
         
      }
         
      onDataReceived?(flow, data, isComplete, error, endPoint)
    })
  }
}





A few things here to consider:
  1. I suspect that this is elided, but I want to make sure you are opening the NEAppProxyUDPFlow with a localEndpoint as a first step here. It looked like you were doing this previously, just want to make sure you are again before handleData is called.

  2. For UDP will need to use receiveMessage on your NWConnection object in DNSServerConnection.

  3. I suspect you do not want your NWConnection callbacks being sent on the main queue in this context. Define a worker queue for your provider.

  4. On your inBoundCopier you need a way to cancel all of the DNSServerConnection objects that are running as part of this flow if any errors take place. I'd suggest keeping track of all of these connections associated by a flow. That way if you need to act on all of your connections, you can.

  5. You may run into a situation where you are opening the same remote connection for an outbound datagram twice on a flow. If you implement a strategy where you keep track of the the connections associated with the flow, you could just reuse the same connection and that way you do not need to open a new one.


Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com
@Matt thanks, that was really helpful. Better management of threads and writing datagrams to the correct end point did the trick.

I came across NWConnection.batch and was wondering would it be better to do a batch send of datagrams as opposed to creating NWConnection's for each outgoing datagram. Would a solution like this be better ?

Also, could you please advise on the best way to modify the DNS packets received in flow.readDatagrams before sending them to a custom DNS server ?
@Matt thanks for the clarifications!

@burhanshakir I've used this lib to modify DNS packets: https://github.com/heyvito/dns. If you wanna use EDNS(0) options, packets must follow this: https://tools.ietf.org/html/rfc6891
Hey @dmytrofromukrainka that lib seems to be for Mac OS but I am working with iOS, anyways thanks for the heads up!
@burhanshakir yes, I use that lib with minor changes for iOS project and it works pretty good

@Matt thanks, that was really helpful. Better management of threads and writing datagrams to the correct end point did the trick.

No problem, glad that helped.

I came across NWConnection.batch and was wondering would it be better to do a batch send of datagrams as opposed to creating NWConnection's for each outgoing datagram. Would a solution like this be better ?

I have not tried NWConnection.batch and prefer using a managing class for each outgoing datagram, but that is my preference.


Also, could you please advise on the best way to modify the DNS packets received in flow.readDatagrams before sending them to a custom DNS server ?

No. This is something you will need to research on your own.



Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com
Hey @dmytrofromukrainka could you please tell me a bit on how did you add EDNS0 to the packets using the DNS library there ? I can see the DNSOPTResourceRecord class in the lib but not sure if thats useful for this. Have you used this class as well ?

@burhanshakir - I used your code , I have same query, as we want redirect all the traffic to our own customDNS Server so basically I don't understand where to put CustomDNS Server call also when I run above code it has following issues

  1. I am not able to move ahed after  dnsServer.start() call in outBoundCopier function and receiveData method not trigger

Also could you please tell me , where I should put custom DNS API call and how to process the JSON response receiving from that call

@meaton - can you please help this issue

@burhanshakir -  private let endPoint = NWHostEndpoint(hostname:"," port:"")

In the above line what is hostname and port need to pass her

connection = NWConnection(host: CUSTOM_DNS_IP, port: CUSTOM_DNS_PORT, using: .udp)

In this above line what is CUSTOM_DNS_IP, and CUSTOM_DNS_PORT values

Can you please suggestion.

How to send DNS data packets to a custom DNS server using NEDNSProxyProvider on a supervised device
 
 
Q