let ip = let module M = struct type t = Ipaddr.V4.t let pp = Ipaddr.V4.pp let equal a b = Ipaddr.V4.compare a b = 0 end in (module M : Alcotest.TESTABLE with type t = M.t) let ipset = Alcotest.(slist ip Ipaddr.V4.compare) let p_cs = Alcotest.testable (Ohex.pp_hexdump ()) String.equal module Make_query_tests = struct let produces_same_output () = let rng n = String.make n '\x00' in let name:'a Domain_name.t = Domain_name.of_string_exn "example.com" in let actual, _state = Dns_client.Pure.make_query rng `Tcp `Auto name Dns.Rr_map.A in let expected = Ohex.decode "00 2e 00 00 01 00 00 01 00 00 00 00 00 01 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 00 01 00 01 00 00 29 02 00 00 00 00 00 00 06 00 0b 00 02 04 b0" in Alcotest.check p_cs "produces cool stuff" expected actual let tests = [ "produces same output", `Quick, produces_same_output; "fails on unspported query_type", `Quick, produces_same_output; ] end module Parse_response_tests = struct let unpacks_response () = (* Bytes 3, 4 are set to `00 00` - these represent query ID *) let ipv4_buf = Ohex.decode "00 77 00 00 81 80 00 01 00 01 00 02 00 02 03 66 6f 6f 03 63 6f 6d 00 00 01 00 01 c0 0c 00 01 00 01 00 00 02 2a 00 04 17 17 56 2c c0 0c 00 02 00 01 00 01 87 ae 00 10 03 6e 73 31 09 64 69 67 69 6d 65 64 69 61 c0 10 c0 0c 00 02 00 01 00 01 87 ae 00 06 03 6e 73 32 c0 39 c0 35 00 01 00 01 00 02 40 8a 00 04 17 15 f2 58 c0 51 00 01 00 01 00 02 40 8a 00 04 17 15 f3 77" in (* This `rng` generates zeros, used for the query ID above *) let rng n = String.make n '\x00' in let name:'a Domain_name.t = Domain_name.of_string_exn "foo.com" in let _actual, state = Dns_client.Pure.make_query rng `Tcp `Auto name Dns.Rr_map.A in match Dns_client.Pure.handle_response state ipv4_buf with | Ok `Data _ -> () (* TODO: Alcotest TESTABLE for this return value *) | _ -> ignore(failwith "error") let fails_to_unpack_mismatched () = (* TODO: It is possible to use crowbar here, to generate the ipv4_buf *) (* Bytes 3, 4 are set to `aa aa` - these represent query ID *) let ipv4_buf = Ohex.decode "00 77 aa aa 81 80 00 01 00 01 00 02 00 02 03 66 6f 6f 03 63 6f 6d 00 00 01 00 01 c0 0c 00 01 00 01 00 00 02 2a 00 04 17 17 56 2c c0 0c 00 02 00 01 00 01 87 ae 00 10 03 6e 73 31 09 64 69 67 69 6d 65 64 69 61 c0 10 c0 0c 00 02 00 01 00 01 87 ae 00 06 03 6e 73 32 c0 39 c0 35 00 01 00 01 00 02 40 8a 00 04 17 15 f2 58 c0 51 00 01 00 01 00 02 40 8a 00 04 17 15 f3 77" in (* This `rng` generates zeros, used for the query ID above *) let rng n = String.make n '\x00' in let name:'a Domain_name.t = Domain_name.of_string_exn "foo.com" in let _actual, state = Dns_client.Pure.make_query rng `Tcp `Auto name Dns.Rr_map.A in match Dns_client.Pure.parse_response state ipv4_buf with | Error `Msg _ -> () | __ -> failwith "should have rejected mismatched input" (* TODO: Alcotest TESTABLE for this return value *) let tests = [ "unpacks some kind of response", `Quick, unpacks_response; "fails to unpack response with mismatching query ID", `Quick, fails_to_unpack_mismatched; ] end (* {!Transport} provides a mock implementation of the transport used by Dns_client.Make. The mock data uses the default_debug_info reference cell. *) type debug_info = string let default_debug_info = ref [] module Transport (*: Dns_client.S with type io_addr = debug_info and type stack = unit and type +'a io = 'a *) = struct type io_addr = debug_info type stack = Dns.proto type context = debug_info list ref type t = Dns.proto type +'a io = 'a let create ?nameservers:_ ~timeout:_ proto = proto let nameservers proto = proto, !default_debug_info let rng n = String.make n '\x00' let clock () = 0L let bind a b = b a let lift v = v let close _ = () let connect a = Ok (a, default_debug_info) let send_recv (mock_responses : context) _ = match !mock_responses with | [] -> failwith("nothing to recv from the wire") | hd::tail -> mock_responses := tail; Ok hd end (* Now that we have our {!Transport} implementation we can include the logic that goes on top of it: *) include Dns_client.Make(Transport) module Transport_with_time_machine = struct include Transport (* the timestamps are for: cache lookup1, cache upate, cache lookup2, cache update2 *) let timestamps = ref [0L; 0L; Duration.of_sec 601 ; Duration.of_sec 601] let clock () = match !timestamps with | [] -> assert false | head::tail -> timestamps := tail; head end module Dns_client_with_time_machine = Dns_client.Make(Transport_with_time_machine) module Gethostbyname_tests = struct let foo_com_is_valid () = let domain_name = Domain_name.(of_string_exn "foo.com" |> host_exn) in (* Bytes 3, 4 are set to `00 00` - these represent query ID *) let ipv4_buf = Ohex.decode "00 77 00 00 81 80 00 01 00 01 00 02 00 02 03 66 6f 6f 03 63 6f 6d 00 00 01 00 01 c0 0c 00 01 00 01 00 00 02 2a 00 04 17 17 56 2c c0 0c 00 02 00 01 00 01 87 ae 00 10 03 6e 73 31 09 64 69 67 69 6d 65 64 69 61 c0 10 c0 0c 00 02 00 01 00 01 87 ae 00 06 03 6e 73 32 c0 39 c0 35 00 01 00 01 00 02 40 8a 00 04 17 15 f2 58 c0 51 00 01 00 01 00 02 40 8a 00 04 17 15 f3 77" in let t = create `Tcp in default_debug_info := [ipv4_buf]; match gethostbyname t domain_name with | Ok _ip -> () | Error _ -> failwith "foo.com should have been returned" let returns_from_the_cache () = let domain_name = Domain_name.(of_string_exn "foo.com" |> host_exn) in (* Bytes 3, 4 are set to `00 00` - these represent query ID *) let ipv4_buf = Ohex.decode "00 77 00 00 81 80 00 01 00 01 00 02 00 02 03 66 6f 6f 03 63 6f 6d 00 00 01 00 01 c0 0c 00 01 00 01 00 00 02 2a 00 04 17 17 56 2c c0 0c 00 02 00 01 00 01 87 ae 00 10 03 6e 73 31 09 64 69 67 69 6d 65 64 69 61 c0 10 c0 0c 00 02 00 01 00 01 87 ae 00 06 03 6e 73 32 c0 39 c0 35 00 01 00 01 00 02 40 8a 00 04 17 15 f2 58 c0 51 00 01 00 01 00 02 40 8a 00 04 17 15 f3 77" in let t = create `Tcp in default_debug_info := [ipv4_buf]; match gethostbyname t domain_name with | Error _ -> failwith "foo.com should have been returned" | Ok _ip -> default_debug_info := []; match gethostbyname t domain_name with | Error _ -> failwith "should have been cached" | Ok _ -> () (* we returned content, but the wire stayed silent *) let uses_network_when_cache_evicted () = let domain_name = Domain_name.(of_string_exn "foo.com" |> host_exn) in (* Bytes 3, 4 are set to `00 00` - these represent query ID *) let ipv4_buf = Ohex.decode "00 77 00 00 81 80 00 01 00 01 00 02 00 02 03 66 6f 6f 03 63 6f 6d 00 00 01 00 01 c0 0c 00 01 00 01 00 00 02 2a 00 04 17 17 56 2c c0 0c 00 02 00 01 00 01 87 ae 00 10 03 6e 73 31 09 64 69 67 69 6d 65 64 69 61 c0 10 c0 0c 00 02 00 01 00 01 87 ae 00 06 03 6e 73 32 c0 39 c0 35 00 01 00 01 00 02 40 8a 00 04 17 15 f2 58 c0 51 00 01 00 01 00 02 40 8a 00 04 17 15 f3 77" in let t = Dns_client_with_time_machine.create `Tcp in default_debug_info := [ipv4_buf]; match Dns_client_with_time_machine.gethostbyname t domain_name with | Error _ -> failwith "foo.com should have been returned" | Ok _ip -> default_debug_info := [ipv4_buf]; match Dns_client_with_time_machine.gethostbyname t domain_name with | Error _ -> failwith "should have been cached" | Ok _ -> (* we returned content, AND the wire was used *) assert (!default_debug_info = []) let tests = [ "foo.com is valid", `Quick, foo_com_is_valid; "when cache is populated, return from cache", `Quick, returns_from_the_cache; "when content evicted, use network", `Quick, uses_network_when_cache_evicted; ] end module Getaddrinfo_tests = struct let supports_mx_packets () = let domain_name = Domain_name.(of_string_exn "google.com" |> host_exn) in (* a google.com MX record - bytes 3,4 are set to the query ID 00 00 *) let ipv4_buf = Ohex.decode "02 1e 00 00 81 80 00 01 00 05 00 04 00 0f 06 67 6f 6f 67 6c 65 03 63 6f 6d 00 00 0f 00 01 c0 0c 00 0f 00 01 00 00 02 58 00 11 00 1e 04 61 6c 74 32 05 61 73 70 6d 78 01 6c c0 0c c0 0c 00 0f 00 01 00 00 02 58 00 04 00 0a c0 2f c0 0c 00 0f 00 01 00 00 02 58 00 09 00 28 04 61 6c 74 33 c0 2f c0 0c 00 0f 00 01 00 00 02 58 00 09 00 14 04 61 6c 74 31 c0 2f c0 0c 00 0f 00 01 00 00 02 58 00 09 00 32 04 61 6c 74 34 c0 2f c0 0c 00 02 00 01 00 00 ad 8c 00 06 03 6e 73 31 c0 0c c0 0c 00 02 00 01 00 00 ad 8c 00 06 03 6e 73 34 c0 0c c0 0c 00 02 00 01 00 00 ad 8c 00 06 03 6e 73 33 c0 0c c0 0c 00 02 00 01 00 00 ad 8c 00 06 03 6e 73 32 c0 0c c0 2f 00 01 00 01 00 00 00 e0 00 04 6c b1 77 1b c0 6c 00 01 00 01 00 00 00 0d 00 04 ac d9 c2 1b 04 41 4c 54 32 c0 2f 00 01 00 01 00 00 00 0d 00 04 6c b1 61 1b c0 81 00 01 00 01 00 00 00 e0 00 04 6c b1 08 1a c0 94 00 01 00 01 00 03 8b b6 00 04 d8 ef 20 0a c0 ca 00 01 00 01 00 03 8f 54 00 04 d8 ef 22 0a c0 b8 00 01 00 01 00 03 8b b6 00 04 d8 ef 24 0a c0 a6 00 01 00 01 00 03 97 5d 00 04 d8 ef 26 0a c0 2f 00 1c 00 01 00 00 00 f9 00 10 2a 00 14 50 40 13 0c 01 00 00 00 00 00 00 00 1b c0 6c 00 1c 00 01 00 00 00 f9 00 10 24 04 68 00 40 03 0c 04 00 00 00 00 00 00 00 1b 04 41 4c 54 33 c0 2f 00 1c 00 01 00 00 01 1b 00 10 26 07 f8 b0 40 0e 0c 00 00 00 00 00 00 00 00 1b c0 94 00 1c 00 01 00 02 64 96 00 10 20 01 48 60 48 02 00 32 00 00 00 00 00 00 00 0a c0 ca 00 1c 00 01 00 04 54 3a 00 10 20 01 48 60 48 02 00 34 00 00 00 00 00 00 00 0a c0 b8 00 1c 00 01 00 01 79 ea 00 10 20 01 48 60 48 02 00 36 00 00 00 00 00 00 00 0a c0 a6 00 1c 00 01 00 01 d1 ba 00 10 20 01 48 60 48 02 00 38 00 00 00 00 00 00 00 0a" in let mock_state = create `Tcp in default_debug_info := [ipv4_buf]; match getaddrinfo mock_state Dns.Rr_map.Mx domain_name with | Ok (_ttl, mx_set) -> let make_mx_record (preference, domain_name) = Dns.Mx.{ preference; mail_exchange = Domain_name.host_exn (Domain_name.of_string_exn domain_name) } in (* assert this is Google MX *) Alcotest.(check bool __LOC__ true (Dns.Rr_map.Mx_set.equal mx_set (Dns.Rr_map.Mx_set.of_list (List.map make_mx_record [ (10, "aspmx.l.google.com"); (20, "alt1.aspmx.l.google.com"); (30, "alt2.aspmx.l.google.com"); (40, "alt3.aspmx.l.google.com"); (50, "alt4.aspmx.l.google.com") ])))) | Error _ -> failwith "foo.com should have been returned" let fails_on_partial_udp_packet () = let domain_name = Domain_name.(of_string_exn "google.com" |> host_exn) in (* A partial google.com MX record first two bytes identify this as a TCP packet - dropped here bytes 3,4 are set to the query ID 00 00 *) let udp_buf = Ohex.decode " 00 00 81 80 00 01 00 05 00 04 00 0f 06 67 6f 6f 67 6c 65 03 63 6f " in let mock_state = create `Udp in default_debug_info := [udp_buf]; match getaddrinfo mock_state Dns.Rr_map.Mx domain_name with | Error `Msg actual -> let expected = "Truncated UDP response" in Alcotest.(check string "reports the truncated UDP packet failure" expected actual) | Ok (_, _) -> failwith "Should have reported the Truncated UDP packet" let cname_and_nodata_packet () = (* we request a non-existing record type of existing domain name, which is an alias - the reply is a CNAME with NoData *) (* concretely, requesting AAAA raw.githubusercontent.com, reply is AN: raw.githubusercontent.com CNAME github.map.fastly.net AU: SOA fastly.net *) let domain_name = Domain_name.(of_string_exn "raw.githubusercontent.com" |> host_exn) in let udp_buf = Ohex.decode {| 00 00 81 80 00 01 00 01 00 01 00 00 03 72 61 77 11 67 69 74 68 75 62 75 73 65 72 63 6f 6e 74 65 6e 74 03 63 6f 6d 00 00 1c 00 01 c0 0c 00 05 00 01 00 00 00 16 00 17 06 67 69 74 68 75 62 03 6d 61 70 06 66 61 73 74 6c 79 03 6e 65 74 00 c0 42 00 06 00 01 00 00 00 14 00 2e 03 6e 73 31 c0 42 0a 68 6f 73 74 6d 61 73 74 65 72 06 66 61 73 74 6c 79 c0 22 78 39 c6 29 00 00 0e 10 00 00 02 58 00 09 3a 80 00 00 00 1e|} in let mock_state = create `Udp in default_debug_info := [udp_buf]; match getaddrinfo mock_state Dns.Rr_map.Aaaa domain_name with | Error `Msg actual -> let expected = "DNS cache error no data fastly.net" in let len = String.length expected in Alcotest.(check string __LOC__ expected (String.sub actual 0 len)) | Ok (_, _) -> Alcotest.fail "Should have returned nodata" let tests = [ "supports_mx_packets", `Quick, supports_mx_packets; "a partial UDP response packet fails", `Quick, fails_on_partial_udp_packet; "cname and nodata in packet", `Quick, cname_and_nodata_packet; ] end let tests = [ "make_query tests", Make_query_tests.tests; "parse_response tests", Parse_response_tests.tests; "gethostbyname tests", Gethostbyname_tests.tests; "getaddrinfo tests", Getaddrinfo_tests.tests; ] let () = Alcotest.run "DNS client tests" tests