328 lines
14 KiB
OCaml
328 lines
14 KiB
OCaml
|
|
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
|