(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *) open Dns open Dns_resolver_cache let src = Logs.Src.create "dns_resolver_util" ~doc:"DNS resolver util" module Log = (val Logs.src_log src : Logs.LOG) type e = E : 'a Rr_map.key * 'a Dns_cache.entry -> e let invalid_soa name = let p pre = Result.value ~default:name (Result.bind (Domain_name.prepend_label name "invalid") (fun n -> Domain_name.prepend_label n pre)) in { Soa.nameserver = p "ns" ; hostmaster = p "hostmaster" ; serial = 1l ; refresh = 16384l ; retry = 2048l ; expiry = 1048576l ; minimum = 300l } let rrsig rr_map ty = match Rr_map.find Rrsig rr_map with | Some v -> Rr_map.Rrsig_set.find_first_opt (fun rrsig -> rrsig.Rrsig.type_covered = Rr_map.to_int ty) (snd v) | None -> None let noerror bailiwick (_, flags) ~signed q_name q_type (answer, authority) additional = (* maybe should be passed explicitly (when we don't do qname minimisation) *) let in_bailiwick name = Domain_name.is_subdomain ~domain:bailiwick ~subdomain:name in (* ANSWER *) let answers, anames = match Domain_name.Map.find q_name answer with | None -> (* NODATA (no answer, but SOA (or not) in authority) *) begin (* RFC2308, Sec 2.2 "No data": - answer is empty - authority has a) SOA + NS, b) SOA, or c) nothing *) (* an example for this behaviour is NS: asking for AAAA www.soup.io, get empty answer + SOA in authority asking for AAAA coffee.soup.io, get empty answer + authority *) (* the "sub" should be relaxed - for dig ns mail.mehnert.org I get soa in mehnert.org! --> but how to discover SOA/zone boundaries? *) let rank rrsig = if Packet.Flags.mem `Authoritative flags then Dns_cache.AuthoritativeAuthority (if signed then rrsig else None) else Dns_cache.Additional in match Domain_name.Map.fold (fun name rr_map acc -> if Domain_name.is_subdomain ~subdomain:q_name ~domain:name then match Rr_map.find Soa rr_map with | Some soa -> (name, soa, rr_map) :: acc | None -> acc else acc) authority [] with | (name, soa, rr_map)::_ -> begin match q_type with | `Any -> [] (* i really don't know how to handle ANY NoDATA*) | `K Rr_map.K k -> [ q_name, E (k, `No_data (name, soa)), rank (rrsig rr_map Soa) ] (* this is wrong for the normal iterative algorithm: it asks for foo.com @root, and get .com NS in AU and A in AD | [] when not (Packet.Header.FS.mem `Truncation flags) -> Log.warn (fun m -> m "noerror answer, but nothing in authority whose sub is %a in %a, invalid_soa!" pp_question (q_name, q_type) Name_rr_map.pp authority) ; [ q_type, q_name, Additional, `No_data (q_name, invalid_soa q_name) ] *) end | [] -> [] (* general case when we get an answer from root server *) end, Domain_name.Set.empty | Some rr_map -> let rank rrsig = if Packet.Flags.mem `Authoritative flags then Dns_cache.AuthoritativeAnswer (if signed then rrsig else None) else Dns_cache.NonAuthoritativeAnswer in (* collect those rrsets which are of interest depending on q_type! *) match q_type with | `Any -> Rr_map.fold (fun (B (k, v)) (acc, names) -> (q_name, E (k, `Entry v), rank (rrsig rr_map k)) :: acc, Domain_name.Host_set.fold (fun n acc -> Domain_name.Set.add (Domain_name.raw n) acc) (Rr_map.names k v) names) rr_map ([], Domain_name.Set.empty) | `K (Rr_map.K Cname) -> begin match Rr_map.find Cname rr_map with | Some v -> [ q_name, E (Cname, `Entry v), rank (rrsig rr_map Cname) ], Domain_name.Host_set.fold (fun n acc -> Domain_name.Set.add (Domain_name.raw n) acc) (Rr_map.names Cname v) Domain_name.Set.empty | None -> (* case no cname *) Log.warn (fun m -> m "noerror answer with right name, but no cname in %a, invalid soa for %a" Name_rr_map.pp answer pp_question (q_name, q_type)); [ q_name, E (Cname, `No_data (q_name, invalid_soa q_name)), rank None ], Domain_name.Set.empty end | `K (Rr_map.K k) -> match Rr_map.find k rr_map with | Some v -> [ q_name, E (k, `Entry v), rank (rrsig rr_map k) ], Domain_name.Host_set.fold (fun n acc -> Domain_name.Set.add (Domain_name.raw n) acc) (Rr_map.names k v) Domain_name.Set.empty | None -> match Rr_map.find Cname rr_map with | None -> (* case neither TYP nor cname *) Log.warn (fun m -> m "noerror answer with right name, but not TYP nor cname in %a, invalid soa for %a" Name_rr_map.pp answer pp_question (q_name, q_type)); [ q_name, E (k, `No_data (q_name, invalid_soa q_name)), rank None ], Domain_name.Set.empty | Some cname -> (* explicitly register as CNAME so it'll be found *) (* should we try to find further records for the new alias? *) [ q_name, E (Cname, `Entry cname), rank (rrsig rr_map Cname) ], Domain_name.Set.singleton (snd cname) in (* AUTHORITY - NS and DS records, also nsec and nsec3 *) let ns, nsnames = (* authority points us to NS of q_name! *) (* we collect a list of NS records and the ns names *) (* TODO need to be more careful, q: foo.com a: foo.com a 1.2.3.4 au: foo.com ns blablubb.com ad: blablubb.com A 1.2.3.4 *) let rank s = if Packet.Flags.mem `Authoritative flags then Dns_cache.AuthoritativeAuthority (if signed then s else None) else Dns_cache.Additional in let ns, others, names = Domain_name.Map.fold (fun name map (ns_acc, other_acc, s) -> if in_bailiwick name then let ns, s = match Rr_map.find Ns map with | None -> ns_acc, s | Some (ns : int32 * Domain_name.Host_set.t) -> (name, ns) :: ns_acc, Domain_name.Host_set.fold (fun n acc -> Domain_name.Set.add (Domain_name.raw n) acc) (snd ns) s in let others = match Rr_map.find Nsec map with | None -> other_acc | Some n -> (name, E (Nsec, `Entry n), rank (rrsig map Nsec)) :: other_acc in let others = match Rr_map.find Nsec3 map with | None -> others | Some n -> (name, E (Nsec3, `Entry n), rank (rrsig map Nsec3)) :: others in let others = match Rr_map.find Ds map with | None -> others | Some n -> (name, E (Ds, `Entry n), rank (rrsig map Ds)) :: others in ns, others, s else ns_acc, other_acc, s) authority ([], [], Domain_name.Set.empty) in List.fold_left (fun acc (name, ns) -> (name, E (Ns, `Entry ns), rank None) :: acc) others ns, names in (* ADDITIONAL *) (* maybe only these thingies which are subdomains of q_name? *) (* preserve A/AAAA records only for NS lookups? *) (* now we have processed: - answer (filtered to where name = q_name) - authority with SOA and NS entries - names from these answers, and authority - additional section can contain glue records if needed - only A and AAAA records are of interest for glue *) let glues = let names = Domain_name.Set.union anames nsnames in let names = Domain_name.Set.filter in_bailiwick names in Domain_name.Set.fold (fun name acc -> match Domain_name.Map.find name additional with | None -> acc | Some map -> let a = match Rr_map.find A map with | None -> acc | Some v -> (name, E (A, `Entry v), Dns_cache.Additional) :: acc in match Rr_map.find Aaaa map with | None -> a | Some v -> (name, E (Aaaa, `Entry v), Dns_cache.Additional) :: a) names [] in (* This is defined in RFC2181, Sec9 -- answer is unique if authority or additional is non-empty *) let answer_complete = not (Domain_name.Map.is_empty authority && Domain_name.Map.is_empty additional) in match answers, ns with | [], [] when not answer_complete && Packet.Flags.mem `Truncation flags -> (* special handling for truncated replies.. better not add anything *) Log.warn (fun m -> m "truncated reply for %a, ignoring completely" pp_question (q_name, q_type)); [] | [], [] -> (* not sure if this can happen, maybe discard everything? *) Log.warn (fun m -> m "reply without answers or ns invalid so for %a" pp_question (q_name, q_type)); begin match q_type with | `Any -> [] | `K Rr_map.K k -> [ q_name, E (k,`No_data (q_name, invalid_soa q_name)), Dns_cache.Additional ] end | _, _ -> answers @ ns @ glues let find_soa name authority = let rec go name = match Domain_name.Map.find name authority with | None -> go (Domain_name.drop_label_exn name) | Some rrmap -> match Rr_map.(find Soa rrmap) with | None -> go (Domain_name.drop_label_exn name) | Some soa -> name, soa, rrsig rrmap Soa in try Some (go name) with Invalid_argument _ -> None let nxdomain (_, flags) ~signed name data = (* we can't do much if authoritiative is not set (some auth dns do so) *) (* There are cases where answer is non-empty, but contains a CNAME *) (* RFC 2308 Sec 1 + 2.1 show that NXDomain is for the last QNAME! *) (* -> need to potentially extract CNAME(s) *) let answer, authority = match data with | None -> Name_rr_map.empty, Name_rr_map.empty | Some x -> x in let cnames = let rec go acc name = match Domain_name.Map.find name answer with | None -> acc | Some rrmap -> match Rr_map.(find Cname rrmap) with | None -> acc | Some (ttl, alias) -> go ((name, (ttl, alias), rrsig rrmap Cname) :: acc) alias in go [] name in let soa = find_soa name authority in (* since NXDomain have CNAME semantics, we store them as CNAME *) let rank rrsig = if Packet.Flags.mem `Authoritative flags then Dns_cache.AuthoritativeAnswer (if signed then rrsig else None) else Dns_cache.NonAuthoritativeAnswer in (* we conclude NXDomain, there are 3 cases we care about: no soa in authority and no cname answer -> inject an invalid_soa (avoid loops) a matching soa, no cname -> NoDom q_name _, a matching cname -> NoErr q_name with cname *) let entries = let soa_name, soa, rrsig = match soa with | None -> name, invalid_soa name, None | Some x -> x in match cnames with | [] -> [ name, E (Cname, `No_domain (soa_name, soa)), rrsig ] | rrs -> List.map (fun (name, cname, rrsig) -> (name, E (Cname, `Entry cname), rrsig)) rrs in (* the cname does not matter *) List.map (fun (name, res, rrsig) -> name, res, rank rrsig) entries let scrub zone ~signed qtype p = Log.debug (fun m -> m "scrubbing (bailiwick %a) data %a" Domain_name.pp zone Packet.pp p); let qname = fst p.question in match p.Packet.data with | `Answer data -> Ok (noerror zone p.header ~signed qname qtype data p.additional) | `Rcode_error (Rcode.NXDomain, _, data) -> Ok (nxdomain p.Packet.header ~signed qname data) | e -> Error (Packet.rcode_data e)