173 lines
5.8 KiB
OCaml
173 lines
5.8 KiB
OCaml
|
|
(* {!Transport} provides the implementation of the underlying flow
|
||
|
|
that is in turn used by {!Dns_client.Make} to provide the
|
||
|
|
blocking Unix convenience module:
|
||
|
|
*)
|
||
|
|
|
||
|
|
module Transport : Dns_client.S
|
||
|
|
with type io_addr = Ipaddr.t * int
|
||
|
|
and type stack = unit
|
||
|
|
and type +'a io = 'a
|
||
|
|
= struct
|
||
|
|
type io_addr = Ipaddr.t * int
|
||
|
|
type stack = unit
|
||
|
|
type nameservers =
|
||
|
|
| Static of io_addr list
|
||
|
|
| Resolv_conf of {
|
||
|
|
mutable nameservers : io_addr list;
|
||
|
|
mutable digest : Digest.t option
|
||
|
|
}
|
||
|
|
type t = {
|
||
|
|
protocol : Dns.proto ;
|
||
|
|
nameservers : nameservers ;
|
||
|
|
timeout_ns : int64 ;
|
||
|
|
}
|
||
|
|
type context = {
|
||
|
|
t : t ;
|
||
|
|
fd : Unix.file_descr ;
|
||
|
|
mutable timeout_ns : int64
|
||
|
|
}
|
||
|
|
type +'a io = 'a
|
||
|
|
|
||
|
|
let read_file file =
|
||
|
|
try
|
||
|
|
let fh = open_in file in
|
||
|
|
try
|
||
|
|
let content = really_input_string fh (in_channel_length fh) in
|
||
|
|
close_in_noerr fh ;
|
||
|
|
Ok content
|
||
|
|
with _ ->
|
||
|
|
close_in_noerr fh;
|
||
|
|
Error (`Msg ("Error reading file: " ^ file))
|
||
|
|
with _ -> Error (`Msg ("Error opening file " ^ file))
|
||
|
|
|
||
|
|
let decode_resolv_conf data =
|
||
|
|
match Dns_resolvconf.parse data with
|
||
|
|
| Ok [] -> Error (`Msg "empty nameservers from resolv.conf")
|
||
|
|
| Ok ips -> Ok ips
|
||
|
|
| Error _ as e -> e
|
||
|
|
|
||
|
|
let default_resolvers () = [ Ipaddr.of_string_exn "1.1.1.1", 53 ]
|
||
|
|
|
||
|
|
let maybe_resolv_conf t =
|
||
|
|
match t.nameservers with
|
||
|
|
| Static _ -> ()
|
||
|
|
| Resolv_conf resolv_conf ->
|
||
|
|
let decode_update data dgst =
|
||
|
|
match decode_resolv_conf data with
|
||
|
|
| Ok ips ->
|
||
|
|
resolv_conf.digest <- Some dgst;
|
||
|
|
resolv_conf.nameservers <- List.map (function `Nameserver ip -> (ip, 53)) ips
|
||
|
|
| Error _ ->
|
||
|
|
resolv_conf.digest <- None;
|
||
|
|
resolv_conf.nameservers <- default_resolvers ()
|
||
|
|
in
|
||
|
|
match read_file "/etc/resolv.conf", resolv_conf.digest with
|
||
|
|
| Ok data, Some d ->
|
||
|
|
let digest = Digest.string data in
|
||
|
|
if Digest.equal digest d then () else decode_update data digest
|
||
|
|
| Ok data, None -> decode_update data (Digest.string data)
|
||
|
|
| Error _, None -> ()
|
||
|
|
| Error _, Some _ ->
|
||
|
|
resolv_conf.digest <- None;
|
||
|
|
resolv_conf.nameservers <- default_resolvers ()
|
||
|
|
|
||
|
|
let create ?nameservers ~timeout () =
|
||
|
|
let protocol, nameservers =
|
||
|
|
match nameservers with
|
||
|
|
| Some (proto, ns) -> (proto, Static ns)
|
||
|
|
| None ->
|
||
|
|
let ips, digest =
|
||
|
|
match
|
||
|
|
let ( let* ) = Result.bind in
|
||
|
|
let* data = read_file "/etc/resolv.conf" in
|
||
|
|
let* ips = decode_resolv_conf data in
|
||
|
|
Ok (ips, Digest.string data)
|
||
|
|
with
|
||
|
|
| Error _ -> default_resolvers (), None
|
||
|
|
| Ok (ips, digest) ->
|
||
|
|
List.map (function `Nameserver ip -> (ip, 53)) ips, Some digest
|
||
|
|
in
|
||
|
|
(`Tcp, Resolv_conf { nameservers = ips; digest })
|
||
|
|
in
|
||
|
|
{ protocol ; nameservers ; timeout_ns = timeout }
|
||
|
|
|
||
|
|
let nameservers { protocol ; nameservers = Static nameservers | Resolv_conf { nameservers; _ } ; _ } =
|
||
|
|
protocol, nameservers
|
||
|
|
let clock = Mtime_clock.elapsed_ns
|
||
|
|
let rng = Mirage_crypto_rng.generate ?g:None
|
||
|
|
|
||
|
|
let bind a b = b a
|
||
|
|
let lift v = v
|
||
|
|
|
||
|
|
let close { fd ; _ } = try Unix.close fd with _ -> ()
|
||
|
|
|
||
|
|
let with_timeout ctx f =
|
||
|
|
let start = clock () in
|
||
|
|
(* TODO cancel execution of f when time_left is 0 *)
|
||
|
|
let r = f ctx.fd in
|
||
|
|
let stop = clock () in
|
||
|
|
ctx.timeout_ns <- Int64.sub (ctx.timeout_ns) (Int64.sub stop start);
|
||
|
|
if ctx.timeout_ns <= 0L then
|
||
|
|
Error (`Msg "DNS resolution timed out.")
|
||
|
|
else
|
||
|
|
r
|
||
|
|
|
||
|
|
(* there is no connect timeouts, just a request timeout (unix: receive timeout) *)
|
||
|
|
let connect t =
|
||
|
|
maybe_resolv_conf t;
|
||
|
|
match nameservers t with
|
||
|
|
| _, [] -> Error (`Msg "empty nameserver list")
|
||
|
|
| proto, (server, port) :: _ ->
|
||
|
|
try
|
||
|
|
Result.bind
|
||
|
|
(match proto with
|
||
|
|
| `Udp -> Ok Unix.((getprotobyname "udp").p_proto, SOCK_DGRAM)
|
||
|
|
| `Tcp -> Ok Unix.((getprotobyname "tcp").p_proto, SOCK_STREAM))
|
||
|
|
(fun (proto_number, sock_typ) ->
|
||
|
|
let fam = match server with Ipaddr.V4 _ -> Unix.PF_INET | Ipaddr.V6 _ -> Unix.PF_INET6 in
|
||
|
|
let socket = Unix.socket fam sock_typ proto_number in
|
||
|
|
let addr = Unix.ADDR_INET (Ipaddr_unix.to_inet_addr server, port) in
|
||
|
|
let ctx = { t ; fd = socket ; timeout_ns = t.timeout_ns } in
|
||
|
|
try
|
||
|
|
with_timeout ctx (fun fd ->
|
||
|
|
Unix.connect fd addr;
|
||
|
|
Ok (proto, ctx))
|
||
|
|
with e ->
|
||
|
|
close ctx;
|
||
|
|
Error (`Msg (Printexc.to_string e)))
|
||
|
|
with e ->
|
||
|
|
Error (`Msg (Printexc.to_string e))
|
||
|
|
|
||
|
|
let send_recv ctx (str : string) =
|
||
|
|
try
|
||
|
|
begin match
|
||
|
|
with_timeout ctx (fun fd ->
|
||
|
|
Unix.setsockopt_float fd Unix.SO_SNDTIMEO (Duration.to_f ctx.timeout_ns);
|
||
|
|
let res = Unix.send_substring fd str 0 (String.length str) [] in
|
||
|
|
if res <> String.length str then
|
||
|
|
Error (`Msg ("Broken write to upstream NS" ^ (string_of_int res)))
|
||
|
|
else
|
||
|
|
Ok ())
|
||
|
|
with
|
||
|
|
| Error _ as e -> e
|
||
|
|
| Ok () ->
|
||
|
|
let buffer = Bytes.make 2048 '\000' in
|
||
|
|
with_timeout ctx (fun fd ->
|
||
|
|
Unix.setsockopt_float fd Unix.SO_RCVTIMEO (Duration.to_f ctx.timeout_ns);
|
||
|
|
let x = Unix.recv fd buffer 0 (Bytes.length buffer) [] in
|
||
|
|
if x > 0 && x <= Bytes.length buffer then
|
||
|
|
Ok (String.sub (Bytes.unsafe_to_string buffer) 0 x)
|
||
|
|
else
|
||
|
|
Error (`Msg "Reading from NS socket failed"))
|
||
|
|
end
|
||
|
|
with e ->
|
||
|
|
Error (`Msg (Printexc.to_string e))
|
||
|
|
end
|
||
|
|
|
||
|
|
(* Now that we have our {!Transport} implementation we can include the logic
|
||
|
|
that goes on top of it: *)
|
||
|
|
include Dns_client.Make(Transport)
|
||
|
|
|
||
|
|
(* initialize the RNG *)
|
||
|
|
let () = Mirage_crypto_rng_unix.use_default ()
|