mte/unikernel/duniverse/ocaml-dns/mirage/resolver/dns_resolver_mirage.ml

438 lines
17 KiB
OCaml
Raw Normal View History

2025-11-11 02:07:51 +01:00
(* (c) 2018 Hannes Mehnert, all rights reserved *)
open Lwt.Infix
let src = Logs.Src.create "dns_resolver_mirage" ~doc:"effectful DNS resolver"
module Log = (val Logs.src_log src : Logs.LOG)
module Make (S : Tcpip.Stack.V4V6) = struct
module Dns = Dns_mirage.Make(S)
module T = S.TCP
module TLS = Tls_mirage.Make(T)
type t = {
push : (Ipaddr.t * int * string * (int32 * string) Lwt.u) option -> unit ;
primary_data : unit -> Dns_trie.t ;
with_primary_data : Dns_trie.t -> unit ;
update_tls : Tls.Config.server -> unit ;
}
type tls_flow = { tls_flow : TLS.flow ; mutable linger : Cstruct.t }
module FM = Map.Make(struct
type t = Ipaddr.t * int
let compare (ip, p) (ip', p') =
match Ipaddr.compare ip ip' with
| 0 -> compare p p'
| x -> x
end)
let resolver stack ?(root = false) ?(timer = 500) ?(udp = true) ?(tcp = true) ?tls ?(port = 53) ?(tls_port = 853) t =
let server_port = 53 in
let server_tls_port = 853 in
let state = ref t in
(* according to RFC5452 4.5, we can chose source port between 1024-49152 *)
let sport () = 1024 + Randomconv.int ~bound:48128 Mirage_crypto_rng.generate in
(* TODO limit these maps so we don't use too much memory *)
let tcp_in = ref FM.empty in
let ocaml_in = ref FM.empty in
let auth = ref Ipaddr.Map.empty in
let tls_auth = ref Ipaddr.Map.empty in
let stream, push = Lwt_stream.create () in
let opportunistic = List.mem `Opportunistic_tls_authoritative (Dns_resolver.features t) in
let send_tls flow data =
let len = Cstruct.create 2 in
Cstruct.BE.set_uint16 len 0 (Cstruct.length data);
TLS.writev flow [len; data] >>= function
| Ok () -> Lwt.return (Ok ())
| Error e ->
Log.err (fun m -> m "tls error %a while writing" TLS.pp_write_error e);
TLS.close flow >|= fun () ->
Error ()
in
let rec read_tls ({ tls_flow ; linger } as f) length =
if Cstruct.length linger >= length then
let a, b = Cstruct.split linger length in
f.linger <- b;
Lwt.return (Ok a)
else
TLS.read tls_flow >>= function
| Ok `Eof -> Log.debug (fun m -> m "end of file while reading"); TLS.close tls_flow >|= fun () -> Error ()
| Error e -> Log.warn (fun m -> m "error reading TLS: %a" TLS.pp_error e); TLS.close tls_flow >|= fun () -> Error ()
| Ok (`Data d) ->
f.linger <- Cstruct.append linger d;
read_tls f length
in
let read_tls_packet f =
read_tls f 2 >>= function
| Error () -> Lwt.return (Error ())
| Ok k ->
let len = Cstruct.BE.get_uint16 k 0 in
read_tls f len
in
let retry_tls = Duration.of_day 1 in (* from RFC 9539, 4.3 "damping" *)
let tls_timeout = Duration.of_sec 2 in (* RFC 9539, 4.3 "timeout" (4s), we use 2s *)
let rec client_tls_out cfg dst port =
tls_auth := Ipaddr.Map.add dst (`Tls_tried (Mirage_mtime.elapsed_ns ())) !tls_auth;
T.create_connection (S.tcp stack) (dst, port) >>= function
| Error e ->
(* do i need to report this back into the resolver? what are their options then? *)
Log.err (fun m -> m "error %a while establishing tcp connection to %a:%d"
T.pp_error e Ipaddr.pp dst port) ;
Lwt.return (Error ())
| Ok flow ->
Log.debug (fun m -> m "established new outgoing TCP connection to %a:%d"
Ipaddr.pp dst port);
TLS.client_of_flow cfg flow >|= function
| Error e ->
Log.warn (fun m -> m "TLS error (to %a:%d): %a" Ipaddr.pp dst port
TLS.pp_write_error e);
Error ()
| Ok tls ->
let cfg =
match TLS.epoch tls with
| Error () -> cfg
| Ok ed ->
let anchors = Result.get_ok Ca_certs_nss.trust_anchors in
let authenticator =
let time () = Some (Mirage_ptime.now ()) in
match X509.Validation.verify_chain_of_trust ~host:None ~ip:dst
~time ~anchors ed.Tls.Core.peer_certificate_chain
with
| Ok _ ->
Log.info (fun m -> m "NS %a using ca-certs-nss authenticator"
Ipaddr.pp dst);
Result.get_ok (Ca_certs_nss.authenticator ())
| Error _ ->
match ed.peer_certificate with
| None ->
Log.info (fun m -> m "NS %a no certificate provided"
Ipaddr.pp dst);
fun ?ip:_ ~host:_ _certs -> Ok None
| Some cert ->
let fingerprint =
X509.(Public_key.fingerprint (Certificate.public_key cert))
in
Log.info (fun m -> m "NS %a using key-fingerprint %a authenticator"
Ohex.pp fingerprint Ipaddr.pp dst);
X509.Authenticator.key_fingerprint ~time ~hash:`SHA256 ~fingerprint
in
Result.get_ok (Tls.Config.client ~authenticator ())
in
tls_auth := Ipaddr.Map.add dst (`Tls_succeeded cfg) !tls_auth;
Log.debug (fun m -> m "tls connection to %a:%d" Ipaddr.pp dst port);
auth := Ipaddr.Map.add dst (`Tls tls) !auth ;
Lwt.async (fun () ->
let tls_and_linger = { tls_flow = tls ; linger = Cstruct.empty } in
let rec loop () =
read_tls_packet tls_and_linger >>= function
| Error () ->
Log.debug (fun m -> m "removing %a from auth" Ipaddr.pp dst) ;
auth := Ipaddr.Map.remove dst !auth ;
Lwt.return_unit
| Ok data ->
let now = Mirage_ptime.now () in
let ts = Mirage_mtime.elapsed_ns () in
let new_state, answers, queries =
let data = Cstruct.to_string data in
Dns_resolver.handle_buf !state now ts false `Tcp dst port data
in
state := new_state ;
Lwt_list.iter_p handle_answer answers >>= fun () ->
Lwt_list.iter_p handle_query queries >>= fun () ->
loop ()
in
loop ()) ;
Ok ()
and client_tcp_out dst port =
T.create_connection (S.tcp stack) (dst, port) >|= function
| Error e ->
(* do i need to report this back into the resolver? what are their options then? *)
Log.err (fun m -> m "error %a while establishing tcp connection to %a:%d"
T.pp_error e Ipaddr.pp dst port) ;
Error ()
| Ok flow ->
Log.debug (fun m -> m "established new outgoing TCP connection to %a:%d"
Ipaddr.pp dst port);
auth := Ipaddr.Map.add dst (`Tcp flow) !auth ;
Lwt.async (fun () ->
let f = Dns.of_flow flow in
let rec loop () =
Dns.read_tcp f >>= function
| Error () ->
Log.debug (fun m -> m "removing %a from auth" Ipaddr.pp dst) ;
auth := Ipaddr.Map.remove dst !auth ;
Lwt.return_unit
| Ok data ->
let now = Mirage_ptime.now () in
let ts = Mirage_mtime.elapsed_ns () in
let new_state, answers, queries =
let data = Cstruct.to_string data in
Dns_resolver.handle_buf !state now ts false `Tcp dst port data
in
state := new_state ;
Lwt_list.iter_p handle_answer answers >>= fun () ->
Lwt_list.iter_p handle_query queries >>= fun () ->
loop ()
in
loop ()) ;
Ok ()
and client_tcp dst port ~tls_port data =
match Ipaddr.Map.find_opt dst !auth with
| None ->
begin
let try_it = match Ipaddr.Map.find_opt dst !tls_auth with
| None -> Some None
| Some `Tls_succeeded cfg -> Some (Some cfg)
| Some `Tls_tried ts ->
if Int64.(ts >= sub (Mirage_mtime.elapsed_ns ()) retry_tls) then
Some None
else
None
in
(match try_it with
| Some cfg when opportunistic ->
let cfg =
match cfg with
| None ->
let authenticator ?ip:_ ~host:_ _certs = Ok None in
Result.get_ok (Tls.Config.client ~authenticator ())
| Some cfg -> cfg
in
client_tls_out cfg dst tls_port
| _ ->
client_tcp_out dst port) >>= function
| Error () ->
let sport = sport () in
S.UDP.listen (S.udp stack) ~port:sport (udp_cb sport false) ;
Dns.send_udp stack sport dst port (Cstruct.of_string data)
| Ok () -> client_tcp dst port ~tls_port data
end
| Some `Tcp x ->
begin
Dns.send_tcp x (Cstruct.of_string data) >>= function
| Ok () -> Lwt.return_unit
| Error () ->
auth := Ipaddr.Map.remove dst !auth ;
client_tcp dst port ~tls_port data
end
| Some `Tls tls ->
begin
send_tls tls (Cstruct.of_string data) >>= function
| Ok () -> Lwt.return_unit
| Error () ->
auth := Ipaddr.Map.remove dst !auth ;
client_tcp dst port ~tls_port data
end
and maybe_tcp dst port data =
(match Ipaddr.Map.find_opt dst !auth with
| Some `Tcp flow -> Dns.send_tcp flow (Cstruct.of_string data)
| Some `Tls tls -> send_tls tls (Cstruct.of_string data)
| None -> Lwt.return (Error ())) >>= function
| Ok () -> Lwt.return_unit
| Error () ->
let try_tls =
match Ipaddr.Map.find_opt dst !tls_auth with
| None -> true
| Some `Tls_succeeded _ -> true
| Some `Tls_tried ts ->
Int64.(ts >= sub (Mirage_mtime.elapsed_ns ()) retry_tls)
in
(if try_tls then
Lwt.pick [
(Mirage_sleep.ns tls_timeout >|= fun () -> `Timeout);
(client_tcp dst port ~tls_port:server_tls_port data >|= fun () -> `Used_tls)
]
else
Lwt.return `Timeout) >>= function
| `Timeout ->
let sport = sport () in
S.UDP.listen (S.udp stack) ~port:sport (udp_cb sport false) ;
Dns.send_udp stack sport dst port (Cstruct.of_string data)
| `Used_tls -> Lwt.return_unit
and handle_query (proto, dst, data) = match proto with
| `Udp -> maybe_tcp dst server_port data
| `Tcp -> client_tcp dst server_port ~tls_port:server_tls_port data
and handle_answer (proto, dst, dst_port, ttl, data) = match proto with
| `Udp -> Dns.send_udp stack port dst dst_port (Cstruct.of_string data)
| `Tcp ->
let from_tcp = FM.find_opt (dst, dst_port) !tcp_in in
let from_ocaml = FM.find_opt (dst, dst_port) !ocaml_in in
match from_tcp, from_ocaml with
| None, None ->
Log.err (fun m -> m "wanted to answer %a:%d via TCP, but couldn't find a flow"
Ipaddr.pp dst dst_port) ;
Lwt.return_unit
| Some (`Tcp flow), None ->
(Dns.send_tcp flow (Cstruct.of_string data) >|= function
| Ok () -> ()
| Error () -> tcp_in := FM.remove (dst, dst_port) !tcp_in)
| Some (`Tls flow), None ->
(send_tls flow (Cstruct.of_string data) >|= function
| Ok () -> ()
| Error () -> tcp_in := FM.remove (dst, dst_port) !tcp_in)
| None, Some wk -> begin
ocaml_in := FM.remove (dst, dst_port) !ocaml_in;
Lwt.wakeup wk (ttl, data);
Lwt.return_unit end
| Some _, Some _ -> assert false
and udp_cb lport req ~src ~dst:_ ~src_port buf =
let buf = Cstruct.to_string buf in
let now = Mirage_ptime.now ()
and ts = Mirage_mtime.elapsed_ns ()
in
let new_state, answers, queries =
Dns_resolver.handle_buf !state now ts req `Udp src src_port buf
in
if not req then
S.UDP.unlisten (S.udp stack) ~port:lport;
state := new_state ;
Lwt_list.iter_p handle_answer answers >>= fun () ->
Lwt_list.iter_p handle_query queries
in
if udp then begin
S.UDP.listen (S.udp stack) ~port (udp_cb port true);
Log.info (fun f -> f "DNS resolver listening on UDP port %d" port);
end;
let rec ocaml_cb () =
Lwt_stream.get stream >>= function
| Some (dst_ip, dst_port, data, wk) ->
ocaml_in := FM.add (dst_ip, dst_port) wk !ocaml_in;
let now = Mirage_ptime.now () in
let ts = Mirage_mtime.elapsed_ns () in
let new_state, answers, queries =
Dns_resolver.handle_buf !state now ts true `Tcp dst_ip dst_port data in
state := new_state ;
Lwt_list.iter_p handle_answer answers >>= fun () ->
Lwt_list.iter_p handle_query queries >>= fun () ->
ocaml_cb ()
| None -> Lwt.return_unit in
Lwt.async ocaml_cb;
let tcp_cb query flow =
let dst_ip, dst_port = T.dst flow in
Log.debug (fun m -> m "tcp connection from %a:%d" Ipaddr.pp dst_ip dst_port) ;
tcp_in := FM.add (dst_ip, dst_port) (`Tcp flow) !tcp_in ;
let f = Dns.of_flow flow in
let rec loop () =
Dns.read_tcp f >>= function
| Error () ->
tcp_in := FM.remove (dst_ip, dst_port) !tcp_in ;
Lwt.return_unit
| Ok data ->
let data = Cstruct.to_string data in
let now = Mirage_ptime.now () in
let ts = Mirage_mtime.elapsed_ns () in
let new_state, answers, queries =
Dns_resolver.handle_buf !state now ts query `Tcp dst_ip dst_port data
in
state := new_state ;
Lwt_list.iter_p handle_answer answers >>= fun () ->
Lwt_list.iter_p handle_query queries >>= fun () ->
loop ()
in
loop ()
in
if tcp then begin
S.TCP.listen (S.tcp stack) ~port (tcp_cb true);
Log.info (fun m -> m "DNS resolver listening on TCP port %d" port);
end;
let tls_cb cfg flow =
let dst_ip, dst_port = T.dst flow in
TLS.server_of_flow cfg flow >>= function
| Error e ->
Log.warn (fun m -> m "TLS error (from %a:%d): %a" Ipaddr.pp dst_ip dst_port
TLS.pp_write_error e);
Lwt.return_unit
| Ok tls ->
Log.debug (fun m -> m "tls connection from %a:%d" Ipaddr.pp dst_ip dst_port);
tcp_in := FM.add (dst_ip, dst_port) (`Tls tls) !tcp_in ;
let tls_and_linger = { tls_flow = tls ; linger = Cstruct.empty } in
let rec loop () =
read_tls_packet tls_and_linger >>= function
| Error () ->
tcp_in := FM.remove (dst_ip, dst_port) !tcp_in ;
Lwt.return_unit
| Ok data ->
let data = Cstruct.to_string data in
let now = Mirage_ptime.now () in
let ts = Mirage_mtime.elapsed_ns () in
let new_state, answers, queries =
Dns_resolver.handle_buf !state now ts true `Tcp dst_ip dst_port data
in
state := new_state ;
Lwt_list.iter_p handle_answer answers >>= fun () ->
Lwt_list.iter_p handle_query queries >>= fun () ->
loop ()
in
loop ()
in
let update_tls tls_cfg =
S.TCP.listen (S.tcp stack) ~port:tls_port (tls_cb tls_cfg);
in
(match tls with
| None -> ()
| Some cfg ->
update_tls cfg;
Log.info (fun m -> m "DNS resolver listening on TLS port %d" tls_port));
let rec time () =
let new_state, answers, queries =
Dns_resolver.timer !state (Mirage_mtime.elapsed_ns ())
in
state := new_state ;
Lwt_list.iter_p handle_answer answers >>= fun () ->
Lwt_list.iter_p handle_query queries >>= fun () ->
Mirage_sleep.ns (Duration.of_ms timer) >>= fun () ->
time ()
in
Lwt.async time ;
let primary_data () =
Dns_resolver.primary_data !state
in
let with_primary_data data =
let (t, outs) =
Dns_resolver.with_primary_data !state
(Mirage_ptime.now ())
(Mirage_mtime.elapsed_ns ())
data
in
state := t;
if outs <> [] then
Log.warn (fun m -> m "Updating resolver's primary name server resulted
in 'notify's. Secondaries in the resolver's primary DNS is *not*
supported. The 'notify's are discarded.")
in
if root then begin
let rec root () =
let new_state, q = Dns_resolver.query_root !state (Mirage_mtime.elapsed_ns ()) `Tcp in
state := new_state ;
handle_query q >>= fun () ->
Mirage_sleep.ns (Duration.of_day 6) >>= fun () ->
root ()
in
Lwt.async root end ;
{ push; primary_data; with_primary_data; update_tls }
let resolve_external { push; _ } (dst_ip, dst_port) data =
let th, wk = Lwt.wait () in
push (Some (dst_ip, dst_port, data, wk));
th
let primary_data { primary_data; _ } = primary_data ()
let update_primary_data { with_primary_data; _ } data = with_primary_data data
let update_tls { update_tls; _ } tls_config = update_tls tls_config
end