This commit is contained in:
parent
aa2ff7b2f0
commit
2f3113f55d
11742 changed files with 1223940 additions and 0 deletions
297
unikernel/duniverse/ocaml-tls/eio/tests/fuzz.ml
Normal file
297
unikernel/duniverse/ocaml-tls/eio/tests/fuzz.ml
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
(* Fuzz testing for tls-eio.
|
||||
|
||||
This code picks two random strings, one for the client to send and one for
|
||||
the server. It then starts a send and receive fiber for each end.
|
||||
|
||||
A dispatcher fiber then sends commands to these worker fibers
|
||||
(see [action] for the possible actions).
|
||||
|
||||
This is intended to check for bugs in the Eio wrapper (rather than in Tls itself).
|
||||
At the moment, it's just checking that tls-eio works when used correctly.
|
||||
Each endpoint overlaps reads with writes (but not reads with other reads or
|
||||
writes with other writes).
|
||||
|
||||
Some possible future improvements:
|
||||
|
||||
- It currently only checks the basic read/write/close operations.
|
||||
It should be extended to check [reneg], etc too.
|
||||
|
||||
- Currently, cancelling a read operation marks the Tls flow as broken.
|
||||
We should allow resuming after a cancelled read, and test that here.
|
||||
|
||||
- We should try injecting faults and make sure they're handled sensibly.
|
||||
|
||||
- It would be good to get coverage reports for these tests.
|
||||
However, this requires changes to crowbar:
|
||||
https://github.com/stedolan/crowbar/issues/4#issuecomment-1310277551
|
||||
(a patched version reported 54% coverage of Tls_eio.ml) *)
|
||||
|
||||
open Eio.Std
|
||||
|
||||
let src = Logs.Src.create "fuzz" ~doc:"Fuzz tests"
|
||||
module Log = (val Logs.src_log src : Logs.LOG)
|
||||
|
||||
module W = Eio.Buf_write
|
||||
|
||||
type transmit_amount = Mock_socket.transmit_amount
|
||||
|
||||
type op =
|
||||
| Send of int (* The application sends some bytes to Tls *)
|
||||
| Transmit of transmit_amount (* The network sends some types to the peer *)
|
||||
| Recv (* The application tries to read some data *)
|
||||
| Shutdown_send (* The application shuts down the sending side *)
|
||||
|
||||
let label name gen =
|
||||
Crowbar.with_printer Fmt.(const string name) gen
|
||||
|
||||
let op =
|
||||
Crowbar.choose @@ [
|
||||
Crowbar.(map [range 4096]) (fun n -> Send n);
|
||||
Crowbar.(map [range ~min:1 4096]) (fun n -> Transmit (`Bytes n));
|
||||
label "recv" @@ Crowbar.const Recv;
|
||||
label "shutdown-send" @@ Crowbar.const Shutdown_send;
|
||||
]
|
||||
|
||||
type dir = To_client | To_server
|
||||
|
||||
let pp_dir f = function
|
||||
| To_server -> Fmt.string f "client-to-server"
|
||||
| To_client -> Fmt.string f "server-to-client"
|
||||
|
||||
let dir =
|
||||
Crowbar.choose [
|
||||
label "server-to-client" @@ Crowbar.const To_client;
|
||||
label "client-to-server" @@ Crowbar.const To_server;
|
||||
]
|
||||
|
||||
(* A test case is a random sequence of [action]s, followed by party shutting
|
||||
down the sending side of the connection (if it hasn't already done so) and
|
||||
the network draining any queued traffic.
|
||||
|
||||
Once all fibers have finished, we check that what was sent matches the data
|
||||
that has been received. *)
|
||||
|
||||
let action =
|
||||
Crowbar.option (Crowbar.pair dir op) (* None means yield *)
|
||||
|
||||
(* A [Path] is one direction (either server-to-client or client-to-server).
|
||||
The two paths can be tested mostly independently (except for shutdown at the moment). *)
|
||||
module Path : sig
|
||||
type t
|
||||
|
||||
val create :
|
||||
sender:(Tls_eio.t, exn) result Promise.t ->
|
||||
receiver:(Tls_eio.t, exn) result Promise.t ->
|
||||
transmit:(transmit_amount -> unit) ->
|
||||
dir -> string -> t
|
||||
(** Create a test driver for one direction, from [sender] to [receiver].
|
||||
[transmit n] causes [n] bytes to be transferred over the mock network. *)
|
||||
|
||||
val close : t -> unit
|
||||
(** [close t] causes the sender to close the socket for sending.
|
||||
Futher send operations will be ignored. *)
|
||||
|
||||
val run : t -> unit
|
||||
(** Run the send and receive fibers. Returns once the receiver has read EOF. *)
|
||||
|
||||
val enqueue : t -> op -> unit
|
||||
(** Send a command to the send or receive fiber (depending on [op]). *)
|
||||
end = struct
|
||||
type t = {
|
||||
dir : dir;
|
||||
message : string; (* The complete message to be transmitted over this path. *)
|
||||
(* We need to construct [t] before the handshake is done, so these are promises: *)
|
||||
sender : Tls_eio.t Promise.or_exn;
|
||||
receiver : Tls_eio.t Promise.or_exn;
|
||||
mutable sent : int; (* Bytes of [message] sent so far *)
|
||||
mutable recv : int; (* Bytes of [message] received so far *)
|
||||
send_commands : [`Send of int | `Exit] Eio.Stream.t; (* Commands for the sending fiber *)
|
||||
recv_commands : [`Recv | `Drain] Eio.Stream.t; (* Commands for the receiving fiber *)
|
||||
transmit : transmit_amount -> unit;
|
||||
}
|
||||
|
||||
let pp_dir f t =
|
||||
pp_dir f t.dir
|
||||
|
||||
let create ~sender ~receiver ~transmit dir message =
|
||||
let send_commands = Eio.Stream.create max_int in
|
||||
let recv_commands = Eio.Stream.create max_int in
|
||||
{ dir; message; sender; receiver; sent = 0; recv = 0;
|
||||
send_commands; recv_commands; transmit }
|
||||
|
||||
let shutdown t =
|
||||
Eio.Stream.add t.send_commands `Exit
|
||||
|
||||
let close t =
|
||||
shutdown t; (* Sender stops sending *)
|
||||
t.transmit `Drain; (* Network transmits everything *)
|
||||
Eio.Stream.add t.recv_commands `Drain (* Receiver reads everything *)
|
||||
|
||||
let run_send_thread t =
|
||||
let sender = Promise.await_exn t.sender in
|
||||
Logs.info (fun f -> f "%a: sender ready" pp_dir t);
|
||||
let rec aux () =
|
||||
match Eio.Stream.take t.send_commands with
|
||||
| `Exit ->
|
||||
Log.info (fun f -> f "%a: shutdown send (Tls level)" pp_dir t);
|
||||
Eio.Flow.shutdown sender `Send
|
||||
| `Send len ->
|
||||
let available = String.length t.message - t.sent in
|
||||
let len = min len available in
|
||||
if len > 0 then (
|
||||
let msg = Cstruct.of_string ~off:t.sent ~len t.message in
|
||||
t.sent <- t.sent + len;
|
||||
Log.info (fun f -> f "%a: sending %S" pp_dir t (Cstruct.to_string msg));
|
||||
Eio.Flow.write sender [msg];
|
||||
);
|
||||
aux ()
|
||||
in
|
||||
aux()
|
||||
|
||||
let run_recv_thread t =
|
||||
let recv = Promise.await_exn t.receiver in
|
||||
Logs.info (fun f -> f "%a: receiver ready" pp_dir t);
|
||||
try
|
||||
let drain = ref false in
|
||||
while true do
|
||||
if !drain = false then (
|
||||
begin match Eio.Stream.take t.recv_commands with
|
||||
| `Recv -> ()
|
||||
| `Drain -> drain := true
|
||||
end
|
||||
);
|
||||
let buf = Cstruct.create 4096 in
|
||||
let got = Eio.Flow.single_read recv buf in
|
||||
let received = Cstruct.to_string buf ~len:got in
|
||||
Log.info (fun f -> f "%a: received %S" pp_dir t received);
|
||||
let expected = String.sub t.message t.recv got in
|
||||
if received <> expected then
|
||||
Fmt.failwith "%a: excepted %S but got %S!" pp_dir t expected received;
|
||||
t.recv <- t.recv + got
|
||||
done
|
||||
with End_of_file ->
|
||||
if t.recv <> t.sent then (
|
||||
Fmt.failwith "%a: Sender sent %d bytes, but receiver got EOF after reading only %d"
|
||||
pp_dir t
|
||||
t.sent
|
||||
t.recv
|
||||
);
|
||||
Log.info (fun f -> f "%a: recv thread done (got EOF)" pp_dir t)
|
||||
|
||||
let run t =
|
||||
Fiber.both
|
||||
(fun () -> run_send_thread t)
|
||||
(fun () -> run_recv_thread t)
|
||||
|
||||
let pp_amount f = function
|
||||
| `Bytes n -> Fmt.pf f "%d bytes" n
|
||||
| `Drain -> Fmt.string f "all bytes"
|
||||
|
||||
let enqueue t = function
|
||||
| Send i->
|
||||
Log.info (fun f -> f "%a: enqueue send %d bytes of plaintext" pp_dir t i);
|
||||
Eio.Stream.add t.send_commands @@ `Send i;
|
||||
| Recv ->
|
||||
Log.info (fun f -> f "%a: enqueue read from Tls" pp_dir t);
|
||||
Eio.Stream.add t.recv_commands @@ `Recv;
|
||||
| Transmit i ->
|
||||
Log.info (fun f -> f "%a: enqueue transmit %a over network" pp_dir t pp_amount i);
|
||||
t.transmit i
|
||||
| Shutdown_send ->
|
||||
Log.info (fun f -> f "%a: enqueue shutdown send" pp_dir t);
|
||||
shutdown t
|
||||
end
|
||||
|
||||
module Config : sig
|
||||
val client : Tls.Config.client
|
||||
val server : Tls.Config.server
|
||||
end = struct
|
||||
let null_auth ?ip:_ ~host:_ _ = Ok None
|
||||
|
||||
let client =
|
||||
Result.get_ok (Tls.Config.client ~authenticator:null_auth ())
|
||||
|
||||
let read_file path =
|
||||
let ch = open_in_bin path in
|
||||
let len = in_channel_length ch in
|
||||
let data = really_input_string ch len in
|
||||
close_in ch;
|
||||
data
|
||||
|
||||
let server =
|
||||
let certs = Result.get_ok (X509.Certificate.decode_pem_multiple (read_file "server.pem")) in
|
||||
let pk = Result.get_ok (X509.Private_key.decode_pem (read_file "server.key")) in
|
||||
let certificates = `Single (certs, pk) in
|
||||
Result.get_ok Tls.Config.(server ~version:(`TLS_1_0, `TLS_1_3) ~certificates ~ciphers:Ciphers.supported ())
|
||||
end
|
||||
|
||||
let dispatch_commands ~to_server ~to_client actions =
|
||||
let rec aux = function
|
||||
| [] ->
|
||||
Log.info (fun f -> f "dispatch_commands: done");
|
||||
Path.close to_client;
|
||||
Path.close to_server
|
||||
| None :: xs ->
|
||||
Fiber.yield (); aux xs
|
||||
| Some (dir, op) :: xs ->
|
||||
let path =
|
||||
match dir with
|
||||
| To_server-> to_server
|
||||
| To_client -> to_client
|
||||
in
|
||||
Path.enqueue path op;
|
||||
aux xs
|
||||
in
|
||||
aux actions
|
||||
|
||||
(* In some runs we automatically perform these actions first, which allows the handshake to complete.
|
||||
This lets the fuzz tester get to the interesting cases more quickly. *)
|
||||
let quickstart_actions = [
|
||||
Some (To_server, Transmit (`Bytes 4096));
|
||||
None; (* Client sends handshake *)
|
||||
None; (* Server reads handshake *)
|
||||
Some (To_client, Transmit (`Bytes 4096));
|
||||
None; (* Server replies to handshake *)
|
||||
None; (* Client reads reply *)
|
||||
Some (To_server, Transmit (`Bytes 4096));
|
||||
None; (* Client sends final part *)
|
||||
None; (* Server receives it *)
|
||||
Some (To_client, Recv);
|
||||
Some (To_server, Recv);
|
||||
]
|
||||
|
||||
let main client_message server_message quickstart actions =
|
||||
let actions =
|
||||
if quickstart then quickstart_actions @ actions
|
||||
else actions
|
||||
in
|
||||
Eio_mock.Backend.run @@ fun () ->
|
||||
Switch.run @@ fun sw ->
|
||||
let insecure_test_rng = Mirage_crypto_rng.create (module Test_rng) in
|
||||
Mirage_crypto_rng.set_default_generator insecure_test_rng;
|
||||
let client_socket, server_socket = Mock_socket.create_pair () in
|
||||
let server_flow = Fiber.fork_promise ~sw (fun () -> Tls_eio.server_of_flow Config.server server_socket) in
|
||||
let client_flow = Fiber.fork_promise ~sw (fun () -> Tls_eio.client_of_flow Config.client client_socket) in
|
||||
let to_server =
|
||||
Path.create
|
||||
~sender:client_flow
|
||||
~receiver:server_flow
|
||||
~transmit:(Mock_socket.transmit client_socket)
|
||||
To_server client_message in
|
||||
let to_client =
|
||||
Path.create
|
||||
~sender:server_flow
|
||||
~receiver:client_flow
|
||||
~transmit:(Mock_socket.transmit server_socket)
|
||||
To_client server_message
|
||||
in
|
||||
Fiber.all [
|
||||
(fun () -> dispatch_commands actions ~to_server ~to_client);
|
||||
(fun () -> Path.run to_server);
|
||||
(fun () -> Path.run to_client);
|
||||
]
|
||||
|
||||
let () =
|
||||
Logs.set_level (Some Warning);
|
||||
Logs.set_reporter (Logs_fmt.reporter ());
|
||||
Crowbar.(add_test ~name:"random ops" [bytes; bytes; bool; list action] main)
|
||||
Loading…
Add table
Add a link
Reference in a new issue