mirage / ocaml-dns

OCaml implementation of the DNS protocol
BSD 2-Clause "Simplified" License
105 stars 43 forks source link

Directly use Happy_eyeballs_lwt instead of a copy of it #346

Closed dinosaure closed 5 months ago

dinosaure commented 1 year ago

This commit delete a dependency cycle between happy-eyeballs, happy-eyeballs-lwt, dns and dns-client-lwt. The basic happy-eyeballs-lwt implementation is not able yet to resolve domain-name but the user can inject a getaddrinfo which may come from dns-client-lwt. The idea is: 1) create a happy-eyeballs-lwt instance 2) create a dns-client-lwt instance 3) inject Dns_client_lwt.getaddrinfo into our happy-eyeballs-lwt instance

This patch delete a duplicate code about happy-eyeballs implementations.

Related to robur-coop/happy-eyeballs#38

dinosaure commented 1 year ago

We probably can fully delete the cycle between happy-eyeballs-lwt and dns-client-lwt and replace 'response Dns.Rr_map.key by a polymorphic variant for instance.

hannesm commented 1 year ago

We probably can fully delete the cycle between happy-eyeballs-lwt and dns-client-lwt and replace 'response Dns.Rr_map.key by a polymorphic variant for instance.

Indeed, but that would be rather itchy, no? We'd need to have `A | `AAAA as queries, and then `A of Ipaddr.V4.t set (or list) | `AAAA of Ipaddr.V6.t set as replies... I'm quite fine with having happy-eyeballs-lwt depend on Dns.

hannesm commented 1 year ago

Hmm, I guess I'm still a bit puzzled. What I thought about the API:

So, what is there to note? I believe each application should only have a single happy-eyeballs instance. There's no need for having multiple (there would be if there would be multiple network stacks, but there aren't). A single instance means a shared DNS cache, and a simpler way to debug everything. With this PR, the dns-client-lwt uses the very same happy-eyeballs instance \o/

So, for a client there are then two choices (by default):

Does this make sense? This also means that we do not need a inject operation, but can just provide the getaddrinfo / getaddrinfo6 when we instantiate the happy-eyeballs.

hannesm commented 1 year ago

issues with the proposal above (and my changes to your happy-eyeballs PR):

So, another proposal:

hannesm commented 1 year ago

see 3fc14f0b0da8aac45547424f0c7ce0182ae137be what I have in mind :)

hannesm commented 6 months ago

I rebased and added some commits, this is now as well adapting dns-client-mirage to use happy-eyeballs-mirage.

A review would be great (API + whether it works), it seems to compile, but I haven't tested it.

dinosaure commented 6 months ago

I finally tried to make a simple unikernel.ml:

let ( let* ) = Lwt.bind

module Make
  (S : Tcpip.Stack.V4V6)
  (H : Happy_eyeballs_mirage.S with type flow = S.TCP.flow)
  (D : Dns_client_mirage.S) = struct
  let start _stack he _dns =
    let* flow = H.connect he "google.com" [ 80; 443 ] in
    match flow with
    | Ok ((ipaddr, port), flow) ->
      Fmt.pr ">>> connected to google.com via %a:%d\n%!" Ipaddr.pp ipaddr port;
      S.TCP.close flow
    | Error (`Msg msg) -> failwith msg
end

And this is the config.ml.

open Mirage

let connect_handler =
  main "Unikernel.Make"
    (stackv4v6 @-> happy_eyeballs @-> dns_client @-> job)

let stackv4v6 = generic_stackv4v6 default_network
let happy_eyeballs = generic_happy_eyeballs stackv4v6
let dns = generic_dns_client stackv4v6 happy_eyeballs

let () = register "connect" [ connect_handler $ stackv4v6 $ happy_eyeballs $ dns ]

Of course, we need to upgrade the mirage tool and reverse again the dependency. I did a draft here: mirage/mirage#1543. WDYT?

hannesm commented 6 months ago

your example looks nice. but can we remove the "dns" and generic_dns_client stackv4v6 happy_eyeballs from config.ml?

dinosaure commented 6 months ago

your example looks nice. but can we remove the "dns" and generic_dns_client stackv4v6 happy_eyeballs from config.ml?

So we can ignore it but we must explicitly say that we want the DNS stack at some points to be able to do some DNS resolution into our happy-eyeballs instance. We can make a "super device" in the mirage tool then which aggregates happy_eyeballs's arguments (timeout) and dns's arguments (nameservers) to return an happy_eyeballs instance which is able to resolve domain-names (and just ignore then the dns instance) but such things can be done only into the mirage tool - I don't have any idea to do so at this level (without the mirage tool) nicely.

hannesm commented 6 months ago

And looking into your code, esp. in the mirage changes, I guess:

but I've no idea how we can write that in the mirage tool. maybe we can depend on generic_dns_client in happy_eyeballs device, and then bind a getaddrinfo and call inject from the mirage tool?

hannesm commented 6 months ago

I'd like a user of happy eyeballs device be:

let ( let* ) = Lwt.bind

module Make
  (S : Tcpip.Stack.V4V6)
  (H : Happy_eyeballs_mirage.S with type flow = S.TCP.flow) = struct

  let start _stack he =
    let* flow = H.connect he "google.com" [ 80; 443 ] in
    match flow with
    | Ok ((ipaddr, port), flow) ->
      Fmt.pr ">>> connected to google.com via %a:%d\n%!" Ipaddr.pp ipaddr port;
      S.TCP.close flow
    | Error (`Msg msg) -> failwith msg
end

And this is the config.ml.

open Mirage

let connect_handler =
  main "Unikernel.Make"
    (stackv4v6 @-> happy_eyeballs @-> job)

let stackv4v6 = generic_stackv4v6 default_network
let happy_eyeballs = generic_happy_eyeballs stackv4v6

let () = register "connect" [ connect_handler $ stackv4v6 $ happy_eyeballs ]

Do you think that is possible?

dinosaure commented 6 months ago

To re-centralize my initial point, this PR and the PR on happy-eyeballs re-contextualize happy-eyeballs as a mechanic to handle something like connect(2) with proper timeouts firstly. Then, we can (and usually we do) extend it via a DNS stack (ocaml-dns) to be able to gethostbyname(3) and connect(2) but, for my point of view, it's not necessary the case - we could also want an happy-eyeballs without the DNS stack. This view is concretized by the fact that we must explicitly say that we want a DNS stack via generic_dns_client.

Do you think that is possible?

So it's actually possible to hide everything and say that the given happy-eyeballs allocates a dns-client and do the Happy_eyeballs_mirage.inject in the produced code of the mirage tool - we have two possibilities here, we can just use a first-class module and locally apply the Dns_client_mirage.Make client and create a dns-client instance & inject it and returns the happy_eyeballs instance or we can provide another module like Happy_eyeballs_mirage_with_dns.Make which will do the plumbing for us. But this means that a DNS instance is forgotten for the user perspective.

If we go down this path, we must extend arguments of Mirage.happy_eyeballs to be able to specify dns-clients's arguments like nameservers (and Mirage.happy_eyeballs already has several arguments).

hannesm commented 6 months ago

From our discussion on IRC, we figured:

The reasoning for the latter is that we will use only a single happy-eyeballs instance across the entire unikernel:

We should settle this (as mentioned, it likely requires some minor modification to happy-eyeballs) to get this through the door and cut a release of DNS. I plan to work on this today.

hannesm commented 6 months ago

Timeouts and delays can be specified during establishing a connection in happy-eyeballs with https://github.com/robur-coop/happy-eyeballs/pull/42

hannesm commented 5 months ago

Ok, I updated this PR in respect to the happy-eyeballs 1.1.0 release, and the discussion we had on IRC @dinosaure. Would be great if you could take a look into the recent commits, and whether this is in line with what you think should be done.

Now, there's happy_eyeballs being passed around and extended with getaddrinfo. The timeout for connect_ips is the one passed to Dns_client.create -- which is a good approximation (still not the right value, as remarked in the source comment).

dinosaure commented 5 months ago

I updated the mirage/mirage#1543 according to your recent change. The unikernel itself did not change but it compiles again if you pin this version of dns-client-mirage.

dinosaure commented 5 months ago

For me, the PR is ok to merge now :+1:. And the mirage devices are updated. The example given below works and was tested.