From c7d8751b1c800726ac7f8e7bdd69d1a521f0e0c2 Mon Sep 17 00:00:00 2001 From: Calascibetta Romain Date: Wed, 22 May 2024 09:41:11 +0200 Subject: [PATCH 01/10] Use Lwt.Syntax and avoid some >>= fun () patterns --- dao.ml | 60 +++++++++++++++++++++++----------------------------- unikernel.ml | 11 ++++------ 2 files changed, 30 insertions(+), 41 deletions(-) diff --git a/dao.ml b/dao.ml index 2361630..78f0065 100644 --- a/dao.ml +++ b/dao.ml @@ -65,43 +65,35 @@ let read_rules rules client_ip = number = 0;})] let vifs client domid = + let open Lwt.Syntax in match int_of_string_opt domid with | None -> Log.err (fun f -> f "Invalid domid %S" domid); Lwt.return [] | Some domid -> - let path = Printf.sprintf "backend/vif/%d" domid in - Xen_os.Xs.immediate client (fun handle -> - directory ~handle path >>= - Lwt_list.filter_map_p (fun device_id -> - match int_of_string_opt device_id with - | None -> Log.err (fun f -> f "Invalid device ID %S for domid %d" device_id domid); Lwt.return_none - | Some device_id -> - let vif = { ClientVif.domid; device_id } in - Lwt.try_bind - (fun () -> Xen_os.Xs.read handle (Printf.sprintf "%s/%d/ip" path device_id)) - (fun client_ip -> - let client_ip' = match String.split_on_char ' ' client_ip with - | [] -> Log.err (fun m -> m "unexpected empty list"); "" - | [ ip ] -> ip - | ip::rest -> - Log.warn (fun m -> m "ignoring IPs %s from %a, we support one IP per client" - (String.concat " " rest) ClientVif.pp vif); - ip - in - match Ipaddr.V4.of_string client_ip' with - | Ok ip -> Lwt.return (Some (vif, ip)) - | Error `Msg msg -> - Log.err (fun f -> f "Error parsing IP address of %a from %s: %s" - ClientVif.pp vif client_ip msg); - Lwt.return None - ) - (function - | Xs_protocol.Enoent _ -> Lwt.return None - | ex -> - Log.err (fun f -> f "Error getting IP address of %a: %s" - ClientVif.pp vif (Printexc.to_string ex)); - Lwt.return None - ) - )) + let path = Fmt.str "backend/vif/%d" domid in + let fn handle = + let* entries = directory ~handle path in + let fn device_id = match int_of_string_opt device_id with + | None -> + Log.err (fun f -> f "Invalid device ID %S for domid %d" device_id domid); + Lwt.return_none + | Some device_id -> + let vif = { ClientVif.domid; device_id } in + let fn () = + let* str = Xen_os.Xs.read handle (Fmt.str "%s/%d/ip" path device_id) in + let[@warning "-8"] client_ip :: _ = String.split_on_char ' ' str in + Lwt.return_some (vif, Ipaddr.V4.of_string_exn client_ip) in + Lwt.catch fn @@ function + | Xs_protocol.Enoent _ -> Lwt.return_none + | Ipaddr.Parse_error (msg, client_ip) -> + Log.err (fun f -> f "Error parsing IP address of %a from %s: %s" + ClientVif.pp vif client_ip msg); + Lwt.return_none + | exn -> + Log.err (fun f -> f "Error getting IP address of %a: %s" + ClientVif.pp vif (Printexc.to_string exn)); + Lwt.return_none in + Lwt_list.filter_map_p fn entries in + Xen_os.Xs.immediate client fn let watch_clients fn = Xen_os.Xs.make () >>= fun xs -> diff --git a/unikernel.ml b/unikernel.ml index b64fd4e..f0e12df 100644 --- a/unikernel.ml +++ b/unikernel.ml @@ -46,15 +46,12 @@ module Main (R : Mirage_crypto_rng_mirage.S)(Clock : Mirage_clock.MCLOCK)(Time : (* Main unikernel entry point (called from auto-generated main.ml). *) let start _random _clock _time = + let open Lwt.Syntax in let start_time = Clock.elapsed_ns () in (* Start qrexec agent and QubesDB agent in parallel *) - let qrexec = RExec.connect ~domid:0 () in - let qubesDB = DB.connect ~domid:0 () in - - (* Wait for clients to connect *) - qrexec >>= fun qrexec -> + let* qrexec = RExec.connect ~domid:0 () in let agent_listener = RExec.listen qrexec Command.handler in - qubesDB >>= fun qubesDB -> + let* qubesDB = DB.connect ~domid:0 () in let startup_time = let (-) = Int64.sub in let time_in_ns = Clock.elapsed_ns () - start_time in @@ -93,7 +90,7 @@ module Main (R : Mirage_crypto_rng_mirage.S)(Clock : Mirage_clock.MCLOCK)(Time : Dao.print_network_config config ; (* Set up client-side networking *) - Client_eth.create config >>= fun clients -> + let* clients = Client_eth.create config in (* Set up routing between networks and hosts *) let router = Dispatcher.create From 98506f5b1b28c8b65d5da9b368f587807400f67b Mon Sep 17 00:00:00 2001 From: Calascibetta Romain Date: Wed, 22 May 2024 11:37:19 +0200 Subject: [PATCH 02/10] Rename some generic fn functions to what they explicitly do --- dao.ml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dao.ml b/dao.ml index 78f0065..0e48a21 100644 --- a/dao.ml +++ b/dao.ml @@ -70,19 +70,19 @@ let vifs client domid = | None -> Log.err (fun f -> f "Invalid domid %S" domid); Lwt.return [] | Some domid -> let path = Fmt.str "backend/vif/%d" domid in - let fn handle = - let* entries = directory ~handle path in - let fn device_id = match int_of_string_opt device_id with + let vifs_of_domain handle = + let* devices = directory ~handle path in + let ip_of_vif device_id = match int_of_string_opt device_id with | None -> Log.err (fun f -> f "Invalid device ID %S for domid %d" device_id domid); Lwt.return_none | Some device_id -> let vif = { ClientVif.domid; device_id } in - let fn () = + let get_client_ip () = let* str = Xen_os.Xs.read handle (Fmt.str "%s/%d/ip" path device_id) in let[@warning "-8"] client_ip :: _ = String.split_on_char ' ' str in Lwt.return_some (vif, Ipaddr.V4.of_string_exn client_ip) in - Lwt.catch fn @@ function + Lwt.catch get_client_ip @@ function | Xs_protocol.Enoent _ -> Lwt.return_none | Ipaddr.Parse_error (msg, client_ip) -> Log.err (fun f -> f "Error parsing IP address of %a from %s: %s" @@ -92,8 +92,8 @@ let vifs client domid = Log.err (fun f -> f "Error getting IP address of %a: %s" ClientVif.pp vif (Printexc.to_string exn)); Lwt.return_none in - Lwt_list.filter_map_p fn entries in - Xen_os.Xs.immediate client fn + Lwt_list.filter_map_p ip_of_vif devices in + Xen_os.Xs.immediate client vifs_of_domain let watch_clients fn = Xen_os.Xs.make () >>= fun xs -> From e179ee36b3d33fd3286ec0401202873a31c5b480 Mon Sep 17 00:00:00 2001 From: Calascibetta Romain Date: Wed, 22 May 2024 11:39:37 +0200 Subject: [PATCH 03/10] Use List.hd instead of [@warning "-8"] --- dao.ml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dao.ml b/dao.ml index 0e48a21..f008d57 100644 --- a/dao.ml +++ b/dao.ml @@ -80,7 +80,7 @@ let vifs client domid = let vif = { ClientVif.domid; device_id } in let get_client_ip () = let* str = Xen_os.Xs.read handle (Fmt.str "%s/%d/ip" path device_id) in - let[@warning "-8"] client_ip :: _ = String.split_on_char ' ' str in + let client_ip = List.hd (String.split_on_char ' ' str) in Lwt.return_some (vif, Ipaddr.V4.of_string_exn client_ip) in Lwt.catch get_client_ip @@ function | Xs_protocol.Enoent _ -> Lwt.return_none From ad1afe99eeda8d7f7ca799e6fa1b891a40a60122 Mon Sep 17 00:00:00 2001 From: Calascibetta Romain Date: Wed, 22 May 2024 11:40:08 +0200 Subject: [PATCH 04/10] Break the line before the 'in' for a multi-line 'let ... in' --- dao.ml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/dao.ml b/dao.ml index f008d57..3e57781 100644 --- a/dao.ml +++ b/dao.ml @@ -81,7 +81,8 @@ let vifs client domid = let get_client_ip () = let* str = Xen_os.Xs.read handle (Fmt.str "%s/%d/ip" path device_id) in let client_ip = List.hd (String.split_on_char ' ' str) in - Lwt.return_some (vif, Ipaddr.V4.of_string_exn client_ip) in + Lwt.return_some (vif, Ipaddr.V4.of_string_exn client_ip) + in Lwt.catch get_client_ip @@ function | Xs_protocol.Enoent _ -> Lwt.return_none | Ipaddr.Parse_error (msg, client_ip) -> @@ -91,8 +92,10 @@ let vifs client domid = | exn -> Log.err (fun f -> f "Error getting IP address of %a: %s" ClientVif.pp vif (Printexc.to_string exn)); - Lwt.return_none in - Lwt_list.filter_map_p ip_of_vif devices in + Lwt.return_none + in + Lwt_list.filter_map_p ip_of_vif devices + in Xen_os.Xs.immediate client vifs_of_domain let watch_clients fn = From 3dc545681de71e5df436761ad301a5770d1e5b4b Mon Sep 17 00:00:00 2001 From: Calascibetta Romain Date: Wed, 22 May 2024 11:47:10 +0200 Subject: [PATCH 05/10] Add a comment about our usage of List.hd (which can fail) and String.split_on_char --- dao.ml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dao.ml b/dao.ml index 3e57781..2e94660 100644 --- a/dao.ml +++ b/dao.ml @@ -81,6 +81,8 @@ let vifs client domid = let get_client_ip () = let* str = Xen_os.Xs.read handle (Fmt.str "%s/%d/ip" path device_id) in let client_ip = List.hd (String.split_on_char ' ' str) in + (* XXX(dinosaure): it's safe to use [List.hd] here, + [String.split_on_char] can not return an empty list. *) Lwt.return_some (vif, Ipaddr.V4.of_string_exn client_ip) in Lwt.catch get_client_ip @@ function From a7cb153ee17246dc850f01b96121d868621df520 Mon Sep 17 00:00:00 2001 From: Calascibetta Romain Date: Wed, 22 May 2024 11:54:07 +0200 Subject: [PATCH 06/10] Use Ipaddr.V4.Map instead of our own IpMap (the first is available since ipaddr.5.2.0) --- client_eth.ml | 16 ++++++++-------- fw_utils.ml | 8 -------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/client_eth.ml b/client_eth.ml index de41f70..fc0b01a 100644 --- a/client_eth.ml +++ b/client_eth.ml @@ -8,7 +8,7 @@ let src = Logs.Src.create "client_eth" ~doc:"Ethernet networks for NetVM clients module Log = (val Logs.src_log src : Logs.LOG) type t = { - mutable iface_of_ip : client_link IpMap.t; + mutable iface_of_ip : client_link Ipaddr.V4.Map.t; changed : unit Lwt_condition.t; (* Fires when [iface_of_ip] changes. *) my_ip : Ipaddr.V4.t; (* The IP that clients are given as their default gateway. *) } @@ -21,21 +21,21 @@ type host = let create config = let changed = Lwt_condition.create () in let my_ip = config.Dao.our_ip in - Lwt.return { iface_of_ip = IpMap.empty; my_ip; changed } + Lwt.return { iface_of_ip = Ipaddr.V4.Map.empty; my_ip; changed } let client_gw t = t.my_ip let add_client t iface = let ip = iface#other_ip in let rec aux () = - match IpMap.find ip t.iface_of_ip with + match Ipaddr.V4.Map.find_opt ip t.iface_of_ip with | Some old -> (* Wait for old client to disappear before adding one with the same IP address. Otherwise, its [remove_client] call will remove the new client instead. *) Log.info (fun f -> f ~header:iface#log_header "Waiting for old client %s to go away before accepting new one" old#log_header); Lwt_condition.wait t.changed >>= aux | None -> - t.iface_of_ip <- t.iface_of_ip |> IpMap.add ip iface; + t.iface_of_ip <- t.iface_of_ip |> Ipaddr.V4.Map.add ip iface; Lwt_condition.broadcast t.changed (); Lwt.return_unit in @@ -43,11 +43,11 @@ let add_client t iface = let remove_client t iface = let ip = iface#other_ip in - assert (IpMap.mem ip t.iface_of_ip); - t.iface_of_ip <- t.iface_of_ip |> IpMap.remove ip; + assert (Ipaddr.V4.Map.mem ip t.iface_of_ip); + t.iface_of_ip <- t.iface_of_ip |> Ipaddr.V4.Map.remove ip; Lwt_condition.broadcast t.changed () -let lookup t ip = IpMap.find ip t.iface_of_ip +let lookup t ip = Ipaddr.V4.Map.find_opt ip t.iface_of_ip let classify t ip = match ip with @@ -79,7 +79,7 @@ module ARP = struct (* We're now treating client networks as point-to-point links, so we no longer respond on behalf of other clients. *) (* - else match IpMap.find ip t.net.iface_of_ip with + else match Ipaddr.V4.Map.find_opt ip t.net.iface_of_ip with | Some client_iface -> Some client_iface#other_mac | None -> None *) diff --git a/fw_utils.ml b/fw_utils.ml index 0307810..f20c63a 100644 --- a/fw_utils.ml +++ b/fw_utils.ml @@ -3,14 +3,6 @@ (** General utility functions. *) -module IpMap = struct - include Map.Make(Ipaddr.V4) - let find x map = - try Some (find x map) - with Not_found -> None - | _ -> Logs.err( fun f -> f "uncaught exception in find...%!"); None -end - (** An Ethernet interface. *) class type interface = object method my_mac : Macaddr.t From 12ed2b268dbf672a4771bc3b04c133a3ea9a79c4 Mon Sep 17 00:00:00 2001 From: Calascibetta Romain Date: Wed, 22 May 2024 16:05:29 +0200 Subject: [PATCH 07/10] Replace the Lwt.async into the right context and localize the global clients map We currently try to spawn 2 fibers [qubes_updated] and [listener] per clients and we already finalise them correctly if the client is disconnected. However, the Lwt.async is localized into add_client instead of where we attach a finalisers for these tasks. The first objective of this patch is to be sure that the Lwt.async is near where we registerd cancellation of these tasks. The second part is to localize the global clients to avoid the ability to read/write on it somewhere else. Only Dispatcher.watch_clients uses it - so it corresponds to a free variable of the Dispatcher.watch_clients closure. --- dao.ml | 2 +- dao.mli | 2 +- dispatcher.ml | 77 ++++++++++++++++++++++++++++----------------------- 3 files changed, 44 insertions(+), 37 deletions(-) diff --git a/dao.ml b/dao.ml index 2e94660..27b8bda 100644 --- a/dao.ml +++ b/dao.ml @@ -113,7 +113,7 @@ let watch_clients fn = end >>= fun items -> Xen_os.Xs.make () >>= fun xs -> Lwt_list.map_p (vifs xs) items >>= fun items -> - fn (List.concat items |> VifMap.of_list); + fn (List.concat items |> VifMap.of_list) >>= fun () -> (* Wait for further updates *) Lwt.fail Xs_protocol.Eagain ) diff --git a/dao.mli b/dao.mli index bff4cbf..c278d16 100644 --- a/dao.mli +++ b/dao.mli @@ -15,7 +15,7 @@ module VifMap : sig val find : key -> 'a t -> 'a option end -val watch_clients : (Ipaddr.V4.t VifMap.t -> unit) -> 'a Lwt.t +val watch_clients : (Ipaddr.V4.t VifMap.t -> unit Lwt.t) -> 'a Lwt.t (** [watch_clients fn] calls [fn clients] with the list of backend clients in XenStore, and again each time XenStore updates. *) diff --git a/dispatcher.ml b/dispatcher.ml index 3768863..9ffcc5b 100644 --- a/dispatcher.ml +++ b/dispatcher.ml @@ -17,8 +17,6 @@ struct module I = Static_ipv4.Make (R) (Clock) (UplinkEth) (Arp) module U = Udp.Make (I) (R) - let clients : Cleanup.t Dao.VifMap.t ref = ref Dao.VifMap.empty - class client_iface eth ~domid ~gateway_ip ~client_ip client_mac : client_link = let log_header = Fmt.str "dom%d:%a" domid Ipaddr.V4.pp client_ip in @@ -344,11 +342,12 @@ struct (** Connect to a new client's interface and listen for incoming frames and firewall rule changes. *) let add_vif get_ts { Dao.ClientVif.domid; device_id } dns_client dns_servers - ~client_ip ~router ~cleanup_tasks qubesDB = - Netback.make ~domid ~device_id >>= fun backend -> + ~client_ip ~router ~cleanup_tasks qubesDB () = + let open Lwt.Syntax in + let* backend = Netback.make ~domid ~device_id in Log.info (fun f -> f "Client %d (IP: %s) ready" domid (Ipaddr.V4.to_string client_ip)); - ClientEth.connect backend >>= fun eth -> + let* eth = ClientEth.connect backend in let client_mac = Netback.frontend_mac backend in let client_eth = router.clients in let gateway_ip = Client_eth.client_gw client_eth in @@ -404,46 +403,54 @@ struct (function Lwt.Canceled -> Lwt.return_unit | e -> Lwt.fail e) in Cleanup.on_cleanup cleanup_tasks (fun () -> Lwt.cancel listener); - Lwt.pick [ qubesdb_updater; listener ] + (* XXX(dinosaure): [qubes_updater] and [listener] can be forgotten, our [cleanup_task] + will cancel them if the client is disconnected. *) + Lwt.async (fun () -> Lwt.pick [ qubesdb_updater; listener ]); + Lwt.return_unit (** A new client VM has been found in XenStore. Find its interface and connect to it. *) let add_client get_ts dns_client dns_servers ~router vif client_ip qubesDB = + let open Lwt.Syntax in let cleanup_tasks = Cleanup.create () in Log.info (fun f -> f "add client vif %a with IP %a" Dao.ClientVif.pp vif Ipaddr.V4.pp client_ip); - Lwt.async (fun () -> - Lwt.catch - (fun () -> - add_vif get_ts vif dns_client dns_servers ~client_ip ~router - ~cleanup_tasks qubesDB) - (fun ex -> - Log.warn (fun f -> - f "Error with client %a: %s" Dao.ClientVif.pp vif - (Printexc.to_string ex)); - Lwt.return_unit)); - cleanup_tasks + let* () = + Lwt.catch (add_vif get_ts vif dns_client dns_servers ~client_ip ~router + ~cleanup_tasks qubesDB) + @@ fun exn -> + Log.warn (fun f -> + f "Error with client %a: %s" Dao.ClientVif.pp vif + (Printexc.to_string exn)); + Lwt.return_unit + in + Lwt.return cleanup_tasks (** Watch XenStore for notifications of new clients. *) let wait_clients get_ts dns_client dns_servers qubesDB router = - Dao.watch_clients (fun new_set -> - (* Check for removed clients *) - !clients - |> Dao.VifMap.iter (fun key cleanup -> - if not (Dao.VifMap.mem key new_set) then ( - clients := !clients |> Dao.VifMap.remove key; - Log.info (fun f -> f "client %a has gone" Dao.ClientVif.pp key); - Cleanup.cleanup cleanup)); - (* Check for added clients *) - new_set - |> Dao.VifMap.iter (fun key ip_addr -> - if not (Dao.VifMap.mem key !clients) then ( - let cleanup = - add_client get_ts dns_client dns_servers ~router key ip_addr - qubesDB - in - Log.debug (fun f -> f "client %a arrived" Dao.ClientVif.pp key); - clients := !clients |> Dao.VifMap.add key cleanup))) + let open Lwt.Syntax in + let clients : Cleanup.t Dao.VifMap.t ref = ref Dao.VifMap.empty in + Dao.watch_clients @@ fun new_set -> + (* Check for removed clients *) + let clean_up_clients key cleanup = + if not (Dao.VifMap.mem key new_set) then begin + clients := !clients |> Dao.VifMap.remove key; + Log.info (fun f -> f "client %a has gone" Dao.ClientVif.pp key); + Cleanup.cleanup cleanup + end + in + Dao.VifMap.iter clean_up_clients !clients; + (* Check for added clients *) + let rec go seq = match Seq.uncons seq with + | None -> Lwt.return_unit + | Some ((key, ipaddr), seq) when not (Dao.VifMap.mem key !clients) -> + let* cleanup = add_client get_ts dns_client dns_servers ~router key ipaddr qubesDB in + Log.debug (fun f -> f "client %a arrived" Dao.ClientVif.pp key); + clients := Dao.VifMap.add key cleanup !clients; + go seq + | Some (_, seq) -> go seq + in + go (Dao.VifMap.to_seq new_set) let send_dns_client_query t ~src_port ~dst ~dst_port buf = match t.uplink with From 9156d580df8487d8a18a679797a8ee5850828c53 Mon Sep 17 00:00:00 2001 From: Hannes Mehnert Date: Tue, 15 Oct 2024 21:37:50 +0200 Subject: [PATCH 08/10] cleanup whitespace --- dispatcher.ml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dispatcher.ml b/dispatcher.ml index 9ffcc5b..9dd374e 100644 --- a/dispatcher.ml +++ b/dispatcher.ml @@ -447,7 +447,7 @@ struct let* cleanup = add_client get_ts dns_client dns_servers ~router key ipaddr qubesDB in Log.debug (fun f -> f "client %a arrived" Dao.ClientVif.pp key); clients := Dao.VifMap.add key cleanup !clients; - go seq + go seq | Some (_, seq) -> go seq in go (Dao.VifMap.to_seq new_set) From ceb712ec60c621453a042045d57fa72ed9217b98 Mon Sep 17 00:00:00 2001 From: Hannes Mehnert Date: Tue, 15 Oct 2024 21:39:35 +0200 Subject: [PATCH 09/10] minor: reword XXX to NOTE --- dao.ml | 2 +- dispatcher.ml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dao.ml b/dao.ml index 27b8bda..9344c1f 100644 --- a/dao.ml +++ b/dao.ml @@ -81,7 +81,7 @@ let vifs client domid = let get_client_ip () = let* str = Xen_os.Xs.read handle (Fmt.str "%s/%d/ip" path device_id) in let client_ip = List.hd (String.split_on_char ' ' str) in - (* XXX(dinosaure): it's safe to use [List.hd] here, + (* NOTE(dinosaure): it's safe to use [List.hd] here, [String.split_on_char] can not return an empty list. *) Lwt.return_some (vif, Ipaddr.V4.of_string_exn client_ip) in diff --git a/dispatcher.ml b/dispatcher.ml index 9dd374e..60927f6 100644 --- a/dispatcher.ml +++ b/dispatcher.ml @@ -403,7 +403,7 @@ struct (function Lwt.Canceled -> Lwt.return_unit | e -> Lwt.fail e) in Cleanup.on_cleanup cleanup_tasks (fun () -> Lwt.cancel listener); - (* XXX(dinosaure): [qubes_updater] and [listener] can be forgotten, our [cleanup_task] + (* NOTE(dinosaure): [qubes_updater] and [listener] can be forgotten, our [cleanup_task] will cancel them if the client is disconnected. *) Lwt.async (fun () -> Lwt.pick [ qubesdb_updater; listener ]); Lwt.return_unit From 1406855a9e901aa4a71a5ba0a333e5368a33a970 Mon Sep 17 00:00:00 2001 From: Hannes Mehnert Date: Tue, 15 Oct 2024 21:49:57 +0200 Subject: [PATCH 10/10] update checksum --- build-with.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-with.sh b/build-with.sh index c54d999..5252f23 100755 --- a/build-with.sh +++ b/build-with.sh @@ -20,5 +20,5 @@ $builder build -t qubes-mirage-firewall . echo Building Firewall... $builder run --rm -i -v `pwd`:/tmp/orb-build:Z qubes-mirage-firewall echo "SHA2 of build: $(sha256sum ./dist/qubes-firewall.xen)" -echo "SHA2 last known: 4b1f743bf4540bc8a9366cf8f23a78316e4f2d477af77962e50618753c4adf10" +echo "SHA2 last known: 2392386d9056b17a648f26b0c5d1c72b93f8a197964c670b2b45e71707727317" echo "(hashes should match for released versions)"