package dns-resolver

  1. Overview
  2. Docs

Source file dns_resolver_utils.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
(* (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)