(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)

open Dns

type key = [ `raw ] Domain_name.t * Packet.Question.qtype

let pp_key = Dns_resolver_cache.pp_question

let src = Logs.Src.create "dns_resolver" ~doc:"DNS resolver"
module Log = (val Logs.src_log src : Logs.LOG)

(* The cache (a Map!?) for answers: once a specific type/name comes in, we know
   which questions can progress now *)
module QM = Map.Make(struct
    type t = key
    let compare (n, t) (n', t') =
      match Domain_name.compare n n' with
      | 0 -> Packet.Question.compare_qtype t t'
      | x -> x
  end)

type awaiting = {
  ts : int64;
  retry : int;
  proto : proto;
  zone : [ `raw ] Domain_name.t;
  edns : Edns.t option;
  ip : Ipaddr.t;
  port : int;
  question : key;
  id : int;
  checking_disabled : bool;
  dnssec_ok : bool;
}

let awaiting_eq a b =
  Ipaddr.compare a.ip b.ip = 0 &&
  Int.equal a.port b.port &&
  Domain_name.equal (fst a.question) (fst b.question) &&
  Packet.Question.compare_qtype (snd a.question) (snd b.question) = 0 &&
  Int.equal a.id b.id

module TM = Map.Make(struct
    type t = key * Ipaddr.t * int * int
    let compare ((n, t), ip, port, id) ((n', t'), ip', port', id') =
      let andThen v f = match v with 0 -> f () | x -> x in
      andThen (Domain_name.compare n n')
        (fun () -> andThen (Packet.Question.compare_qtype t t')
            (fun () -> andThen (Ipaddr.compare ip ip')
                (fun () -> andThen (Int.compare port port')
                    (fun () -> Int.compare id id'))))
  end)

let retry_interval = Duration.of_ms 500

type feature =
  [ `Dnssec | `Qname_minimisation | `Opportunistic_tls_authoritative ]

module FS = Set.Make(struct
    type t = feature
    let compare a b = match a, b with
      | `Dnssec, `Dnssec -> 0 | `Dnssec, _ -> 1 | _, `Dnssec -> -1
      | `Qname_minimisation, `Qname_minimisation -> 0
      | `Qname_minimisation, _ -> 1
      | _, `Qname_minimisation -> -1
      | `Opportunistic_tls_authoritative, `Opportunistic_tls_authoritative -> 0
(*      | `Opportunistic_tls_authoritative, _ -> 1
        | _, `Opportunistic_tls_authoritative -> -1 *)
  end)

type t = {
  ip_protocol : [ `Both | `Ipv4_only | `Ipv6_only ];
  features : FS.t  ;
  rng : int -> string ;
  primary : Dns_server.Primary.s ;
  cache : Dns_cache.t ;
  transit : awaiting TM.t ;
  queried : awaiting list QM.t ;
  mutable clients : Ipaddr.Set.t ;
  record_clients : bool ;
}

let create ?(record_clients = true) ?(cache_size = 10000) ?(ip_protocol = `Both) features now rng primary =
  let cache = Dns_cache.empty cache_size in
  let cache =
    List.fold_left (fun cache (name, b) ->
        Dns_cache.set cache now
          name A Dns_cache.Additional
          (`Entry b))
      cache Dns_resolver_root.a_records
  in
  let cache =
    List.fold_left (fun cache (name, b) ->
        Dns_cache.set cache now
          name Aaaa Dns_cache.Additional
          (`Entry b))
      cache Dns_resolver_root.aaaa_records
  in
  let cache =
    Dns_cache.set cache now
      Domain_name.root Ns Dns_cache.Additional
      (`Entry Dns_resolver_root.ns_records)
  in
  let cache =
    Dns_cache.set cache now
      Domain_name.root Ds Dns_cache.Additional
      (`Entry (Int32.max_int, Dnssec.root_ds))
  in
  let features = FS.of_list features in
  { ip_protocol ; features ; rng ; cache ; primary ; transit = TM.empty ; queried = QM.empty ;
    clients = Ipaddr.Set.empty ; record_clients }

let features t = FS.elements t.features

let pick rng = function
  | [] -> None
  | [ x ] -> Some x
  | xs -> Some (List.nth xs (Randomconv.int ~bound:(List.length xs) rng))

let build_query ?id ?(recursion_desired = false) ?(checking_disabled = false) ?(dnssec_ok = true) t ts proto question retry zone edns ip =
  let id = match id with Some id -> id | None -> Randomconv.int16 t.rng in
  let header =
    let flags =
        (* tell the NS we will do the checking.
           See: https://www.rfc-editor.org/rfc/rfc4035#section-4.6 *)
        let flags =
          if FS.mem `Dnssec t.features then
            Packet.Flags.singleton `Checking_disabled
          else Packet.Flags.empty
        in
        if recursion_desired then
          Packet.Flags.add `Recursion_desired flags
        else
          flags
    in
    id, flags
  in
  let el = { ts; retry; proto; zone; edns; ip; port = 53; question; id; checking_disabled; dnssec_ok } in
  let key = question, ip, el.port, id in
  let transit =
    if TM.mem key t.transit then
      Log.warn (fun m -> m "overwriting transit of %a" pp_key question) ;
    TM.add key el t.transit
  in
  let packet = Packet.create ?edns header (question :> Packet.Question.t) `Query in
  let cs, _ = Packet.encode proto packet in
  { t with transit }, cs

let query ?recursion_desired t ts await retry ip name typ =
  let k = (name, typ) in
  let await = { await with retry = succ await.retry } in
  (* TODO here we may want to use the _default protocol_ (and edns settings) instead of `Udp *)
  let payload_size = if FS.mem `Dnssec t.features then Some 1220 (* from RFC 4035 4.1 *) else None in
  let edns = Some (Edns.create ~dnssec_ok:(FS.mem `Dnssec t.features) ?payload_size ()) in
  let t, packet = build_query ?recursion_desired ~checking_disabled:await.checking_disabled ~dnssec_ok:await.dnssec_ok t ts `Udp k retry await.zone edns ip in
  let queried =
    let q = Option.value ~default:[] (QM.find_opt k t.queried) in
    if List.exists (awaiting_eq await) q then q else await :: q
  in
  let t = { t with queried = QM.add k queried t.queried } in
  Log.debug (fun m -> m "query: query %a %a" Ipaddr.pp ip pp_key k) ;
  (packet, ip), t

let was_in_transit t key id sender sport =
  let tm_key = key, sender, sport, id in
  match TM.find tm_key t with
  | exception Not_found ->
    Log.warn (fun m -> m "key %a not present in set (likely retransmitted)"
                  pp_key key);
    None, t
  | awaiting ->
    if Ipaddr.compare sender awaiting.ip = 0 && id = awaiting.id then
      Some (awaiting.zone, awaiting.edns), TM.remove tm_key t
    else
      (Log.warn (fun m -> m "unsolicited reply for %a (id %04X vs o_id %04X, sender %a vs o_sender %a)"
                    pp_key key id awaiting.id Ipaddr.pp sender Ipaddr.pp awaiting.ip);
       None, t)

let find_queries t k =
  match QM.find k t with
  | exception Not_found ->
    Log.warn (fun m -> m "couldn't find entry %a in map" pp_key k) ;
    t, []
  | vals ->
    QM.remove k t, vals

let handle_query ?(retry = 0) t ts awaiting =
  if Int64.sub ts awaiting.ts > Int64.shift_left retry_interval 2 then begin
    Log.warn (fun m -> m "dropping q %a from %a:%d (timed out)"
                  pp_key awaiting.question Ipaddr.pp awaiting.ip awaiting.port);
    `Nothing, t
  end else
    let dnssec = FS.mem `Dnssec t.features && not awaiting.checking_disabled in
    let qname_minimisation = FS.mem `Qname_minimisation t.features in
    let r, cache = Dns_resolver_cache.handle_query t.cache ~qname_minimisation ~dnssec ~dnssec_ok:awaiting.dnssec_ok ~rng:t.rng t.ip_protocol ts awaiting.question in
    let t = { t with cache } in
    match r with
    | `Queries _ when awaiting.retry >= 10 ->
      Log.warn (fun m -> m "dropping q %a from %a:%d (already sent 10 packets)"
                   pp_key awaiting.question Ipaddr.pp awaiting.ip awaiting.port);
      (* TODO reply with error! *)
      `Nothing, t
    | `Queries [] ->
      Log.warn (fun m -> m "dropping q %a from %a:%d (queries is empty)"
                   pp_key awaiting.question Ipaddr.pp awaiting.ip awaiting.port);
      `Nothing, t
    | `Queries qs ->
      let query_one (acc, t) (zone, (nam, types), ip) =
        Log.debug (fun m -> m "have to query (zone %a) %a using ip %a"
                      Domain_name.pp zone
                      Fmt.(list ~sep:(any ", ") pp_key)
                      (List.map (fun t -> (nam, t)) types)
                      Ipaddr.pp ip);
        let await = { awaiting with zone } in
        List.fold_left (fun (acc, t) typ ->
            let r, t = query t ts await retry ip nam typ in
            r :: acc, t)
          (acc, t) types
      in
      let r, t = List.fold_left query_one ([], t) qs in
      `Query r, t
    | `Reply (flags, answer, additional) ->
      let time = Int64.sub ts awaiting.ts in
      let max_size, edns = Edns.reply awaiting.edns in
      let packet = Packet.create ?edns ?additional (awaiting.id, flags) (awaiting.question :> Packet.Question.t) (answer :> Packet.data) in
      Log.debug (fun m -> m "answering %a after %a %d out packets: %a"
                    pp_key awaiting.question Duration.pp time awaiting.retry
                    Packet.pp packet) ;
      Dns_resolver_metrics.response_metric time;
      let cs, _ = Packet.encode ?max_size awaiting.proto packet in
      let ttl = Packet.minimum_ttl (answer :> Packet.data) in
      `Answer (ttl, cs), t

let scrub_it t proto zone edns ts ~signed qtype p =
  match Dns_resolver_utils.scrub zone ~signed qtype p, edns with
  | Ok xs, _ ->
    let cache =
      List.fold_left
        (fun t (n, Dns_resolver_utils.E (ty, e), r) ->
           (*Log.debug (fun m -> m "Dns_cache.set %a %a %a"
                            Rr_map.ppk (K ty) Domain_name.pp n (Dns_cache.pp_entry ty) e) ;*)
           Dns_cache.set t ts n ty r e)
        t xs
    in
    if Packet.Flags.mem `Truncation (snd p.header) && proto = `Udp then
      (Log.warn (fun m -> m "NS truncated reply, using TCP now") ;
       `Upgrade_to_tcp cache)
    else
      `Cache cache
  | Error Rcode.FormErr, Some _ ->
    Log.warn (fun m -> m "NS sent FormErr, retrying without edns!") ;
    `Query_without_edns
  | Error e, _ ->
    Log.warn (fun m -> m "NS didn't like us %a" Rcode.pp e) ;
    `Try_another_ns

let handle_primary t now ts proto sender sport packet _request buf =
  (* makes only sense to ask primary for query=true since we'll never issue questions from primary *)
  let handle_inner name =
    let t, answer, _, _ = Dns_server.Primary.handle_packet t now ts proto sender sport packet name in
    match answer with
    | None -> `None (* TODO incoming ??? are never replied to - should be revised!? *)
    | Some reply ->
      (* delegation if authoritative is not set! *)
      if Packet.Flags.mem `Authoritative (snd reply.header) then begin
        Log.debug (fun m -> m "authoritative reply %a" Packet.pp reply) ;
        let reply =
          match Dns_block.edns reply with
          | None -> reply
          | Some edns ->
            Dns_resolver_metrics.resolver_stats `Blocked;
            Dns.Packet.with_edns reply (Some edns)
        in
        let r = Packet.encode proto reply in
        let ttl = Packet.minimum_ttl reply.data in
        `Reply (t, (reply, ttl, r))
      end else match reply.data with
        | `Answer data -> `Delegation (data, reply.additional)
        | _ -> `None (* not authoritative, error!! *)
  in
  match Dns_server.(handle_tsig (Primary.server t) now packet buf) with
  | Error (e, data) ->
    Log.err (fun m -> m "tsig failed %a" Tsig_op.pp_e e);
    begin match data with
      | Some data -> `Reply (t, 0l, data)
      | None -> `None
    end
  | Ok None ->
    begin match handle_inner None with
      | `Reply (t, (_, ttl, (out, _))) -> `Reply (t, ttl, out)
      | `None -> `None
      | `Delegation d -> `Delegation d
    end
  | Ok (Some (name, tsig, mac, key)) ->
    match handle_inner (Some name) with
    | `Reply (t, (reply, ttl, (buf, max_size))) ->
      begin match Dns_server.((Primary.server t).tsig_sign) ~max_size ~mac name tsig ~key reply buf with
        | None ->
          Log.warn (fun m -> m "couldn't use %a to tsig sign, using unsigned reply" Domain_name.pp name) ;
          `Reply (t, ttl, buf)
        | Some (buf, _) -> `Reply (t, 0l, buf)
      end
    | `None -> `None
    | `Delegation x -> `Delegation x

let handle_awaiting_queries ?retry t ts (name, typ) =
  let queried, values = find_queries t.queried (name, typ) in
  let t = { t with queried } in
  List.fold_left (fun (t, out_a, out_q) awaiting ->
      Log.debug (fun m -> m "now querying %a" pp_key awaiting.question) ;
      match handle_query ?retry t ts awaiting with
      | `Nothing, t -> t, out_a, out_q
      | `Query pkts, t -> t, out_a, (List.map (fun (pkt, dst) -> (`Udp, dst, pkt)) pkts) @ out_q
      | `Answer (ttl, pkt), t -> t, (awaiting.proto, awaiting.ip, awaiting.port, ttl, pkt) :: out_a, out_q)
    (t, [], []) values

let resolve t ts proto sender sport req =
  match req.Packet.data, Packet.Question.qtype req.Packet.question with
  | `Query, Some q_type ->
    Log.debug (fun m -> m "resolving %a" Packet.Question.pp req.question) ;
    if not (Packet.Flags.mem `Recursion_desired (snd req.Packet.header)) then
      Log.warn (fun m -> m "recursion not desired") ;
    (* ask the cache *)
    let checking_disabled = Packet.Flags.mem `Checking_disabled (snd req.header)
    and dnssec_ok = match req.edns with None -> false | Some edns -> edns.Edns.dnssec_ok in
    let awaiting = { ts; retry = 0; proto; zone = Domain_name.root ; edns = req.edns; ip = sender; port = sport; question = (fst req.question, q_type); id = fst req.header; checking_disabled; dnssec_ok } in
    begin match handle_query t ts awaiting with
      | `Answer (ttl, pkt), t ->
        Log.debug (fun m -> m "answer %a" Packet.Question.pp req.question) ;
        t, [ (proto, sender, sport, ttl, pkt) ], []
      | `Nothing, t ->
        Log.debug (fun m -> m "nothing %a" Packet.Question.pp req.question) ;
        t, [], [] (* TODO: send a reply!? *)
      | `Query pkts, t ->
        Log.debug (fun m -> m "query %d %a" (List.length pkts) Packet.Question.pp req.question) ;
        t, [], List.map (fun (packet, dst) -> `Udp, dst, packet) pkts
    end
  | _ ->
    Log.err (fun m -> m "ignoring %a" Packet.pp req);
    let pkt = Packet.create
        (fst req.header, Packet.Flags.empty) req.question
        (`Rcode_error (Rcode.NotImp, Packet.opcode_data req.data, None))
    in
    let buf, _ = Packet.encode proto pkt in
    t, [ proto, sender, sport, 0l, buf ], []

let handle_reply t now ts proto sender sport packet reply =
  match reply, Packet.Question.qtype packet.Packet.question with
  | `Answer _, Some qtype
  | `Rcode_error (Rcode.NXDomain, Opcode.Query, _), Some qtype
  | `Rcode_error (Rcode.ServFail, Opcode.Query, _), Some qtype ->
    begin
      Log.debug (fun m -> m "handling reply to %a" Packet.Question.pp packet.question);
      (* (a) first check whether frame was in transit! *)
      let key = fst packet.question, qtype in
      let r, transit = was_in_transit t.transit key (fst packet.header) sender sport in
      let t = { t with transit } in
      match r with
      | None -> Ok (t, [], [])
      | Some (zone, edns) ->
        (* (b) DNSSec verification of RRs *)
        let t, packet, signed =
          if FS.mem `Dnssec t.features then
            let t, dnskeys =
              match qtype with
              | `K K Rr_map.Dnskey ->
                let cache, ds = Dns_cache.get t.cache ts zone Rr_map.Ds in
                { t with cache },
                begin match ds with
                  | Ok (`Entry (_, ds_set), _) ->
                    let keys = match reply with
                      | `Answer (a, _) -> Name_rr_map.find zone Rr_map.Dnskey a
                      | _ -> None
                    in
                    let ds_set = Dnssec.filter_ds_if_sha2_present ds_set in
                    Option.map (fun (_, dnskeys) ->
                        Rr_map.Ds_set.fold (fun ds acc ->
                            match Dnssec.validate_ds zone dnskeys ds with
                            | Ok key -> Rr_map.Dnskey_set.add key acc
                            | Error `Msg msg ->
                              Log.debug (fun m -> m "couldn't validate DS (for %a): %s"
                                            Domain_name.pp zone msg);
                              acc
                            | Error `Extended e ->
                              Log.debug (fun m -> m "couldn't validate DS (for %a): %a"
                                            Domain_name.pp zone
                                            Extended_error.pp e);
                              acc)
                          ds_set Rr_map.Dnskey_set.empty)
                      keys
                  | _ ->
                    Log.warn (fun m -> m "no DS in cache for %a" Domain_name.pp zone);
                    None
                end
              | _ ->
                let cache, dnskeys = Dns_cache.get t.cache ts zone Rr_map.Dnskey in
                { t with cache },
                match dnskeys with
                | Ok (`Entry (_, dnskey_set), _) -> Some dnskey_set
                | _ ->
                  Log.warn (fun m -> m "no DNSKEYS in cache for %a" Domain_name.pp zone);
                  None
            in
            let packet, signed =
              match dnskeys with
              | None ->
                Log.warn (fun m -> m "no DNSKEY present, couldn't validate packet");
                packet, false
              | Some dnskeys ->
                match Dnssec.verify_packet now dnskeys packet with
                | Ok packet -> packet, true
                | Error `Msg msg ->
                  Log.err (fun m -> m "error %s verifying reply %a"
                              msg Packet.pp_reply reply);
                  packet, false
            in
            t, packet, signed
          else
            t, packet, false
        in
        (* (c) now we scrub and either *)
        match scrub_it t.cache proto zone edns ts ~signed qtype packet with
        | `Query_without_edns ->
          let t, cs = build_query t ts proto key 1 zone None sender in
          Log.debug (fun m -> m "resolve: requery without edns %a %a"
                         Ipaddr.pp sender pp_key key) ;
          Ok (t, [], [ `Udp, sender, cs ])
        | `Upgrade_to_tcp cache ->
          (* RFC 2181 Sec 9: correct would be to drop entire frame, and retry with tcp *)
          (* but we're happy to retrieve the partial information, it may be useful *)
          let t = { t with cache } in
          (* this may provoke the very same question again -
             but since tcp is first, that should trigger the TCP connection,
             which is then reused... ok, we may send the same query twice
             with different ids *)
          (* TODO we may want to get rid of the handle_awaiting_queries
             entirely here!? *)
          let (t, out_a, out_q), recursion_desired =
            handle_awaiting_queries t ts key, false
          in
          let edns = Some (Edns.create ~dnssec_ok:(FS.mem `Dnssec t.features) ()) in
          let t, cs = build_query ~recursion_desired t ts `Tcp key 1 zone edns sender in
          Log.debug (fun m -> m "resolve: upgrade to tcp %a %a"
                         Ipaddr.pp sender pp_key key) ;
          Ok (t, out_a, (`Tcp, sender, cs) :: out_q)
        | `Try_another_ns ->
          (* is this the right behaviour? by luck we'll use another path *)
          Ok (handle_awaiting_queries t ts key)
        | `Cache cache ->
          let t = { t with cache } in
          Ok (handle_awaiting_queries t ts key)
    end
  | v, _ ->
    Log.err (fun m -> m "ignoring reply %a" Packet.pp_reply v);
    Error ()

let handle_delegation t ts proto sender sport req (delegation, add_data) =
  Log.debug (fun m -> m "handling delegation %a (for %a)" Packet.Answer.pp delegation Packet.pp req) ;
  match req.Packet.data, Packet.Question.qtype req.question with
  | `Query, Some qtype ->
    let dnssec = FS.mem `Dnssec t.features && not (Packet.Flags.mem `Checking_disabled (snd req.header))
    and dnssec_ok = match req.edns with None -> false | Some edns -> edns.Edns.dnssec_ok
    in
    let r, cache = Dns_resolver_cache.answer ~dnssec ~dnssec_ok t.cache ts (fst req.question) qtype in
    let t = { t with cache } in
    begin match r with
      | `Query name ->
        (* we should look into delegation for the actual delegation name,
           but instead we're looking for any glue (A) in additional *)
        let ips =
          let ip4s, ip6s =
            Domain_name.Map.fold (fun _ rrmap (ip4s, ip6s) ->
                (match Rr_map.(find A rrmap) with
                 | None -> ip4s
                 | Some (_, ip4s') -> Ipaddr.V4.Set.union ip4s ip4s'),
                (match Rr_map.(find Aaaa rrmap) with
                 | None -> ip6s
                 | Some (_, ip6s') -> Ipaddr.V6.Set.union ip6s ip6s'))
            add_data (Ipaddr.V4.Set.empty, Ipaddr.V6.Set.empty)
          in
          let ip4s = List.map (fun ip -> Ipaddr.V4 ip) (Ipaddr.V4.Set.elements ip4s)
          and ip6s = List.map (fun ip -> Ipaddr.V6 ip) (Ipaddr.V6.Set.elements ip6s)
          in
          match t.ip_protocol with
            | `Both -> ip4s @ ip6s
            | `Ipv4_only -> ip4s
            | `Ipv6_only -> ip6s
        in
        begin match pick t.rng ips with
          | None ->
            Log.err (fun m -> m "something is wrong, delegation but no IP");
            t, [], []
          | Some ip ->
            Log.debug (fun m -> m "found ip %a, maybe querying %a"
                           Ipaddr.pp ip pp_key (name, qtype)) ;
            (* TODO is Domain_name.root correct here? *)
            let checking_disabled = Packet.Flags.mem `Checking_disabled (snd req.header)
            and dnssec_ok = match req.edns with None -> false | Some edns -> edns.Edns.dnssec_ok
            in
            let await = { ts; retry = 0; proto; zone = Domain_name.root; edns = req.edns; ip = sender; port = sport; question = (fst req.question, qtype); id = fst req.header; checking_disabled; dnssec_ok } in
            let (cs, ip), t = query ~recursion_desired:true t ts await 0 ip name qtype in
            t, [], [ `Udp, ip, cs ]
        end
      | `Packet (flags, reply, additional) ->
        let max_size, edns = Edns.reply req.edns in
        Log.debug (fun m -> m "delegation reply for %a from cache: %a"
                       Packet.pp req Packet.pp_reply reply) ;
        let packet = Packet.create ?edns ?additional (fst req.header, flags) req.question (reply :> Packet.data) in
        let ttl = Packet.minimum_ttl (reply :> Packet.data) in
        let pkt, _ = Packet.encode ?max_size proto packet in
        Dns_resolver_metrics.response_metric 0L;
        t, [ proto, sender, sport, ttl, pkt ], []
        (* send it out! we've a cache hit here! *)
    end
  | _ ->
    Log.err (fun m -> m "ignoring %a" Packet.pp req) ;
    let pkt =
      Packet.create (fst req.header, Packet.Flags.empty)
        req.question (`Rcode_error (Rcode.NotImp, Packet.opcode_data req.data, None))
    in
    t, [ proto, sender, sport, 0l, fst (Packet.encode proto pkt) ], []

let handle_buf t now ts query_allowed proto sender sport buf =
  match Packet.decode buf with
(*  | Error (`Bad_edns_version v) ->
    Log.err (fun m -> m "bad edns version (from %a:%d) %u for@.%a"
                 Ipaddr.pp sender sport
                 v Cstruct.hexdump_pp buf) ;
    t, handle_error ~error:Dns_enum.BadVersOrSig proto sender sport buf, [] *)
  | Error e ->
    Dns_resolver_metrics.resolver_stats `Error;
    Log.err (fun m -> m "decode error (from %a:%d) %a for@.%a"
                 Ipaddr.pp sender sport
                 Packet.pp_err e Ohex.pp buf) ;
    let answer = match Packet.raw_error buf Rcode.FormErr with
      | None -> []
      | Some data -> [ proto, sender, sport, 0l, data ]
    in
    t, answer, []
  | Ok res ->
    Log.debug (fun m -> m "reacting to packet from %a:%d"
                   Ipaddr.pp sender sport) ;
    match res.Packet.data with
    | #Packet.reply as reply ->
      begin
        match handle_reply t now ts proto sender sport res reply with
        | Ok a ->
          Log.debug (fun m -> m "handled reply %a:%d"
                        Ipaddr.pp sender sport) ;
          a
        | Error () -> t, [], []
      end
    | #Packet.request as req when query_allowed ->
      Dns_resolver_metrics.resolver_stats `Queries;
      if t.record_clients then
        if not (Ipaddr.Set.mem sender t.clients) then begin
          t.clients <- Ipaddr.Set.add sender t.clients;
          Dns_resolver_metrics.resolver_stats `Clients
        end;
      begin
        match handle_primary t.primary now ts proto sender sport res req buf with
        | `Reply (primary, ttl, pkt) ->
          Dns_resolver_metrics.response_metric 0L;
          Log.debug (fun m -> m "handled primary %a:%d" Ipaddr.pp sender sport) ;
          { t with primary }, [ proto, sender, sport, ttl, pkt ], []
        | `Delegation dele ->
          Log.debug (fun m -> m "handled delegation %a:%d" Ipaddr.pp sender sport) ;
          handle_delegation t ts proto sender sport res dele
        | `None ->
          Log.debug (fun m -> m "resolving %a:%d" Ipaddr.pp sender sport) ;
          (* DNSSEC request DS / DNSKEY / NS from auth *)
          resolve t ts proto sender sport res
      end
    | _ ->
      Log.err (fun m -> m "ignoring unsolicited packet (query allowed? %b) %a" query_allowed Packet.pp res);
      t, [], []

let query_root t now proto =
  let root_ip () =
    match pick t.rng (Dns_resolver_root.ips t.ip_protocol) with
    | None -> assert false
    | Some x -> x
  in
  let ip =
    match Dns_cache.get t.cache now Domain_name.root Ns with
    | _, Ok (`Entry (_, names), _) ->
      let ip4s, ip6s =
        Domain_name.Host_set.fold (fun name (v4s, v6s) ->
            (match snd (Dns_cache.get t.cache now (Domain_name.raw name) A) with
             | Ok (`Entry (_, ips), _) -> Ipaddr.V4.Set.union ips v4s
             | _ -> v4s),
            (match snd (Dns_cache.get t.cache now (Domain_name.raw name) Aaaa) with
             | Ok (`Entry (_, ips), _) -> Ipaddr.V6.Set.union ips v6s
             | _ -> v6s))
          names (Ipaddr.V4.Set.empty, Ipaddr.V6.Set.empty)
      in
      let ip4s = List.map (fun ip -> Ipaddr.V4 ip) (Ipaddr.V4.Set.elements ip4s)
      and ip6s = List.map (fun ip -> Ipaddr.V6 ip) (Ipaddr.V6.Set.elements ip6s)
      in
      let ips = match t.ip_protocol with
        | `Both -> ip4s @ ip6s
        | `Ipv4_only -> ip4s
        | `Ipv6_only -> ip6s
      in
      begin match pick t.rng ips with
        | Some ip -> ip
        | None -> root_ip ()
      end
    | _ -> root_ip ()
  in
  let question = Domain_name.root, `K (Rr_map.K Ns)
  and id = Randomconv.int16 t.rng
  and edns = Some (Edns.create ())
  and checking_disabled = false
  and dnssec_ok = true
  in
  let el =
    { ts = now; retry = 0; proto; zone = Domain_name.root; edns; ip; port = 53; question; id; checking_disabled; dnssec_ok }
  in
  let key = question, ip, el.port, id in
  let t = { t with transit = TM.add key el t.transit } in
  let packet = Packet.create ?edns (id, Packet.Flags.empty) question `Query in
  let cs, _ = Packet.encode proto packet in
  t, (proto, ip, cs)

let max_retries = 5

let err_retries t question =
  let t, reqs = find_queries t question in
  t, List.fold_left (fun acc awaiting ->
      Log.debug (fun m -> m "now erroring to %a" pp_key awaiting.question) ;
      let packet = Packet.create (awaiting.id, Packet.Flags.empty)
          (awaiting.question :> Packet.Question.t)
          (`Rcode_error (Rcode.ServFail, Opcode.Query, None))
      in
      let buf, _ = Packet.encode awaiting.proto packet in
      (awaiting.proto, awaiting.ip, awaiting.port, 0l, buf) :: acc)
    [] reqs

let timer t ts =
  let transit, rem =
    TM.partition
      (fun _ awaiting -> Int64.sub ts awaiting.ts < retry_interval)
      t.transit
  in
  let t = { t with transit } in
  if not (TM.is_empty transit && TM.is_empty rem) then
    Log.debug (fun m -> m "try_other timer wheel -- keeping %d, running over %d"
                   (TM.cardinal transit) (TM.cardinal rem)) ;
  TM.fold (fun ((name, typ), _ip, _port, _id) awaiting (t, out_a, out_q) ->
      let retry = succ awaiting.retry in
      if retry < max_retries then begin
        let t, outa, outq = handle_awaiting_queries ~retry t ts (name, typ) in
        (t, outa @ out_a, outq @ out_q)
      end else begin
        Log.info (fun m -> m "retry limit exceeded for %a at %a!"
                      pp_key (name, typ) Ipaddr.pp awaiting.ip) ;
        let queried, out_as = err_retries t.queried (name, typ) in
        ({ t with queried }, out_as @ out_a, out_q)
      end)
    rem (t, [], [])

let primary_data t = Dns_server.Primary.data t.primary

let with_primary_data t now ts data =
  let primary, outs = Dns_server.Primary.with_data t.primary now ts data in
  { t with primary }, outs
