mirage icon indicating copy to clipboard operation
mirage copied to clipboard

Dhcp ipv4 and leases

Open reynir opened this issue 3 weeks ago • 5 comments

This depends on https://github.com/mirage/charrua/pull/142.

This PR makes available the DHCP lease from Mirage.ipv4_of_dhcp to configure other devices. As an example Mirage.generic_dns_client is extended so it can get DNS servers from DHCP. Another candidate could be syslog that may use DHCP log server as its log destination.

The code currently compiles, but I am almost certain it's not 100% correct. I am opening a draft PR as I think it is of interest to get early feedback on the overall design. I think the interface is how I want it; what needs work is the code generation that I'm almost certain isn't correct.

The ideas have been tested out in this PR: https://github.com/robur-coop/traceroute/pull/7

The new Mirage.dhcp_requests type contains mutable state. This is somewhat straying away from the current style. I think however it has merit. The challenge is that between a device x and a dhcp_ipv4 there is a sort of mutual dependency: x wants a lease from dhcp_ipv4 while dhcp_ipv4 is required to know what DHCP option x and any other dependents are interested in. I think with mutable state we have a somewhat nice way of propagating back from x to dhcp_ipv4 what dhcp options we are interested in.

reynir avatar Dec 03 '25 10:12 reynir

I did some fixes and now I can compile the following unikernel which gets its ip address and its dns servers from DHCP and does a HTTP request. What I don't like so much so far is I had to build the TCP/IP stack manually. It would be great to use generic_stackv4v6 and I may look into modifying that.

Unikernel code and run output

Config.ml:

open Mirage

let client =
  main "Unikernel.Client" (alpn_client @-> job)

let () =
  let e = ethif default_network in
  let a = arp e in
  let dhcp_requests, dhcp_ipv4 = ipv4_of_dhcp default_network e a in
  let stack =
    direct_stackv4v6 (dhcp_proj_net $ dhcp_ipv4) e a (dhcp_proj_ipv4 $ dhcp_ipv4)
      (create_ipv6 (dhcp_proj_net $ dhcp_ipv4) e)
  in
  let he = generic_happy_eyeballs stack in
  let dns = generic_dns_client ~dhcp:(dhcp_requests, dhcp_proj_lease $ dhcp_ipv4) stack he in
  let alpn_client =
    let mimic = mimic_happy_eyeballs stack he dns in
    paf_client (tcpv4v6_of_stackv4v6 stack) mimic
  in
  register "http-fetch" [ client $ alpn_client ]

Unikernel.ml:

open Lwt.Syntax

module K = struct
  open Cmdliner
  let url =
    let doc = Arg.info ~doc:"URL to fetch" [ "url" ] in
    Mirage_runtime.register_arg @@
    Arg.(value & opt string "https://mirageos.org/" doc)
end

module Client (_ : Http_mirage_client.S) = struct
  let start http =
    let url = K.url () in
    Fmt.pr "Fetching %s with http-mirage-client\n\n%!" url;
    let+ r =
      Http_mirage_client.request http url
        (fun _response len data -> Lwt.return (len + String.length data))
        0
    in
    match r with
    | Error e -> Fmt.pr "Error fetching: %a\n%!" Mimic.pp_error e
    | Ok (_response, len) ->
      Fmt.pr "Received body length: %u\n" len;
      Fmt.pr "http-mirage-client fetch done\n------------\n%!"
end

Sample run with no arguments on a network with DHCP that says to use DNS server 172.23.0.53:

console 2025-12-05T11:42:06-00:00:             |      ___|
console 2025-12-05T11:42:06-00:00:   __|  _ \  |  _ \ __ \
console 2025-12-05T11:42:06-00:00: \__ \ (   | | (   |  ) |
console 2025-12-05T11:42:06-00:00: ____/\___/ _|\___/____/
console 2025-12-05T11:42:06-00:00: Solo5: Bindings version v0.10.0
console 2025-12-05T11:42:06-00:00: Solo5: Memory map: 32 MB addressable:
console 2025-12-05T11:42:06-00:00: Solo5:   reserved @ (0x0 - 0xfffff)
console 2025-12-05T11:42:06-00:00: Solo5:       text @ (0x100000 - 0x47bfff)
console 2025-12-05T11:42:06-00:00: Solo5:     rodata @ (0x47c000 - 0x53efff)
console 2025-12-05T11:42:06-00:00: Solo5:       data @ (0x53f000 - 0x8b1fff)
console 2025-12-05T11:42:06-00:00: Solo5:       heap >= 0x8b2000 < stack < 0x2000000
console 2025-12-05T11:42:06-00:00: 2025-12-05T11:42:06-00:00: [INFO] [netif] Plugging into service with mac 00:80:41:28:4e:fe mtu 1500
console 2025-12-05T11:42:06-00:00: 2025-12-05T11:42:06-00:00: [INFO] [ethernet] Connected Ethernet interface 00:80:41:28:4e:fe
console 2025-12-05T11:42:06-00:00: 2025-12-05T11:42:06-00:00: [INFO] [dhcp_client_lwt] Lease obtained! IP: 172.23.1.129, routers: 172.23.0.1
console 2025-12-05T11:42:06-00:00: 2025-12-05T11:42:06-00:00: [INFO] [ARP] Sending gratuitous ARP for 172.23.1.129 (00:80:41:28:4e:fe)
console 2025-12-05T11:42:06-00:00: 2025-12-05T11:42:06-00:00: [INFO] [ipv6] IP6: Starting
console 2025-12-05T11:42:06-00:00: 2025-12-05T11:42:06-00:00: [INFO] [ndpc6] ND6: Unsupported ND option in RA: ty=14 len=1
console 2025-12-05T11:42:07-00:00: 2025-12-05T11:42:07-00:00: [INFO] [ndpc6] IP6: Processing unknown option, MSB 5
console 2025-12-05T11:42:07-00:00: 2025-12-05T11:42:07-00:00: [INFO] [ndpc6] ICMP6: Unknown packet type: ty=143
console 2025-12-05T11:42:07-00:00: 2025-12-05T11:42:07-00:00: [INFO] [ipv6] IP6: Started with fe80::280:41ff:fe28:4efe
console 2025-12-05T11:42:07-00:00: 2025-12-05T11:42:07-00:00: [INFO] [tcp.pcb] TCP layer connected on 172.23.1.129/16, fe80::280:41ff:fe28:4efe/64
console 2025-12-05T11:42:07-00:00: 2025-12-05T11:42:07-00:00: [INFO] [udp] UDP layer connected on 172.23.1.129/16, fe80::280:41ff:fe28:4efe/64
console 2025-12-05T11:42:07-00:00: 2025-12-05T11:42:07-00:00: [INFO] [tcpip-stack-direct] Dual TCP/IP stack assembled: mac=00:80:41:28:4e:fe,ip=172.23.1.129/16, fe80::280:41ff:fe28:4efe/64
console 2025-12-05T11:42:07-00:00: 2025-12-05T11:42:07-00:00: [WARNING] [dns_client_mirage] ignoring UDP nameservers 172.23.0.53:53, using TCP nameservers 172.23.0.53:53
console 2025-12-05T11:42:07-00:00: Fetching https://mirageos.org/ with http-mirage-client
console 2025-12-05T11:42:07-00:00: 2025-12-05T11:42:07-00:00: [INFO] [ndpc6] IP6: Processing unknown option, MSB 5
console 2025-12-05T11:42:07-00:00: 2025-12-05T11:42:07-00:00: [INFO] [ndpc6] ICMP6: Unknown packet type: ty=143
console 2025-12-05T11:42:07-00:00: 2025-12-05T11:42:07-00:00: [INFO] [ndpc6] IP6: Processing unknown option, MSB 5
console 2025-12-05T11:42:07-00:00: 2025-12-05T11:42:07-00:00: [INFO] [ndpc6] ICMP6: Unknown packet type: ty=143
console 2025-12-05T11:42:08-00:00: Received body length: 19711
console 2025-12-05T11:42:08-00:00: http-mirage-client fetch done
console 2025-12-05T11:42:08-00:00: ------------
console 2025-12-05T11:42:08-00:00: Solo5: solo5_exit(0) called

Using the mirage-skeleton/device-usage/http-fetch/ code I need to pass --dhcp=true to mirage configure before dhcp works. It seems to me we may want to change the default so we explicitly have to disable dhcp. With current --dhcp=true the usage of dhcp is decided at runtime depending on the --ipv4= etc arguments.

reynir avatar Dec 05 '25 12:12 reynir

Using the mirage-skeleton/device-usage/http-fetch/ code I need to pass --dhcp=true to mirage configure before dhcp works. It seems to me we may want to change the default so we explicitly have to disable dhcp.

Yes, I'm completely in favour to remove this dhcp flag for configure time (or make it default to true); and have by default the decision at run-time whether to use DHCP or not (by providing the --ipv4=.. argument (to avoid DHCP), or not providing it (so the unikernel will ask for DHCP)).

hannesm avatar Dec 05 '25 12:12 hannesm

I changed the default of --dhcp and I added runtime options to the dhcp thing (such as --ipv4, --ipv4-gateway, and the no_init --ipv6-only).

I attempted to change the generic_stackv4v6 so we can interact with the dhcp, but I struggle as generic_stackv4 is either a socket thingie, or it's a direct thingie which may or may not do dhcp. So maybe the dhcp_requests value should be an argument to ipv4_of_dhcp?? so we can create one in any case and just ignore it if it isn't in the case of direct and dhcp.

reynir avatar Dec 05 '25 13:12 reynir

The first commit I just pushed makes the following changes to the config.ml from before necessary

--- config.ml'	2025-12-05 14:44:30.298736766 +0100
+++ config.ml	2025-12-05 14:43:16.848612714 +0100
@@ -4,9 +4,10 @@
   main "Unikernel.Client" (alpn_client @-> job)
 
 let () =
+  let dhcp_requests = make_dhcp_requests () in
   let e = ethif default_network in
   let a = arp e in
-  let dhcp_requests, dhcp_ipv4 = ipv4_of_dhcp default_network e a in
+  let dhcp_ipv4 = ipv4_of_dhcp ~dhcp_requests default_network e a in
   let stack =
     direct_stackv4v6 (dhcp_proj_net $ dhcp_ipv4) e a (dhcp_proj_ipv4 $ dhcp_ipv4)
       (create_ipv6 (dhcp_proj_net $ dhcp_ipv4) e)

The second requires following changes:

--- config.ml'	2025-12-05 14:44:30.298736766 +0100
+++ config.ml	2025-12-05 15:06:09.777211090 +0100
@@ -4,15 +4,10 @@
   main "Unikernel.Client" (alpn_client @-> job)
 
 let () =
-  let e = ethif default_network in
-  let a = arp e in
-  let dhcp_requests, dhcp_ipv4 = ipv4_of_dhcp default_network e a in
-  let stack =
-    direct_stackv4v6 (dhcp_proj_net $ dhcp_ipv4) e a (dhcp_proj_ipv4 $ dhcp_ipv4)
-      (create_ipv6 (dhcp_proj_net $ dhcp_ipv4) e)
-  in
+  let dhcp_requests = make_dhcp_requests () in
+  let stack, lease = generic_stackv4v6_with_lease ~dhcp_requests default_network in
   let he = generic_happy_eyeballs stack in
-  let dns = generic_dns_client ~dhcp:(dhcp_requests, dhcp_proj_lease $ dhcp_ipv4) stack he in
+  let dns = generic_dns_client ~dhcp:(dhcp_requests, lease) stack he in
   let alpn_client =
     let mimic = mimic_happy_eyeballs stack he dns in
     paf_client (tcpv4v6_of_stackv4v6 stack) mimic

As you can see there is now a Mirage.generic_stackv4v6_with_lease which returns a pair of devices: a stack and a lease.

reynir avatar Dec 05 '25 14:12 reynir

Thank you @reynir for your work on that.

I'll try to give a try next week with an update to the example from mirage-nat (https://github.com/mirage/mirage-nat/blob/0f62253d76e957678a30bb39138cf7c4a142a486/example/config.ml#L28-L31) :)

palainp avatar Dec 06 '25 16:12 palainp