123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)openDnsopenDns_resolver_cacheletsrc=Logs.Src.create"dns_resolver_util"~doc:"DNS resolver util"moduleLog=(valLogs.src_logsrc:Logs.LOG)typee=E:'aRr_map.key*'aDns_cache.entry->eletinvalid_soaname=letppre=Result.value~default:name(Result.bind(Domain_name.prepend_labelname"invalid")(funn->Domain_name.prepend_labelnpre))in{Soa.nameserver=p"ns";hostmaster=p"hostmaster";serial=1l;refresh=16384l;retry=2048l;expiry=1048576l;minimum=300l}letrrsigrr_mapty=matchRr_map.findRrsigrr_mapwith|Somev->Rr_map.Rrsig_set.find_first_opt(funrrsig->rrsig.Rrsig.type_covered=Rr_map.to_intty)(sndv)|None->Noneletnoerrorbailiwick(_,flags)~signedq_nameq_type(answer,authority)additional=(* maybe should be passed explicitly (when we don't do qname minimisation) *)letin_bailiwickname=Domain_name.is_subdomain~domain:bailiwick~subdomain:namein(* ANSWER *)letanswers,anames=matchDomain_name.Map.findq_nameanswerwith|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? *)letrankrrsig=ifPacket.Flags.mem`AuthoritativeflagsthenDns_cache.AuthoritativeAuthority(ifsignedthenrrsigelseNone)elseDns_cache.AdditionalinmatchDomain_name.Map.fold(funnamerr_mapacc->ifDomain_name.is_subdomain~subdomain:q_name~domain:namethenmatchRr_map.findSoarr_mapwith|Somesoa->(name,soa,rr_map)::acc|None->accelseacc)authority[]with|(name,soa,rr_map)::_->beginmatchq_typewith|`Any->[](* i really don't know how to handle ANY NoDATA*)|`KRr_map.Kk->[q_name,E(k,`No_data(name,soa)),rank(rrsigrr_mapSoa)](* 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|Somerr_map->letrankrrsig=ifPacket.Flags.mem`AuthoritativeflagsthenDns_cache.AuthoritativeAnswer(ifsignedthenrrsigelseNone)elseDns_cache.NonAuthoritativeAnswerin(* collect those rrsets which are of interest depending on q_type! *)matchq_typewith|`Any->Rr_map.fold(fun(B(k,v))(acc,names)->(q_name,E(k,`Entryv),rank(rrsigrr_mapk))::acc,Domain_name.Host_set.fold(funnacc->Domain_name.Set.add(Domain_name.rawn)acc)(Rr_map.nameskv)names)rr_map([],Domain_name.Set.empty)|`K(Rr_map.KCname)->beginmatchRr_map.findCnamerr_mapwith|Somev->[q_name,E(Cname,`Entryv),rank(rrsigrr_mapCname)],Domain_name.Host_set.fold(funnacc->Domain_name.Set.add(Domain_name.rawn)acc)(Rr_map.namesCnamev)Domain_name.Set.empty|None->(* case no cname *)Log.warn(funm->m"noerror answer with right name, but no cname in %a, invalid soa for %a"Name_rr_map.ppanswerpp_question(q_name,q_type));[q_name,E(Cname,`No_data(q_name,invalid_soaq_name)),rankNone],Domain_name.Set.emptyend|`K(Rr_map.Kk)->matchRr_map.findkrr_mapwith|Somev->[q_name,E(k,`Entryv),rank(rrsigrr_mapk)],Domain_name.Host_set.fold(funnacc->Domain_name.Set.add(Domain_name.rawn)acc)(Rr_map.nameskv)Domain_name.Set.empty|None->matchRr_map.findCnamerr_mapwith|None->(* case neither TYP nor cname *)Log.warn(funm->m"noerror answer with right name, but not TYP nor cname in %a, invalid soa for %a"Name_rr_map.ppanswerpp_question(q_name,q_type));[q_name,E(k,`No_data(q_name,invalid_soaq_name)),rankNone],Domain_name.Set.empty|Somecname->(* explicitly register as CNAME so it'll be found *)(* should we try to find further records for the new alias? *)[q_name,E(Cname,`Entrycname),rank(rrsigrr_mapCname)],Domain_name.Set.singleton(sndcname)in(* AUTHORITY - NS and DS records, also nsec and nsec3 *)letns,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 *)letranks=ifPacket.Flags.mem`AuthoritativeflagsthenDns_cache.AuthoritativeAuthority(ifsignedthenselseNone)elseDns_cache.Additionalinletns,others,names=Domain_name.Map.fold(funnamemap(ns_acc,other_acc,s)->ifin_bailiwicknamethenletns,s=matchRr_map.findNsmapwith|None->ns_acc,s|Some(ns:int32*Domain_name.Host_set.t)->(name,ns)::ns_acc,Domain_name.Host_set.fold(funnacc->Domain_name.Set.add(Domain_name.rawn)acc)(sndns)sinletothers=matchRr_map.findNsecmapwith|None->other_acc|Somen->(name,E(Nsec,`Entryn),rank(rrsigmapNsec))::other_accinletothers=matchRr_map.findNsec3mapwith|None->others|Somen->(name,E(Nsec3,`Entryn),rank(rrsigmapNsec3))::othersinletothers=matchRr_map.findDsmapwith|None->others|Somen->(name,E(Ds,`Entryn),rank(rrsigmapDs))::othersinns,others,selsens_acc,other_acc,s)authority([],[],Domain_name.Set.empty)inList.fold_left(funacc(name,ns)->(name,E(Ns,`Entryns),rankNone)::acc)othersns,namesin(* 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 *)letglues=letnames=Domain_name.Set.unionanamesnsnamesinletnames=Domain_name.Set.filterin_bailiwicknamesinDomain_name.Set.fold(funnameacc->matchDomain_name.Map.findnameadditionalwith|None->acc|Somemap->leta=matchRr_map.findAmapwith|None->acc|Somev->(name,E(A,`Entryv),Dns_cache.Additional)::accinmatchRr_map.findAaaamapwith|None->a|Somev->(name,E(Aaaa,`Entryv),Dns_cache.Additional)::a)names[]in(* This is defined in RFC2181, Sec9 -- answer is unique if authority or
additional is non-empty *)letanswer_complete=not(Domain_name.Map.is_emptyauthority&&Domain_name.Map.is_emptyadditional)inmatchanswers,nswith|[],[]whennotanswer_complete&&Packet.Flags.mem`Truncationflags->(* special handling for truncated replies.. better not add anything *)Log.warn(funm->m"truncated reply for %a, ignoring completely"pp_question(q_name,q_type));[]|[],[]->(* not sure if this can happen, maybe discard everything? *)Log.warn(funm->m"reply without answers or ns invalid so for %a"pp_question(q_name,q_type));beginmatchq_typewith|`Any->[]|`KRr_map.Kk->[q_name,E(k,`No_data(q_name,invalid_soaq_name)),Dns_cache.Additional]end|_,_->answers@ns@gluesletfind_soanameauthority=letrecgoname=matchDomain_name.Map.findnameauthoritywith|None->go(Domain_name.drop_label_exnname)|Somerrmap->matchRr_map.(findSoarrmap)with|None->go(Domain_name.drop_label_exnname)|Somesoa->name,soa,rrsigrrmapSoaintrySome(goname)withInvalid_argument_->Noneletnxdomain(_,flags)~signednamedata=(* 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) *)letanswer,authority=matchdatawith|None->Name_rr_map.empty,Name_rr_map.empty|Somex->xinletcnames=letrecgoaccname=matchDomain_name.Map.findnameanswerwith|None->acc|Somerrmap->matchRr_map.(findCnamerrmap)with|None->acc|Some(ttl,alias)->go((name,(ttl,alias),rrsigrrmapCname)::acc)aliasingo[]nameinletsoa=find_soanameauthorityin(* since NXDomain have CNAME semantics, we store them as CNAME *)letrankrrsig=ifPacket.Flags.mem`AuthoritativeflagsthenDns_cache.AuthoritativeAnswer(ifsignedthenrrsigelseNone)elseDns_cache.NonAuthoritativeAnswerin(* 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
*)letentries=letsoa_name,soa,rrsig=matchsoawith|None->name,invalid_soaname,None|Somex->xinmatchcnameswith|[]->[name,E(Cname,`No_domain(soa_name,soa)),rrsig]|rrs->List.map(fun(name,cname,rrsig)->(name,E(Cname,`Entrycname),rrsig))rrsin(* the cname does not matter *)List.map(fun(name,res,rrsig)->name,res,rankrrsig)entriesletscrubzone~signedqtypep=Log.debug(funm->m"scrubbing (bailiwick %a) data %a"Domain_name.ppzonePacket.ppp);letqname=fstp.questioninmatchp.Packet.datawith|`Answerdata->Ok(noerrorzonep.header~signedqnameqtypedatap.additional)|`Rcode_error(Rcode.NXDomain,_,data)->Ok(nxdomainp.Packet.header~signedqnamedata)|e->Error(Packet.rcode_datae)