mte/unikernel/duniverse/ocaml-dns/unix/client/dns_client_unix.ml

173 lines
5.8 KiB
OCaml
Raw Normal View History

2025-11-11 02:07:51 +01:00
(* {!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 ()