This commit is contained in:
swrup 2025-11-11 02:07:51 +01:00
parent aa2ff7b2f0
commit 2f3113f55d
11742 changed files with 1223940 additions and 0 deletions

View file

@ -0,0 +1,111 @@
(* (c) 2018 Hannes Mehnert, all rights reserved *)
open Lwt.Infix
let src = Logs.Src.create "dns_certify_mirage" ~doc:"effectful DNS certify"
module Log = (val Logs.src_log src : Logs.LOG)
module Make (S : Tcpip.Stack.V4V6) = struct
module D = Dns_mirage.Make(S)
let nsupdate_csr flow host keyname zone dnskey csr =
match
Dns_certify.nsupdate Mirage_crypto_rng.generate Mirage_ptime.now
~host ~keyname ~zone dnskey csr
with
| Error s -> Lwt.return (Error s)
| Ok (out, cb) ->
D.send_tcp (D.flow flow) (Cstruct.of_string out) >>= function
| Error () -> Lwt.return (Error (`Msg "tcp sending error"))
| Ok () -> D.read_tcp flow >|= function
| Error () -> Error (`Msg "tcp receive err")
| Ok data -> match cb (Cstruct.to_string data) with
| Error e -> Error (`Msg (Fmt.str "nsupdate reply error %a" Dns_certify.pp_u_err e))
| Ok () -> Ok ()
let query_certificate flow name csr =
match Dns_certify.query Mirage_crypto_rng.generate (Mirage_ptime.now ()) name csr with
| Error e -> Lwt.return (Error e)
| Ok (out, cb) ->
D.send_tcp (D.flow flow) (Cstruct.of_string out) >>= function
| Error () -> Lwt.return (Error (`Msg "couldn't send tcp"))
| Ok () ->
D.read_tcp flow >|= function
| Error () -> Error (`Msg "error while reading answer")
| Ok data -> match cb (Cstruct.to_string data) with
| Error e -> Error e
| Ok cert -> Ok cert
let query_certificate_or_csr flow hostname keyname zone dnskey csr =
query_certificate flow hostname csr >>= function
| Ok certificate ->
Log.info (fun m -> m "found certificate in DNS") ;
Lwt.return (Ok certificate)
| Error (`Msg msg) ->
Log.err (fun m -> m "error %s" msg) ;
Lwt.return (Error (`Msg msg))
| Error ((`Decode _ | `Bad_reply _ | `Unexpected_reply _) as e) ->
Log.err (fun m -> m "query error %a, giving up" Dns_certify.pp_q_err e);
Lwt.return (Error (`Msg "query error"))
| Error `No_tlsa ->
Log.info (fun m -> m "no certificate in DNS, need to transmit the CSR") ;
nsupdate_csr flow hostname keyname zone dnskey csr >>= function
| Error (`Msg msg) ->
Log.err (fun m -> m "failed to nsupdate TLSA %s" msg) ;
Lwt.fail_with "nsupdate issue"
| Ok () ->
let rec wait_for_cert ?(retry = 10) () =
if retry = 0 then
Lwt.return (Error (`Msg "too many retries, giving up"))
else
query_certificate flow hostname csr >>= function
| Ok certificate ->
Log.info (fun m -> m "finally found a certificate") ;
Lwt.return (Ok certificate)
| Error (`Msg msg) ->
Log.err (fun m -> m "error while querying certificate %s" msg) ;
Lwt.return (Error (`Msg msg))
| Error (#Dns_certify.q_err as q) ->
Log.info (fun m -> m "still waiting for certificate, got error %a" Dns_certify.pp_q_err q) ;
Mirage_sleep.ns (Duration.of_sec 2) >>= fun () ->
wait_for_cert ~retry:(pred retry) ()
in
wait_for_cert ()
let retrieve_certificate stack (dns_key_name, dns_key) ~hostname ?(additional_hostnames = []) ?(key_type = `RSA) ?key_data ?key_seed ?bits dns port =
let zone = Domain_name.(host_exn (drop_label_exn ~amount:2 dns_key_name)) in
let not_sub subdomain = not (Domain_name.is_subdomain ~subdomain ~domain:zone) in
if not_sub hostname then
invalid_arg "hostname not a subdomain of zone provided by dns_key"
else
let key =
let seed_or_data, data = match key_data, key_seed with
| None, None -> invalid_arg "neither key_data nor key_seed is supplied"
| Some data, _ -> Some `Data, data
| None, Some seed -> Some `Seed, seed
in
Result.fold
~ok:Fun.id
~error:(function `Msg msg -> invalid_arg ("key generation failed: " ^ msg))
(X509.Private_key.of_string ?seed_or_data ?bits key_type data)
in
match
let more_hostnames = additional_hostnames in
Dns_certify.signing_request hostname ~more_hostnames key
with
| Error (`Msg m) -> invalid_arg ("create signing request failed: " ^ m)
| Ok csr ->
S.TCP.create_connection (S.tcp stack) (dns, port) >>= function
| Error e ->
Log.err (fun m -> m "error %a while connecting to name server"
S.TCP.pp_error e);
Lwt.return (Error (`Msg "couldn't connect to name server"))
| Ok flow ->
let flow = D.of_flow flow in
query_certificate_or_csr flow hostname dns_key_name zone dns_key csr >>= fun certificate ->
S.TCP.close (D.flow flow) >|= fun () ->
match certificate with
| Error e -> Error e
| Ok (cert, chain) -> Ok (cert :: chain, key)
end

View file

@ -0,0 +1,21 @@
(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)
module Make (S : Tcpip.Stack.V4V6) : sig
val retrieve_certificate :
S.t -> ([`raw ] Domain_name.t * Dns.Dnskey.t) ->
hostname:[ `host ] Domain_name.t ->
?additional_hostnames:[ `raw ] Domain_name.t list ->
?key_type:X509.Key_type.t -> ?key_data:string -> ?key_seed:string ->
?bits:int -> S.TCP.ipaddr -> int ->
(X509.Certificate.t list * X509.Private_key.t, [ `Msg of string ]) result Lwt.t
(** [retrieve_certificate stack dns_key ~hostname ~key_type ~key_data ~key_seed ~bits server_ip port]
generates a private key (using [key_type], [key_data], [key_seed], and
[bits]), a certificate signing request for the given [hostname] and
[additional_hostnames], and sends [server_ip] an nsupdate (DNS-TSIG with
[dns_key]) with the csr as TLSA record, awaiting for a matching
certificate as TLSA record. Requires a service that interacts with let's
encrypt to transform the CSR into a signed certificate. If something
fails, an exception (via [Lwt.fail]) is raised. This is meant for
unikernels that require a valid TLS certificate before they can start
their service (i.e. most web servers, mail servers). *)
end

View file

@ -0,0 +1,5 @@
(library
(name dns_certify_mirage)
(public_name dns-certify.mirage)
(wrapped false)
(libraries dns dns-mirage dns-certify mirage-crypto-rng mirage-crypto-pk lwt duration mirage-sleep mirage-ptime tcpip))

View file

@ -0,0 +1,482 @@
open Lwt.Infix
let src = Logs.Src.create "dns_client_mirage" ~doc:"effectful DNS client layer"
module Log = (val Logs.src_log src : Logs.LOG)
module IM = Map.Make(Int)
module type S = sig
type happy_eyeballs
module Transport :
sig
include Dns_client.S
with type +'a io = 'a Lwt.t
and type io_addr = [
| `Plaintext of Ipaddr.t * int
| `Tls of Tls.Config.client * Ipaddr.t * int
]
val happy_eyeballs : t -> happy_eyeballs
end
include module type of Dns_client.Make(Transport)
val nameserver_of_string : string ->
(Dns.proto * Transport.io_addr, [> `Msg of string ]) result
val connect :
?cache_size:int ->
?edns:[ `None | `Auto | `Manual of Dns.Edns.t ] ->
?nameservers:string list ->
?timeout:int64 -> Transport.stack ->
t Lwt.t
end
module Make
(S : Tcpip.Stack.V4V6)
(H : Happy_eyeballs_mirage.S with type stack = S.t
and type flow = S.TCP.flow) = struct
type happy_eyeballs = H.t
module TLS = Tls_mirage.Make(S.TCP)
let auth_err = match X509.Authenticator.of_string "" with
| Ok _ -> "should not happen"
| Error `Msg m -> m
let format = {|
The format of an IP address and optional port is:
- '[::1]:port' for an IPv6 address, or
- '127.0.0.1:port' for an IPv4 address.
The format of a nameserver is:
- 'udp:IP' where the first element is the string "udp" and the [IP] as described
above (port defaults to 53): UDP packets to the provided IP address will be
sent from a random source port;
- 'tcp:IP' where the first element is the string "tcp" and the [IP] as described
above (port defaults to 53): a TCP connection to the provided IP address will
be established;
- 'tls:IP' where the first element is the string "tls", the [IP] as described
above (port defaults to 853): a TCP connection will be established, on top of
which a TLS handshake with the authenticator
(https://github.com/mirage/ca-certs-nss) will be done (which checks for the
IP address being in the certificate as SubjectAlternativeName);
- 'tls:IP!hostname' where the first element is the string "tls",
the [IP] as described above (port defaults to 853), the [hostname] a host name
used for the TLS authentication: a TCP connection will be established, on top
of which a TLS handshake with the authenticator
(https://github.com/mirage/ca-certs-nss) will be done;
- 'tls:IP!hostname!authenticator' where the first element is the string "tls",
the [IP] as described above (port defaults to 853), the [hostname] a host name
used for the TLS authentication, and the [authenticator] an X509
authenticator: a TCP connection will be established, on top of which a TLS
handshake with the authenticator will be done.
|} ^ auth_err
let nameserver_of_string str =
let ( let* ) = Result.bind in
begin match String.split_on_char ':' str with
| "tls" :: rest ->
let str = String.concat ":" rest in
( match String.split_on_char '!' str with
| [ nameserver ] ->
let* ipaddr, port = Ipaddr.with_port_of_string ~default:853 nameserver in
let* authenticator = Ca_certs_nss.authenticator () in
let* tls = Tls.Config.client ~authenticator () in
Ok (`Tcp, `Tls (tls, ipaddr, port))
| nameserver :: opt_hostname :: authenticator ->
let* ipaddr, port = Ipaddr.with_port_of_string ~default:853 nameserver in
let peer_name, data =
match
let* dn = Domain_name.of_string opt_hostname in
Domain_name.host dn
with
| Ok hostname -> Some hostname, String.concat "!" authenticator
| Error _ -> None, String.concat "!" (opt_hostname :: authenticator)
in
let* authenticator =
if data = "" then
Ca_certs_nss.authenticator ()
else
let* a = X509.Authenticator.of_string data in
Ok (a (fun () -> Some (Mirage_ptime.now ())))
in
let* tls = Tls.Config.client ~authenticator ?peer_name () in
Ok (`Tcp, `Tls (tls, ipaddr, port))
| [] -> assert false )
| "tcp" :: nameserver ->
let str = String.concat ":" nameserver in
let* ipaddr, port = Ipaddr.with_port_of_string ~default:53 str in
Ok (`Tcp, `Plaintext (ipaddr, port))
| "udp" :: nameserver ->
let str = String.concat ":" nameserver in
let* ipaddr, port = Ipaddr.with_port_of_string ~default:53 str in
Ok (`Udp, `Plaintext (ipaddr, port))
| _ ->
Error (`Msg ("Unable to decode nameserver " ^ str))
end |> Result.map_error (function `Msg e -> `Msg (e ^ format))
module Transport :
sig
include Dns_client.S
with type stack = S.t * happy_eyeballs
and type +'a io = 'a Lwt.t
and type io_addr = [
| `Plaintext of Ipaddr.t * int
| `Tls of Tls.Config.client * Ipaddr.t * int
]
val happy_eyeballs : t -> happy_eyeballs
end = struct
type stack = S.t * happy_eyeballs
type io_addr = [
| `Plaintext of Ipaddr.t * int
| `Tls of Tls.Config.client * Ipaddr.t * int
]
type +'a io = 'a Lwt.t
module IS = Set.Make(Int)
type t = {
nameservers : io_addr list ;
proto : Dns.proto ;
timeout_ns : int64 ;
stack : S.t ;
mutable udp_ports : IS.t ;
mutable flow : [`Plain of S.TCP.flow | `Tls of TLS.flow ] option ;
mutable connected_condition : (unit, [ `Msg of string ]) result Lwt_condition.t option ;
mutable requests : (Cstruct.t * (Cstruct.t, [ `Msg of string ]) result Lwt_condition.t) IM.t ;
he : H.t ;
}
type context = t
let clock = Mirage_mtime.elapsed_ns
let happy_eyeballs { he ; _ } = he
let read_udp t ip ip_us ~src ~dst ~src_port:_ data =
if Ipaddr.compare ip_us dst = 0 && Ipaddr.compare ip src = 0 &&
Cstruct.length data > 12 (* minimum DNS length (header length) *)
then
(let id = Cstruct.BE.get_uint16 data 0 in
(match IM.find_opt id t.requests with
| None -> Log.warn (fun m -> m "received unsolicited data, ignoring")
| Some (_, cond) -> Lwt_condition.broadcast cond (Ok data)));
Lwt.return_unit
let generate_udp_port t =
let rec go retries =
if retries = 0 then
Error (`Msg "couldn't find a free UDP port")
else
let port = 1024 + ((String.get_uint16_be (Mirage_crypto_rng.generate 2) 0) mod (65536 - 1024)) in
if IS.mem port t.udp_ports then
go (retries - 1)
else
(t.udp_ports <- IS.add port t.udp_ports;
Ok port)
in
go 32
let create ?nameservers ~timeout (stack, he) =
let proto, nameservers = match nameservers with
| None ->
let authenticator = match Ca_certs_nss.authenticator () with
| Ok a -> a
| Error `Msg m -> invalid_arg ("bad CA certificates " ^ m)
in
let tls_cfg =
let peer_name = Dns_client.default_resolver_hostname in
match Tls.Config.client ~authenticator ~peer_name () with
| Ok a -> a
| Error `Msg m -> invalid_arg ("invalid TLS configuration: " ^ m)
in
let ns =
List.map (fun ip -> `Tls (tls_cfg, ip, 853))
Dns_client.default_resolvers
in
`Tcp, ns
| Some (a, ns) -> a, ns
in
{
nameservers ;
proto ;
timeout_ns = timeout ;
stack ;
udp_ports = IS.empty ;
flow = None ;
connected_condition = None ;
requests = IM.empty ;
he ;
}
let nameservers { proto ; nameservers ; _ } = proto, nameservers
let rng n = Mirage_crypto_rng.generate ?g:None n
let with_timeout time_left f =
let timeout =
Mirage_sleep.ns time_left >|= fun () ->
Error (`Msg "DNS request timeout")
in
Lwt.pick [ f ; timeout ]
let bind = Lwt.bind
let lift = Lwt.return
let rec read_loop ?(linger = Cstruct.empty) t flow =
let process cs =
let rec handle_data data =
let cs_len = Cstruct.length data in
if cs_len > 2 then
let len = Cstruct.BE.get_uint16 data 0 in
if cs_len - 2 >= len then
let packet, rest =
if cs_len - 2 = len
then data, Cstruct.empty
else Cstruct.split data (len + 2)
in
let id = Cstruct.BE.get_uint16 packet 2 in
(match IM.find_opt id t.requests with
| None -> Log.warn (fun m -> m "received unsolicited data, ignoring")
| Some (_, cond) -> Lwt_condition.broadcast cond (Ok packet));
handle_data rest
else
read_loop ~linger:data t flow
else
read_loop ~linger:data t flow
in
handle_data (if Cstruct.length linger = 0 then cs else Cstruct.append linger cs)
in
match flow with
| `Plain flow ->
begin
S.TCP.read flow >>= function
| Error e ->
t.flow <- None;
Log.err (fun m -> m "error %a reading from resolver" S.TCP.pp_error e);
Lwt.return_unit
| Ok `Eof ->
t.flow <- None;
if not (IM.is_empty t.requests) then
Log.info (fun m -> m "end of file reading from resolver");
Lwt.return_unit
| Ok (`Data cs) ->
process cs
end
| `Tls flow ->
begin
TLS.read flow >>= function
| Error e ->
t.flow <- None;
Log.err (fun m -> m "error %a reading from resolver" TLS.pp_error e);
Lwt.return_unit
| Ok `Eof ->
t.flow <- None;
if not (IM.is_empty t.requests) then
Log.info (fun m -> m "end of file reading from resolver");
Lwt.return_unit
| Ok (`Data cs) ->
process cs
end
let query_one flow data =
match flow with
| `Plain flow ->
begin
S.TCP.write flow data >>= function
| Error e ->
Lwt.return (Error (`Msg (Fmt.to_to_string S.TCP.pp_write_error e)))
| Ok () -> Lwt.return (Ok ())
end
| `Tls flow ->
begin
TLS.write flow data >>= function
| Error e ->
Lwt.return (Error (`Msg (Fmt.to_to_string TLS.pp_write_error e)))
| Ok () -> Lwt.return (Ok ())
end
let req_all flow t =
IM.fold (fun _id (data, _) r ->
r >>= function
| Error _ as e -> Lwt.return e
| Ok () -> query_one flow data)
t.requests (Lwt.return (Ok ()))
let to_pairs =
List.map (function `Plaintext (ip, port)
| `Tls (_, ip, port) -> (ip, port))
let find_ns ns (addr, port) =
List.find (function `Plaintext (ip, p) | `Tls (_, ip, p) ->
Ipaddr.compare ip addr = 0 && p = port)
ns
let rec connect_ns t nameservers =
let connected_condition = Lwt_condition.create () in
t.connected_condition <- Some connected_condition ;
let ns = to_pairs nameservers in
(* The connect_timeout given here is a bit too much, since it should
be (a) connect to the remote NS (b) send query, receive answer.
At the moment, how this is done, is that we use the connect_timeout
for (a) and another separate one for (b). Since we do connection
pooling, it is slightly tricky to use only a single connect_timeout. *)
H.connect_ip ~connect_timeout:t.timeout_ns t.he ns >>= function
| Error `Msg msg ->
let err = Error (`Msg (Fmt.str "error %s connecting to resolver %a"
msg
Fmt.(list ~sep:(any ", ") (pair ~sep:(any ":") Ipaddr.pp int))
(to_pairs t.nameservers)))
in
Lwt_condition.broadcast connected_condition err;
t.connected_condition <- None;
Log.err (fun m -> m "error connecting to resolver %s" msg);
Lwt.return err
| Ok (addr, flow) ->
let continue flow =
t.flow <- Some flow;
Lwt.async (fun () ->
read_loop t flow >>= fun () ->
if not (IM.is_empty t.requests) then
connect_ns t t.nameservers >|= function
| Error `Msg msg ->
Log.err (fun m -> m "error while connecting to resolver: %s" msg)
| Ok () -> ()
else
Lwt.return_unit);
Lwt_condition.broadcast connected_condition (Ok ());
t.connected_condition <- None;
req_all flow t
in
let config = find_ns t.nameservers addr in
match config with
| `Plaintext _ -> continue (`Plain flow)
| `Tls (tls_cfg, _ip, _port) ->
TLS.client_of_flow tls_cfg flow >>= function
| Ok tls -> continue (`Tls tls)
| Error e ->
Log.warn (fun m -> m "error establishing TLS connection to %a:%d: %a"
Ipaddr.pp (fst addr) (snd addr) TLS.pp_write_error e);
let ns' =
List.filter (function
| `Tls (_, ip, port) ->
not (Ipaddr.compare ip (fst addr) = 0 && port = snd addr)
| _ -> true)
nameservers
in
if ns' = [] then begin
let err = Error (`Msg "no further nameservers configured") in
Lwt_condition.broadcast connected_condition err;
t.connected_condition <- None;
Lwt.return err
end else
connect_ns t ns'
let connect t =
let to_tcp = function
| Ok () -> Ok (`Tcp, t)
| Error `Msg msg -> Error (`Msg msg)
in
match t.proto with
| `Udp -> Lwt.return (Ok (`Udp, t))
| `Tcp -> match t.flow, t.connected_condition with
| Some _, _ -> Lwt.return (Ok (`Tcp, t))
| None, Some w -> Lwt_condition.wait w >|= to_tcp
| None, None -> connect_ns t t.nameservers >|= to_tcp
let close _f =
(* ignoring this here *)
Lwt.return_unit
let send_recv t tx =
let ( >>>= ) = Lwt_result.bind in
if Cstruct.length tx > 4 then
match t.proto, t.flow with
| `Udp, _ ->
let dst, dst_port = match t.nameservers with
| `Plaintext (ip, port) :: _ -> ip, port
| _ -> assert false
in
let src = S.IP.src (S.ip t.stack) ~dst in
let id = Cstruct.BE.get_uint16 tx 0 in
Lwt.return (generate_udp_port t) >>>= fun udp_port ->
with_timeout t.timeout_ns
(S.UDP.listen (S.udp t.stack) ~port:udp_port (read_udp t dst src);
(S.UDP.write ~src_port:udp_port ~dst ~dst_port (S.udp t.stack) tx >|= function
| Error e -> Error (`Msg (Fmt.to_to_string S.UDP.pp_error e))
| Ok () -> Ok ()) >>>= fun () ->
let cond = Lwt_condition.create () in
t.requests <- IM.add id (tx, cond) t.requests;
let open Lwt.Infix in
Lwt_condition.wait cond >|= fun data ->
match data with Ok _ | Error `Msg _ as r -> r) >|= fun r ->
S.UDP.unlisten (S.udp t.stack) ~port:udp_port;
t.udp_ports <- IS.remove udp_port t.udp_ports;
t.requests <- IM.remove id t.requests;
r
| `Tcp, None -> Lwt.return (Error (`Msg "no connection to resolver"))
| `Tcp, Some flow ->
let id = Cstruct.BE.get_uint16 tx 2 in
with_timeout t.timeout_ns
(let open Lwt_result.Infix in
query_one flow tx >>= fun () ->
let cond = Lwt_condition.create () in
t.requests <- IM.add id (tx, cond) t.requests;
let open Lwt.Infix in
Lwt_condition.wait cond >|= fun data ->
match data with Ok _ | Error `Msg _ as r -> r) >|= fun r ->
t.requests <- IM.remove id t.requests;
r
else
Lwt.return (Error (`Msg "invalid context (data length <= 4)"))
let send_recv t tx =
Lwt_result.map Cstruct.to_string (send_recv t (Cstruct.of_string tx))
end
include Dns_client.Make(Transport)
let decode_nameservers ?(nameservers= []) () =
let nameservers =
List.map
(fun nameserver -> match nameserver_of_string nameserver with
| Ok nameserver -> nameserver
| Error (`Msg err) -> invalid_arg err)
nameservers
in
let tcp, udp =
List.fold_left (fun (tcp, udp) -> function
| `Tcp, a -> a :: tcp, udp
| `Udp, a -> tcp, a :: udp)
([], []) nameservers
in
match tcp, udp with
| [], [] -> None
| [], _::_ -> Some (`Udp, udp)
| _::_, [] -> Some (`Tcp, tcp)
| _::_, udps ->
let pp_io_addr ppf = function
|`Plaintext (ip, port) -> Fmt.pf ppf "%a:%u" Ipaddr.pp ip port
| `Tls (_, ip, port) -> Fmt.pf ppf "TLS: %a:%u" Ipaddr.pp ip port
in
Log.warn (fun m -> m "ignoring UDP nameservers %a, using TCP nameservers %a"
Fmt.(list ~sep:(any ", ") pp_io_addr) udps
Fmt.(list ~sep:(any ", ") pp_io_addr) tcp);
Some (`Tcp, tcp)
let connect ?cache_size ?edns ?nameservers ?timeout (stack, he) =
let nameservers = decode_nameservers ?nameservers () in
let t = create ?cache_size ?edns ?nameservers ?timeout (stack, he) in
let getaddrinfo record domain_name =
let open Lwt_result.Infix in
match record with
| `A ->
getaddrinfo t Dns.Rr_map.A domain_name >|= fun (_ttl, set) ->
Ipaddr.V4.Set.fold (fun ipv4 -> Ipaddr.Set.add (Ipaddr.V4 ipv4))
set Ipaddr.Set.empty
| `AAAA ->
getaddrinfo t Dns.Rr_map.Aaaa domain_name >|= fun (_ttl, set) ->
Ipaddr.V6.Set.fold (fun ipv6 -> Ipaddr.Set.add (Ipaddr.V6 ipv6))
set Ipaddr.Set.empty
in
H.inject (Transport.happy_eyeballs (transport t)) getaddrinfo;
Lwt.return t
end

View file

@ -0,0 +1,59 @@
module type S = sig
type happy_eyeballs
module Transport :
sig
include Dns_client.S
with type +'a io = 'a Lwt.t
and type io_addr = [
| `Plaintext of Ipaddr.t * int
| `Tls of Tls.Config.client * Ipaddr.t * int
]
val happy_eyeballs : t -> happy_eyeballs
end
include module type of Dns_client.Make(Transport)
val nameserver_of_string : string ->
(Dns.proto * Transport.io_addr, [> `Msg of string ]) result
(** [nameserver_of_string authenticators str] returns a {!Transport.io_addr}
from the given string. The format is:
- [udp:<ipaddr>(:port)?] for a plain nameserver and we will communicate
with it {i via} the UDP protocol
- [tcp:<ipaddr>(:port)?] for a plain nameserver and we will communicate
with it {i via} the TCP protocol
- [tls:<ipaddr>(:port)?((!hostname)?!authenticator)?] for a nameserver and
we will communicate with it {i via} the TCP protocol plus the TLS
encrypted layer. The user can verify the nameserver {i via} an
{i authenticator} (see {!X509.Authenticator.of_string} for the format
of it). The {i hostname} can be provided to be used as peer name by the
authenticator. By default, {!Ca_certs_nss.authenticator} is used.
*)
val connect :
?cache_size:int ->
?edns:[ `None | `Auto | `Manual of Dns.Edns.t ] ->
?nameservers:string list ->
?timeout:int64 ->
Transport.stack -> t Lwt.t
(** [connect ?cache_size ?edns ?nameservers ?timeout (stack, happy_eyeballs)]
creates a DNS entity which is able to resolve domain-name. It expects
few optional arguments:
- [cache_size] the size of the LRU cache,
- [edns] the behaviour of whether or not to send edns in queries,
- [nameservers] a list of {i nameservers} used to resolve domain-names,
- [timeout] (in nanoseconds), passed to {create}.
The provided [happy_eyeballs] will use [t] for resolving hostnames.
@raise [Invalid_argument] if given strings don't respect formats explained
by {!nameserver_of_string}.
*)
end
module Make
(S : Tcpip.Stack.V4V6)
(H : Happy_eyeballs_mirage.S with type stack = S.t
and type flow = S.TCP.flow)
: S with type Transport.stack = S.t * H.t
and type happy_eyeballs = H.t

View file

@ -0,0 +1,5 @@
(library
(name dns_client_mirage)
(public_name dns-client-mirage)
(libraries domain-name ipaddr mirage-crypto-rng mirage-sleep tcpip mirage-ptime mirage-mtime dns-client happy-eyeballs happy-eyeballs-mirage tls-mirage ca-certs-nss)
(wrapped false))

View file

@ -0,0 +1,85 @@
(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)
open Lwt.Infix
let src = Logs.Src.create "dns_mirage" ~doc:"effectful DNS layer"
module Log = (val Logs.src_log src : Logs.LOG)
module Make (S : Tcpip.Stack.V4V6) = struct
module IPM = struct
include 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 find k t = try Some (find k t) with Not_found -> None
end
module U = S.UDP
module T = S.TCP
type f = {
flow : T.flow ;
mutable linger : Cstruct.t ;
}
let of_flow flow = { flow ; linger = Cstruct.empty }
let flow { flow ; _ } = flow
let rec read_exactly f length =
let dst_ip, dst_port = T.dst f.flow in
if Cstruct.length f.linger >= length then
let a, b = Cstruct.split f.linger length in
f.linger <- b ;
Lwt.return (Ok a)
else
T.read f.flow >>= function
| Ok `Eof ->
Log.debug (fun m -> m "end of file on flow %a:%d" Ipaddr.pp dst_ip dst_port) ;
T.close f.flow >>= fun () ->
Lwt.return (Error ())
| Error e ->
Log.err (fun m -> m "error %a reading flow %a:%d" T.pp_error e Ipaddr.pp dst_ip dst_port) ;
T.close f.flow >>= fun () ->
Lwt.return (Error ())
| Ok (`Data b) ->
f.linger <- Cstruct.append f.linger b ;
read_exactly f length
let send_udp stack src_port dst dst_port data =
Log.debug (fun m -> m "udp: sending %d bytes from %d to %a:%d"
(Cstruct.length data) src_port Ipaddr.pp dst dst_port) ;
U.write ~src_port ~dst ~dst_port (S.udp stack) data >|= function
| Error e -> Log.warn (fun m -> m "udp: failure %a while sending from %d to %a:%d"
U.pp_error e src_port Ipaddr.pp dst dst_port)
| Ok () -> ()
let send_tcp flow answer =
let dst_ip, dst_port = T.dst flow in
Log.debug (fun m -> m "tcp: sending %d bytes to %a:%d" (Cstruct.length answer) Ipaddr.pp dst_ip dst_port) ;
let len = Cstruct.create 2 in
Cstruct.BE.set_uint16 len 0 (Cstruct.length answer) ;
T.write flow (Cstruct.append len answer) >>= function
| Ok () -> Lwt.return (Ok ())
| Error e ->
Log.err (fun m -> m "tcp: error %a while writing to %a:%d" T.pp_write_error e Ipaddr.pp dst_ip dst_port) ;
T.close flow >|= fun () ->
Error ()
let send_tcp_multiple flow datas =
Lwt_list.fold_left_s (fun acc d ->
match acc with
| Error () -> Lwt.return (Error ())
| Ok () -> send_tcp flow d)
(Ok ()) datas
let read_tcp flow =
read_exactly flow 2 >>= function
| Error () -> Lwt.return (Error ())
| Ok l ->
let len = Cstruct.BE.get_uint16 l 0 in
read_exactly flow len
end

View file

@ -0,0 +1,36 @@
(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)
module Make (S : Tcpip.Stack.V4V6) : sig
module IPM : sig
include Map.S with type key = Ipaddr.t * int
val find : Ipaddr.t * int -> 'a t -> 'a option
end
(** [IPM] is a map using [ip * port] as key. *)
type f
(** A 2byte-length per message flow abstraction, the embedding of DNS frames
via TCP. *)
val of_flow : S.TCP.flow -> f
(** [of_flow flow] is [f]. *)
val flow : f -> S.TCP.flow
(** [flow f] is the underlying flow. *)
val read_tcp : f -> (Cstruct.t, unit) result Lwt.t
(** [read_tcp f] returns either a buffer or an error (logs actual error). *)
val send_tcp : S.TCP.flow -> Cstruct.t -> (unit, unit) result Lwt.t
(** [send_tcp flow buf] sends the buffer, either succeeds or fails (logs
actual error). *)
val send_tcp_multiple : S.TCP.flow -> Cstruct.t list ->
(unit, unit) result Lwt.t
(** [send_tcp_multiple flow bufs] sends the buffers, either succeeds or fails
(logs actual error). *)
val send_udp : S.t -> int -> Ipaddr.t -> int -> Cstruct.t -> unit Lwt.t
(** [send_udp stack source_port dst dst_port buf] sends the [buf] as UDP
packet to [dst] on [dst_port]. *)
end

View file

@ -0,0 +1,5 @@
(library
(name dns_mirage)
(public_name dns-mirage)
(wrapped false)
(libraries dns tcpip ipaddr lwt))

View file

@ -0,0 +1,437 @@
(* (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

View file

@ -0,0 +1,16 @@
(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)
module Make (S : Tcpip.Stack.V4V6) : sig
type t
val resolver
: S.t -> ?root:bool -> ?timer:int -> ?udp:bool -> ?tcp:bool -> ?tls:Tls.Config.server -> ?port:int -> ?tls_port:int
-> Dns_resolver.t -> t
(** [resolver stack ~root ~timer ~udp ~tcp ~tls ~port ~tls_port resolver]
registers a caching resolver on the provided protocols [udp], [tcp], [tls]
using [port] for udp and tcp (defaults to 53), [tls_port] for tls (defaults
to 853) using the [resolver] configuration. The [timer] is in milliseconds
and defaults to 500 milliseconds.*)
include Dns_resolver_mirage_shared.S with type t := t
end

View file

@ -0,0 +1,10 @@
(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)
module type S = sig
type t
val resolve_external : t -> Ipaddr.t * int -> string -> (int32 * string) Lwt.t
val primary_data : t -> Dns_trie.t
val update_primary_data : t -> Dns_trie.t -> unit
val update_tls : t -> Tls.Config.server -> unit
end

View file

@ -0,0 +1,22 @@
(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)
module type S = sig
type t
val resolve_external : t -> Ipaddr.t * int -> string -> (int32 * string) Lwt.t
(** [resolve_external t (ip, port) data] resolves for [(ip, port)] the query
[data] and returns a pair of the minimum TTL and a response. *)
val primary_data : t -> Dns_trie.t
(** [primary_data t] is the DNS trie of the primary for the resolver [t]. *)
val update_primary_data : t -> Dns_trie.t -> unit
(** [update_primary_data t data] updates the primary for the resolver [t]
with the DNS trie [data]. Any 'notify's to secondaries are discarded -
secondary name servers are not supported in this setup. *)
val update_tls : t -> Tls.Config.server -> unit
(** [update_tls t tls_config] updates the tls configuration to [tls_config].
If the resolver wasn't already listening for TLS connections it will
start listening. *)
end

View file

@ -0,0 +1,13 @@
(library
(name dns_resolver_mirage)
(public_name dns-resolver.mirage)
(wrapped false)
(modules dns_resolver_mirage)
(libraries dns dns-resolver dns-server dns-mirage lwt duration mirage-sleep mirage-ptime mirage-mtime tcpip mirage-crypto-rng tls tls-mirage ca-certs-nss dns-resolver.mirage.shared))
(library
(name dns_resolver_mirage_shared)
(public_name dns-resolver.mirage.shared)
(wrapped false)
(modules dns_resolver_mirage_shared)
(libraries ipaddr dns-server tcpip tls))

View file

@ -0,0 +1,334 @@
(* (c) 2018 Hannes Mehnert, all rights reserved *)
open Lwt.Infix
let src = Logs.Src.create "dns_server_mirage" ~doc:"effectful DNS server"
module Log = (val Logs.src_log src : Logs.LOG)
module Make (S : Tcpip.Stack.V4V6) = struct
let inc =
let f = function
| `Udp_query -> "udp queries"
| `Udp_answer -> "udp answers"
| `Tcp_query -> "tcp queries"
| `Tcp_answer -> "tcp answers"
| `Tcp -> "tcp-server"
| `Tcp_client -> "tcp-client"
| `Tcp_keep -> "keep tcp flow"
| `Notify -> "request"
| `On_update -> "on update"
| `On_notify -> "on notify"
| `Tcp_cache_add -> "tcp cache add"
| `Tcp_cache_drop -> "tcp cache drop"
in
let src = Dns.counter_metrics ~f "dns-server-mirage" in
(fun x -> Metrics.add src (fun x -> x) (fun d -> d x))
module Dns = Dns_mirage.Make(S)
module T = S.TCP
let primary ?(on_update = fun ~old:_ ~authenticated_key:_ ~update_source:_ _ -> Lwt.return_unit) ?(on_notify = fun _ _ -> Lwt.return None) ?(timer = 2) ?(port = 53) stack t =
let state = ref t in
let tcp_out = ref Ipaddr.Map.empty in
let drop ip =
if Ipaddr.Map.mem ip !tcp_out then begin
inc `Tcp_cache_drop;
tcp_out := Ipaddr.Map.remove ip !tcp_out ;
state := Dns_server.Primary.closed !state ip
end
in
let connect recv_task ip =
inc `Tcp_client;
let dport = 53 in
Log.debug (fun m -> m "creating connection to %a:%d" Ipaddr.pp ip dport) ;
T.create_connection (S.tcp stack) (ip, dport) >>= function
| Error e ->
Log.err (fun m -> m "error %a while establishing tcp connection to %a:%d"
T.pp_error e Ipaddr.pp ip port) ;
Lwt.return (Error ())
| Ok flow ->
inc `Tcp_cache_add;
tcp_out := Ipaddr.Map.add ip flow !tcp_out ;
Lwt.async (recv_task ip dport flow);
Lwt.return (Ok flow)
in
let send_notify recv_task (ip, data) =
inc `Notify;
let data = List.map Cstruct.of_string data in
let connect_and_send ip =
connect recv_task ip >>= function
| Ok flow -> Dns.send_tcp_multiple flow data
| Error () -> Lwt.return (Error ())
in
(match Ipaddr.Map.find_opt ip !tcp_out with
| None -> connect_and_send ip
| Some f -> Dns.send_tcp_multiple f data >>= function
| Ok () -> Lwt.return (Ok ())
| Error () -> drop ip ; connect_and_send ip) >>= function
| Ok () -> Lwt.return_unit
| Error () ->
drop ip;
Lwt_list.iter_p (Dns.send_udp stack port ip 53) data
in
let maybe_update_state key ip t =
let old = !state in
let trie server = Dns_server.Primary.data server in
state := t;
if Dns_trie.equal (trie t) (trie old) then
Lwt.return_unit
else begin
inc `On_update ; on_update ~old:(trie old) ~authenticated_key:key ~update_source:ip t
end
and maybe_notify recv_task t now ts = function
| None -> Lwt.return_unit
| Some n -> inc `On_notify ; on_notify n t >>= function
| None -> Lwt.return_unit
| Some (trie, keys) ->
let state', outs = Dns_server.Primary.with_keys t now ts keys in
let state'', outs' = Dns_server.Primary.with_data state' now ts trie in
state := state'';
Lwt_list.iter_p (send_notify recv_task) (outs @ outs')
in
let rec recv_task ip port flow () =
let f = Dns.of_flow flow in
let rec loop () =
Dns.read_tcp f >>= function
| Error () -> drop ip ; Lwt.return_unit
| Ok data ->
inc `Tcp_query;
let now = Mirage_ptime.now () in
let ts = Mirage_mtime.elapsed_ns () in
let t, answers, notify, n, key =
Dns_server.Primary.handle_buf !state now ts `Tcp ip port (Cstruct.to_string data)
in
let n' = match n with
| Some `Keep -> inc `Tcp_cache_add ; inc `Tcp_keep ; tcp_out := Ipaddr.Map.add ip flow !tcp_out ; None
| Some `Notify soa -> Some (`Notify soa)
| Some `Signed_notify soa -> Some (`Signed_notify soa)
| None -> None
in
maybe_update_state key ip t >>= fun () ->
maybe_notify recv_task t now ts n' >>= fun () ->
if answers <> [] then inc `Tcp_answer;
let answers = List.map Cstruct.of_string answers in
(Dns.send_tcp_multiple flow answers >|= function
| Ok () -> ()
| Error () -> drop ip) >>= fun () ->
Lwt_list.iter_p (send_notify recv_task) notify >>= fun () ->
loop ()
in
loop ()
in
let tcp_cb flow =
inc `Tcp;
let dst_ip, dst_port = T.dst flow in
recv_task dst_ip dst_port flow ()
in
S.TCP.listen (S.tcp stack) ~port tcp_cb ;
Log.info (fun m -> m "DNS server listening on TCP port %d" port) ;
let udp_cb ~src ~dst:_ ~src_port buf =
inc `Udp_query;
let buf = Cstruct.to_string buf in
let now = Mirage_ptime.now () in
let ts = Mirage_mtime.elapsed_ns () in
let t, answers, notify, n, key =
Dns_server.Primary.handle_buf !state now ts `Udp src src_port buf
in
let n' = match n with
| None | Some `Keep -> None
| Some `Notify soa -> Some (`Notify soa)
| Some `Signed_notify soa -> Some (`Signed_notify soa)
in
maybe_update_state key src t >>= fun () ->
maybe_notify recv_task t now ts n' >>= fun () ->
if answers <> [] then inc `Udp_answer;
let answers = List.map Cstruct.of_string answers in
(Lwt_list.iter_s (Dns.send_udp stack port src src_port) answers) >>= fun () ->
Lwt_list.iter_p (send_notify recv_task) notify
in
S.UDP.listen (S.udp stack) ~port udp_cb ;
Log.info (fun m -> m "DNS server listening on UDP port %d" port) ;
let rec time () =
let now = Mirage_ptime.now () in
let ts = Mirage_mtime.elapsed_ns () in
let t, notifies = Dns_server.Primary.timer !state now ts in
maybe_update_state None Ipaddr.(V4 V4.localhost) t >>= fun () ->
Lwt_list.iter_p (send_notify recv_task) notifies >>= fun () ->
Mirage_sleep.ns (Duration.of_sec timer) >>= fun () ->
time ()
in
Lwt.async time
let secondary ?(on_update = fun ~old:_ _trie -> Lwt.return_unit) ?(timer = 5) ?(port = 53) stack t =
let state = ref t in
let tcp_out = ref Ipaddr.Map.empty in
let maybe_update_state t =
let old = !state in
let trie server = Dns_server.Secondary.data server in
state := t ;
if Dns_trie.equal (trie t) (trie old) then
Lwt.return_unit
else begin
inc `On_update ; on_update ~old:(trie old) t
end
in
let rec close ~timer ip =
(match Ipaddr.Map.find_opt ip !tcp_out with
| None -> Lwt.return_unit
| Some f -> T.close f) >>= fun () ->
tcp_out := Ipaddr.Map.remove ip !tcp_out ;
let now = Mirage_ptime.now () in
let elapsed = Mirage_mtime.elapsed_ns () in
let state', out = Dns_server.Secondary.closed !state now elapsed ip in
state := state' ;
if not timer then
request ~timer (ip, out)
else
Lwt.return_unit
and read_and_handle ~timer ip f =
Dns.read_tcp f >>= function
| Error () ->
Log.debug (fun m -> m "removing %a from tcp_out" Ipaddr.pp ip) ;
close ~timer ip
| Ok data ->
inc `Tcp_query;
let now = Mirage_ptime.now () in
let elapsed = Mirage_mtime.elapsed_ns () in
let t, answer, out =
Dns_server.Secondary.handle_buf !state now elapsed `Tcp ip (Cstruct.to_string data)
in
maybe_update_state t >>= fun () ->
(match answer with
| None -> Lwt.return (Ok ())
| Some x ->
inc `Tcp_answer;
let x = Cstruct.of_string x in
Dns.send_tcp (Dns.flow f) x >>= function
| Error () ->
Log.debug (fun m -> m "removing %a from tcp_out" Ipaddr.pp ip) ;
close ~timer ip >|= fun () -> Error ()
| Ok () -> Lwt.return (Ok ())) >>= fun r ->
(match out with
| None -> Lwt.return_unit
| Some (ip, data) -> request_one ~timer (ip, data)) >>= fun () ->
match r with
| Ok () -> read_and_handle ~timer ip f
| Error () -> Lwt.return_unit
and request ~timer (ip, data) =
inc `Notify;
let dport = 53 in
match Ipaddr.Map.find_opt ip !tcp_out with
| None ->
begin
Log.debug (fun m -> m "creating connection to %a:%d" Ipaddr.pp ip dport) ;
inc `Tcp_client;
T.create_connection (S.tcp stack) (ip, dport) >>= function
| Error e ->
Log.err (fun m -> m "error %a while establishing tcp connection to %a:%d"
T.pp_error e Ipaddr.pp ip dport) ;
close ~timer ip
| Ok flow ->
tcp_out := Ipaddr.Map.add ip flow !tcp_out ;
let data = List.map Cstruct.of_string data in
Dns.send_tcp_multiple flow data >>= function
| Error () -> close ~timer ip
| Ok () ->
Lwt.async (fun () -> read_and_handle ~timer ip (Dns.of_flow flow)) ;
Lwt.return_unit
end
| Some flow ->
let data = List.map Cstruct.of_string data in
Dns.send_tcp_multiple flow data >>= function
| Ok () -> Lwt.return_unit
| Error () ->
Log.warn (fun m -> m "closing tcp flow to %a:%d, retrying request"
Ipaddr.pp ip dport) ;
T.close flow >>= fun () ->
tcp_out := Ipaddr.Map.remove ip !tcp_out ;
let data = List.map Cstruct.to_string data in
request ~timer (ip, data)
and request_one ~timer (ip, d) = request ~timer (ip, [ d ])
in
let udp_cb ~src ~dst:_ ~src_port buf =
Log.debug (fun m -> m "udp frame from %a:%d" Ipaddr.pp src src_port) ;
inc `Udp_query;
let buf = Cstruct.to_string buf in
let now = Mirage_ptime.now () in
let elapsed = Mirage_mtime.elapsed_ns () in
let t, answer, out =
Dns_server.Secondary.handle_buf !state now elapsed `Udp src buf
in
maybe_update_state t >>= fun () ->
(match out with
| None -> ()
| Some (ip, cs) -> Lwt.async (fun () -> request_one ~timer:false (ip, cs))) ;
match answer with
| None -> Lwt.return_unit
| Some out ->
inc `Udp_answer;
let out = Cstruct.of_string out in
Dns.send_udp stack port src src_port out
in
S.UDP.listen (S.udp stack) ~port udp_cb ;
Log.info (fun m -> m "secondary DNS listening on UDP port %d" port) ;
let tcp_cb flow =
inc `Tcp;
let dst_ip, dst_port = T.dst flow in
tcp_out := Ipaddr.Map.add dst_ip flow !tcp_out ;
Log.debug (fun m -> m "tcp connection from %a:%d" Ipaddr.pp dst_ip dst_port) ;
let f = Dns.of_flow flow in
let rec loop () =
Dns.read_tcp f >>= function
| Error () -> tcp_out := Ipaddr.Map.remove dst_ip !tcp_out ; Lwt.return_unit
| Ok data ->
inc `Tcp_query;
let data = Cstruct.to_string data in
let now = Mirage_ptime.now () in
let elapsed = Mirage_mtime.elapsed_ns () in
let t, answer, out =
Dns_server.Secondary.handle_buf !state now elapsed `Tcp dst_ip data
in
maybe_update_state t >>= fun () ->
(match out with
| None -> ()
| Some (ip, cs) -> Lwt.async (fun () -> request_one ~timer:false (ip, cs)));
match answer with
| None ->
Log.warn (fun m -> m "no TCP output") ;
loop ()
| Some data ->
inc `Tcp_answer;
let data = Cstruct.of_string data in
Dns.send_tcp flow data >>= function
| Ok () -> loop ()
| Error () -> tcp_out := Ipaddr.Map.remove dst_ip !tcp_out ; Lwt.return_unit
in
loop ()
in
S.TCP.listen (S.tcp stack) ~port tcp_cb ;
Log.info (fun m -> m "secondary DNS listening on TCP port %d" port) ;
let rec time () =
let now = Mirage_ptime.now () in
let elapsed = Mirage_mtime.elapsed_ns () in
let t, out = Dns_server.Secondary.timer !state now elapsed in
maybe_update_state t >>= fun () ->
List.iter (fun (ip, cs) ->
Lwt.async (fun () -> request ~timer:true (ip, cs))) out ;
Mirage_sleep.ns (Duration.of_sec timer) >>= fun () ->
time ()
in
Lwt.async time
end

View file

@ -0,0 +1,30 @@
(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)
module Make (S : Tcpip.Stack.V4V6) : sig
val primary :
?on_update:(old:Dns_trie.t -> authenticated_key:[`raw] Domain_name.t option -> update_source:Ipaddr.t -> Dns_server.Primary.s -> unit Lwt.t) ->
?on_notify:([ `Notify of Dns.Soa.t option | `Signed_notify of Dns.Soa.t option ] ->
Dns_server.Primary.s ->
(Dns_trie.t * ([ `raw ] Domain_name.t * Dns.Dnskey.t) list) option Lwt.t) ->
?timer:int -> ?port:int -> S.t -> Dns_server.Primary.s -> unit
(** [primary ~on_update ~timer ~port stack primary] starts a primary server on
[port] (default 53, both TCP and UDP) with the given [primary]
configuration. [timer] is the DNS notify timer in seconds, and defaults to
2 seconds. [on_update ~old ~authenticated_key ~update_source s] is a
callback if the data served by the primary server [s] got updated by a
potentially authenticated nsupdate packet, the used [authenticated_key]
and source [update_source] are passed to the callback. The
[on_notify notify s] callback is executed when a notify request is received
by the primary DNS server (may be used for signaling of a (hidden) DNS
secondary server). *)
val secondary :
?on_update:(old:Dns_trie.t -> Dns_server.Secondary.s -> unit Lwt.t) ->
?timer:int -> ?port:int -> S.t -> Dns_server.Secondary.s ->
unit
(** [secondary ~on_update ~timer ~port stack secondary] starts a secondary
server on [port] (default 53). The [on_update] callback is executed when
the zone changes. The [timer] (in seconds, defaults to 5 seconds) is used
for refreshing zones. *)
end

View file

@ -0,0 +1,5 @@
(library
(name dns_server_mirage)
(public_name dns-server.mirage)
(wrapped false)
(libraries dns dns-server dns-mirage lwt duration randomconv mirage-sleep mirage-ptime mirage-mtime tcpip metrics))

View file

@ -0,0 +1,402 @@
(* mirage stub resolver *)
open Lwt.Infix
open Dns
let src = Logs.Src.create "dns_stub_mirage" ~doc:"effectful DNS stub layer"
module Log = (val Logs.src_log src : Logs.LOG)
module Make (S : Tcpip.Stack.V4V6) = struct
(* data in the wild:
- a request comes in hdr, q
- q to be found in cache
- q not found in cache (to be forwarded to the recursive resolver)
- unless q in transit (this to-be-done if it is worth it (is it?))
- a fresh hdr, q is generated and sent to the recursive resolver
- now hdr, q is registered to be awaited for
-- we can either signal the request task once we found something,
or preserve the original hdr, q together with ip and port
- a reply goes out hdr, q, answer
the "Client" is only concerned about the connection to the resolver, with
multiplexing.
the current API is:
dns_client calls connect .. -> flow
send flow data
recv flow (* potentially multiple times *)
i.e. our flow being (int * _):
connect <nothing>
send (id, _) data <- id <- data[2..3]
recv (id, _) <- registers condition in N[id] ; waits ; removes condition
or phrased differently:
a recv_loop reads continously, whenever a full packet is received,
N[id] is woken up with the packet
*)
let metrics =
let f = function
| `Udp_queries -> "udp-queries"
| `Tcp_queries -> "tcp-queries"
| `Ocaml_queries -> "ocaml-queries"
| `Tcp_connections -> "tcp-connections"
| `Authoritative_answers -> "authoritative-answers"
| `Authoritative_errors -> "authoritative-errors"
| `Reserved_answers -> "reserved-answers"
| `On_update -> "on-update"
| `Resolver_queries -> "resolver-queries"
| `Resolver_answers -> "resolver-answers"
| `Resolver_nodata -> "resolver-nodata"
| `Resolver_nodomain -> "resolver-nodomain"
| `Resolver_servfail -> "resolver-servfail"
| `Resolver_notimp -> "resolver-notimplemented"
in
let metrics = Dns.counter_metrics ~f "stub-resolver" in
(fun x -> Metrics.add metrics (fun x -> x) (fun d -> d x))
module H = Happy_eyeballs_mirage.Make(S)
module Client = Dns_client_mirage.Make(S)(H)
module TLS = Tls_mirage.Make(S.TCP)
(* likely this should contain:
- a primary server (handling updates)
- a client on steroids: multiplexing on connections
- listening for DNS requests from clients:
first find them in primary server
if not authoritative, use the client
*)
(* task management
- multiple requests for the same name, type can be done at the same "time"
-> need to remember outstanding requests and signal to clients
*)
(* take multiple resolver IPs and round-robin / ask both (take first answer,
ignoring ServFail etc.) *)
(* timeout of resolver, retransmission (to another resolver / another flow) *)
module Dns_flow = Dns_mirage.Make(S)
type t = {
client : Client.t ;
reserved : Dns_server.t ;
mutable server : Dns_server.t ;
on_update : old:Dns_trie.t -> ?authenticated_key:[`raw] Domain_name.t -> update_source:Ipaddr.t -> Dns_trie.t -> unit Lwt.t ;
push : (Ipaddr.t * int * string * (int32 * string) Lwt.u) option -> unit ;
mutable update_tls : Tls.Config.server -> unit ;
}
let primary_data { server ; _ } =
server.Dns_server.data
let update_primary_data t trie =
let server = Dns_server.with_data t.server trie in
t.server <- server
let resolve_external { push ; _ } (ip, port) data =
let th, wk = Lwt.wait () in
push (Some (ip, port, data, wk));
th
let update_tls { update_tls ; _ } tls = update_tls tls
let build_reply header question proto ?additional data =
let ttl = Packet.minimum_ttl data in
let packet = Packet.create ?additional header question data in
ttl, fst (Packet.encode proto packet)
let query_server trie question data header proto =
match Dns_server.handle_question trie question with
| Ok (_flags, answer, additional) ->
(* TODO do sth with flags *)
metrics `Authoritative_answers;
let data = `Answer answer in
let ttl = Packet.minimum_ttl data in
let packet = Packet.create ?additional header question data in
let packet =
match Dns_block.edns packet with
| None -> packet
| Some edns ->
Dns_resolver_metrics.resolver_stats `Blocked;
Dns.Packet.with_edns packet (Some edns)
in
let reply = ttl, fst (Packet.encode proto packet) in
Some reply
| Error (Rcode.NotAuth, _) -> None
| Error (rcode, answer) ->
metrics `Authoritative_errors;
let data = `Rcode_error (rcode, Packet.opcode_data data, answer) in
let reply = build_reply header question proto data in
Some reply
let tsig_decode_sign server proto packet buf header question =
let now = Mirage_ptime.now () in
match Dns_server.handle_tsig server now packet buf with
| Error _ ->
let data =
`Rcode_error (Rcode.Refused, Packet.opcode_data packet.Packet.data, None)
in
let reply = build_reply header question proto data in
Error reply
| Ok k ->
let key =
match k with None -> None | Some (keyname, _, _, _) -> Some keyname
in
let sign data =
let ttl = Packet.minimum_ttl data in
let packet = Packet.create header question data in
match k with
| None -> Some (ttl, fst (Packet.encode proto packet))
| Some (keyname, _tsig, mac, dnskey) ->
match Dns_tsig.encode_and_sign ~proto ~mac packet now dnskey keyname with
| Error s -> Log.err (fun m -> m "error %a while signing answer" Dns_tsig.pp_s s); None
| Ok (cs, _) -> Some (ttl, cs)
in
Ok (key, sign)
let axfr_server server proto packet question buf header =
match tsig_decode_sign server proto packet buf header question with
| Error e -> Some e
| Ok (key, sign) ->
match Dns_server.handle_axfr_request server proto key question with
| Error rcode ->
let err = `Rcode_error (rcode, Packet.opcode_data packet.Packet.data, None) in
let reply = build_reply header question proto err in
Some reply
| Ok axfr ->
sign (`Axfr_reply axfr)
let update_server t proto ip packet question u buf header =
let server = t.server in
match tsig_decode_sign server proto packet buf header question with
| Error e -> Lwt.return (Some e)
| Ok (key, sign) ->
match Dns_server.handle_update server proto key question u with
| Ok (trie, _) ->
let old = server.data in
let server' = Dns_server.with_data server trie in
t.server <- server';
metrics `On_update;
t.on_update ~old ?authenticated_key:key ~update_source:ip trie >|= fun () ->
sign `Update_ack
| Error rcode ->
Lwt.return (sign (`Rcode_error (rcode, Opcode.Update, None)))
let server t proto ip packet header question data buf =
match data with
| `Query -> Lwt.return (query_server t.server question data header proto)
| `Axfr_request ->
Lwt.return (axfr_server t.server proto packet question buf header)
| `Update u ->
update_server t proto ip packet question u buf header
| _ ->
let data =
`Rcode_error (Rcode.NotImp, Packet.opcode_data packet.Packet.data, None)
in
let pkt = build_reply header question proto data in
Lwt.return (Some pkt)
let resolve t question data header proto =
metrics `Resolver_queries;
let name = fst question in
match data, snd question with
| `Query, `K Rr_map.K key ->
begin Client.get_resource_record t.client key name >|= function
| Error `Msg msg ->
Log.err (fun m -> m "couldn't resolve %s" msg);
let data = `Rcode_error (Rcode.ServFail, Opcode.Query, None) in
metrics `Resolver_servfail;
let reply = build_reply header question proto data in
Some reply
| Error `No_data (domain, soa) ->
let answer = (Name_rr_map.empty, Name_rr_map.singleton domain Soa soa) in
let data = `Answer answer in
metrics `Resolver_nodata;
let reply = build_reply header question proto data in
Some reply
| Error `No_domain (domain, soa) ->
let answer = (Name_rr_map.empty, Name_rr_map.singleton domain Soa soa) in
let data = `Rcode_error (Rcode.NXDomain, Opcode.Query, Some answer) in
metrics `Resolver_nodomain;
let reply = build_reply header question proto data in
Some reply
| Ok reply ->
let answer = (Name_rr_map.singleton name key reply, Name_rr_map.empty) in
let data = `Answer answer in
metrics `Resolver_answers;
let reply = build_reply header question proto data in
Some reply
end
| _ ->
Log.err (fun m -> m "not implemented %a, data %a"
Dns.Packet.Question.pp question
Dns.Packet.pp_data data);
let data = `Rcode_error (Rcode.NotImp, Packet.opcode_data data, None) in
metrics `Resolver_notimp;
let reply = build_reply header question proto data in
Lwt.return (Some reply)
(* we're now doing up to three lookups for each request:
- in authoritative server (Dns_trie)
- in reserved trie (Dns_trie)
- in resolver cache (Dns_cache)
- asking a remote resolver
instead, on startup authoritative (from external) could be merged with
reserved (but that makes data store very big and not easy to understand
(lots of files for the reserved zones)) *)
let handle t proto ip buf =
match Packet.decode buf with
| Error err ->
Log.err (fun m -> m "couldn't decode %a" Packet.pp_err err);
Dns_resolver_metrics.response_metric 0L;
Dns_resolver_metrics.resolver_stats `Error;
let answer = Packet.raw_error buf Rcode.FormErr in
Lwt.return (Option.map (fun r -> 0l, r) answer)
| Ok packet ->
Dns_resolver_metrics.resolver_stats `Queries;
let start = Mirage_mtime.elapsed_ns () in
let header, question, data = packet.Packet.header, packet.question, packet.data in
(* check header flags: recursion desired (and send recursion available) *)
(server t proto ip packet header question data buf >>= function
| Some data -> Lwt.return (Some data)
| None ->
(* next look in reserved trie! *)
match query_server t.reserved question data header proto with
| Some data -> metrics `Reserved_answers ; Lwt.return (Some data)
| None -> resolve t question data header proto) >|= fun reply ->
let stop = Mirage_mtime.elapsed_ns () in
Dns_resolver_metrics.response_metric (Int64.sub stop start);
reply
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 ()
type tls_flow = { tls_flow : TLS.flow ; mutable linger : Cstruct.t }
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
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
let create ?(cache_size = 10000) ?(udp = true) ?(tcp = true) ?(port = 53) ?tls ?(tls_port = 853) ?edns ?nameservers ?timeout ?(on_update = fun ~old:_ ?authenticated_key:_ ~update_source:_ _trie -> Lwt.return_unit) primary ~happy_eyeballs stack : t Lwt.t =
Client.connect ~cache_size ?edns ?nameservers ?timeout (stack, happy_eyeballs) >|= fun client ->
let server = Dns_server.Primary.server primary in
let stream, push = Lwt_stream.create () in
let reserved = Dns_server.create Dns_resolver_root.reserved Mirage_crypto_rng.generate in
let update_tls _ = () in
let t = { client ; reserved ; server ; on_update ; push ; update_tls } in
let udp_cb ~src ~dst:_ ~src_port buf =
let buf = Cstruct.to_string buf in
metrics `Udp_queries;
handle t `Udp src buf >>= function
| None -> Lwt.return_unit
| Some (_ttl, data) ->
let data = Cstruct.of_string data in
S.UDP.write ~src_port:port ~dst:src ~dst_port:src_port (S.udp stack) data >|= function
| Error e -> Log.warn (fun m -> m "udp: failure %a while sending to %a:%d"
S.UDP.pp_error e Ipaddr.pp src src_port)
| Ok () -> ()
in
if udp then
S.UDP.listen (S.udp stack) ~port udp_cb ;
let tcp_cb flow =
metrics `Tcp_connections;
let dst_ip, dst_port = S.TCP.dst flow in
Log.debug (fun m -> m "tcp connection from %a:%d" Ipaddr.pp dst_ip dst_port) ;
let f = Dns_flow.of_flow flow in
let rec loop () =
Dns_flow.read_tcp f >>= function
| Error () -> Lwt.return_unit
| Ok data ->
metrics `Tcp_queries;
let data = Cstruct.to_string data in
handle t `Tcp dst_ip data >>= function
| None ->
Log.warn (fun m -> m "no TCP output") ;
loop ()
| Some (_ttl, data) ->
let data = Cstruct.of_string data in
Dns_flow.send_tcp flow data >>= function
| Ok () -> loop ()
| Error () -> Lwt.return_unit
in
loop ()
in
if tcp then
S.TCP.listen (S.tcp stack) ~port tcp_cb;
let rec ocaml_cb () =
Lwt_stream.get stream >>= function
| Some (dst_ip, _dst_port, data, wk) ->
metrics `Ocaml_queries;
begin
handle t `Tcp dst_ip data >|= function
| None ->
Log.warn (fun m -> m "no TCP output")
| Some (ttl, data) ->
Lwt.wakeup wk (ttl, data);
end >>= fun () ->
ocaml_cb ()
| None -> Lwt.return_unit in
Lwt.async ocaml_cb;
let tls_cb cfg flow =
let dst_ip, dst_port = S.TCP.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);
let tls_and_linger = { tls_flow = tls ; linger = Cstruct.empty } in
let rec loop () =
read_tls_packet tls_and_linger >>= function
| Error () ->
Lwt.return_unit
| Ok data ->
let data = Cstruct.to_string data in
handle t `Tcp dst_ip data >>= function
| None ->
Log.warn (fun m -> m "no TLS output") ;
loop ()
| Some (_ttl, data) ->
let data = Cstruct.of_string data in
send_tls tls data >>= function
| Ok () -> loop ()
| Error () -> Lwt.return_unit
in
loop ()
in
let update_tls tls_cfg =
S.TCP.listen (S.tcp stack) ~port:tls_port (tls_cb tls_cfg);
in
t.update_tls <- update_tls;
(match tls with None -> () | Some cfg -> update_tls cfg);
t
end

View file

@ -0,0 +1,28 @@
(* (c) 2025 Hannes Mehnert, all rights reserved *)
module Make (S : Tcpip.Stack.V4V6) : sig
type t
module H : sig
include Happy_eyeballs_mirage.S with type stack = S.t and type flow = S.TCP.flow
val connect_device : ?aaaa_timeout:int64 -> ?connect_delay:int64 ->
?connect_timeout:int64 -> ?resolve_timeout:int64 -> ?resolve_retries:int ->
?timer_interval:int64 -> ?getaddrinfo:getaddrinfo -> stack -> t Lwt.t
end
val create : ?cache_size:int -> ?udp:bool -> ?tcp:bool -> ?port:int ->
?tls:Tls.Config.server -> ?tls_port:int ->
?edns:[ `Auto | `Manual of Dns.Edns.t | `None ] ->
?nameservers:string list ->
?timeout:int64 ->
?on_update:(old:Dns_trie.t -> ?authenticated_key:[ `raw ] Domain_name.t ->
update_source:Ipaddr.t -> Dns_trie.t -> unit Lwt.t) ->
Dns_server.Primary.s -> happy_eyeballs:H.t -> S.t -> t Lwt.t
(** [create ~cache_size ~edns ~nameservers ~timeout ~on_update server ~happy_eyeballs stack]
registers a stub resolver on the provided protocols [udp], [tcp], [tls]
using [port] for udp and tcp (defaults to 53), [tls_port] for tls (defaults
to 853) using the [resolver] configuration. The [timer] is in milliseconds
and defaults to 500 milliseconds.*)
include Dns_resolver_mirage_shared.S with type t := t
end

View file

@ -0,0 +1,5 @@
(library
(name dns_stub_mirage)
(public_name dns-stub.mirage)
(wrapped false)
(libraries dns dns-server dns-tsig metrics dns_resolver_shared dns-resolver.mirage.shared dns-mirage dns-client-mirage lwt mirage-ptime tcpip mirage-crypto-rng tls-mirage))