This article is the third in the “Running your own DNS Resolver with MirageOS” series. In the first part, we used the ocaml-dns library to lookup the hostname corresponding with an IP address using its Dns_resolver_mirage module. In the second part, we wrote a simple DNS server, which serves RRs from a zone file using the Dns_server_mirage module.
Today in the third part, we will combine the above to write a simple DNS resolver, which relays queries to another DNS resolver. Then we will compose this with our simple DNS server from last week, to build a resolver which first looks up queries in the host file and if unsuccessful will relay the query to another DNS resolver.
As always, the complete code for these examples is in ocaml-dns-examples.
3.1 DNS FoRwarder
When writing our simple DNS server, we used a function called serve_with_zonefile in Dns_server_mirage to service incoming DNS queries. Now we are going remove a layer of abstraction and instead use serve_with_processor:
val serve_with_processor: t -> port:int -> processor:(module PROCESSOR) -> unit Lwt.t val serve_with_zonefile : t -> port:int -> zonefile:string -> unit Lwt.t
Now instead of passing the function a simple string, representing the filename of zonefile, we pass a first class module, satisfying the PROCESSOR signature. We can generate such a module by writing a process and using processor_of_process:
type ip_endpoint = Ipaddr.t * int type 'a process = src:ip_endpoint -> dst:ip_endpoint -> 'a -> Dns.Query.answer option Lwt.t module type PROCESSOR = sig include Dns.Protocol.SERVER (** DNS responder function. @param src Server sockaddr @param dst Client sockaddr @param Query packet @return Answer packet *) val process : context process end type 'a processor = (module PROCESSOR with type context = 'a) val processor_of_process : Dns.Packet.t process -> Dns.Packet.t processor
So given a Dns.Packet.t process, which is a function of type:
src:ip_endpoint -> dst:ip_endpoint -> Dns.Packet.t -> Dns.Query.answer option Lwt.t
We can now service DNS packets. If we assume that myprocess is a function of this type, we can service DNS queries with the following unikernel
open Lwt open V1_LWT open Dns open Dns_server let port = 53 module Main (C:CONSOLE) (K:KV_RO) (S:STACKV4) = struct module U = S.UDPV4 module DS = Dns_server_mirage.Make(K)(S) let myprocess ~src ~dst packet = ... let start c k s = let server = DS.create s k in let processor = ((Dns_server.processor_of_process myprocess) :> (module Dns_server.PROCESSOR)) in DS.serve_with_processor server ~port ~processor end
Now we will write an implementation of myprocess which will service DNS packets by forwarding them to another DNS resolver and then relaying the response.
Recall from part 1, that you can use the resolve function in Dns_resolver_mirage to do this. All that remains is to wrap invocation of resolve, in a function of type Dns.Packet.t process, which can be done as follows:
let process resolver ~src ~dst packet = let open Packet in match packet.questions with |  -> (* we are not supporting QDCOUNT = 0 *) return None | [q] -> DR.resolve (module Dns.Protocol.Client) resolver resolver_addr resolver_port q.q_class q.q_type q.q_name >>= fun result -> return (Some (Dns.Query.answer_of_response result))) | _ -> (* we are not supporting QDCOUNT > 1 *) return None
3.2 DNS server & forwarder
[this part requires PR 58 on ocaml-dns until it is merged in]
We will extend our DNS forwarded to first check a zonefile, this is achieve with just 3 extra lines:
... DS.eventual_process_of_zonefiles server [zonefile] >>= fun process -> let processor = (processor_of_process (compose process (forwarder resolver)) :> (module Dns_server.PROCESSOR)) in ...
Here we are using compose to use two processes: one called process generated from the zonefile and one called forwarder, from the forwarding code in the last section.
Next time, we will extend our DNS resolver to include a cache.