Library
Module
Module type
Parameter
Class
Class type
Mimic is a very small library that offers a re-implementation of virtual methods for modules. In a large project such as Git or Irmin, keeping in mind the system abstraction required to be compatible with MirageOS, one question transcends all levels:
> How to abstract the network?
In the specific context of Unix/<unistd.h>
, several functions exist to communicate across the network. In particular the idea of a socket. For most projects, the socket seems to be the common denominator for all transmissions.
In the case of Git, the socket can represent a simple TCP/IP connection or a transmission through SSH (using a pipe). For HTTP with TLS, the principle remains the same as long as OpenSSL proposes an equivalent of the socket through a derivation of the TCP/IP socket.
As a proof, Lwt_ssl
proposes this same derivation:
Lwt_ssl.embed_socket : Lwt_unix.file_descr -> Ssl.context -> Lwt_ssl.socket
In any case, it seems that the socket principle itself is the common denominator to protocols like Git, HTTP or SMTP.
It happens that MirageOS offers an interface that describes this socket:
sig
type error
type write_error = private [> `Closed ]
val pp_error : Format.formatter -> error -> unit
val pp_write_error : Format.formatter -> write_error -> unit
type flow
val read : flow -> (Cstruct.t or_eof, error) result Lwt.t
val write : flow -> Cstruct.t -> (unit, error) result Lwt.t
val writev : flow -> Cstruct.t list -> (unit, error) result Lwt.t
val close : flow -> unit Lwt.t
end
NOTE: read
is a method that diverges from the read
/recv
we're used to seeing with <unistd.h>
where the latter requests a buffer to write to. Historically, the idea of Mirage_flow.S.read
gives the ability to implement a zero-copy stack. Indeed, the Cstruct.t
that is returned could directly be the memory page having the TCP/IP packet. Thus, between the TCP/IP driver (the implementation) and the client application, there should be no allocation. However:
With this interface, it can be possible to abstract the socket for protocols like HTTP, Git/Smart or SMTP such as:
module SMTP = Make_SMTP (Tcpip_stack_direct.TCP : Mirage_flow.S)
module HTTP = Make_HTTP (Tcpip_stack_direct.TCP : Mirage_flow.S)
module Smart = Make_Smart (Tcpip_stack_direct.TCP : Mirage_flow.S)
It turns out that ocaml-tls
offers a derivation from a given socket described through Mirage_flow.S
to a "new" socket with TLS:
Tls_mirage.Make : functor (_ : Mirage_flow.S) -> Mirage_flow.S
Therefore, it is possible to upgrade our protocols statically with a TLS layer quite easily:
module TLS = Tls_mirage.Make (Tcpip_stack_direct.TCP)
module SSMTP = Make_SMTP (TLS)
module HTTPS = Make_HTTP (TLS)
The problem with this kind of abstraction is the eminently static aspect of this code. Indeed, the choice between SMTP and SSMTP (HTTP or HTTPS) cannot be made when choosing statically these modules.
This implies that if the type of transmission depends on a value such as an Uri.t
(and its scheme), we need to have access to these 2 modules throughout our process.
Especially since SSMTP
or HTTPS
are themselves directed by an arbitrary choice which is to use ocaml-tls
instead of OpenSSL. It may be essential to let the user choose his TLS implementation.
We would therefore need:
module type Make_SMTP =
functor (Socket : Mirage_flow.S) ->
functor (Tls : functor (Socket : Mirage_flow.S) -> Mirage_flow.S) ->
sig ... end
Thus, we ensure:
Make
Make
module Make_HTTP (Socket : _) (Tls : _) = struct
module Tls = Tls (Socket)
let connect uri = match Uri.scheme with
| Some "https" -> Tls.connect ...
| Some "http" -> Socket.connect ...
end
The problem remains in any case the eminently dynamic aspect of the choice of the transmission protocol which requires a static knowledge of what is a socket and what is a socket with TLS. The problem applies as much for Git with SSH.
This static knowledge required of the modules implementing the socket as well as its possible derivation into a TLS socket puts us in a difficult position when we want to keep the abstraction power of the functors to be compatible with MirageOS - in which neither the TCP/IP nor the TLS implementation can be known globally in advance (in other words, their implementations can only be obtained through a functor).
In MirageOS, all this complexity of the functors can be reduced with the help of functoria
which allows to apply the functors cleanly according to the target. For the example, the TCP/IP stack depends on the target at all since with mirage configure -t unix
, we use the host system stack but for mirage configure -t hvt
, we use mirage-tcpip
.
Unfortunately, this implies to "keep" this level of abstraction for all libraries depending on our SMTP/HTTP/Smart implementation if they want to keep the compatibility with MirageOS.
A "shift" on the functors then occurs systematically which leads to an exponential progression of the number of functors as one advances from layer to layer.
For example, Irmin with Git will have to integrate at the same time:
It is only through all of these functors that we can:
After this "brief" introduction, it is now time to talk about the solution! But it seems clear that if we wanted to essentialize the problem, it would simply be to say:
> how to get a protocol implementation dynamically and without functors?
In the previous explanations we mentioned Mirage_flow.S
. It turns out that it is canonical to any transmission protocol. It allows to describe the TCP/IP protocol, the TCP/IP protocol with TLS or the SSH protocol because in these 3 cases, we only want to:
The abstraction does not work however when it comes to instantiating the socket. Indeed, a TCP/IP transmission only requires an IP address and a port. However, SSH requires much more such as an RSA key.
Conduit 2.0 assumes that these instantiation methods must be known statically. An ADT describes these methods and if it is not exhaustive, it corresponds to the usual cases such as HTTPS or SSMTP.
However, we could also say that for protocols like SSMTP or HTTPS (or SMTP or HTTP), these instantiation methods are not our concern. Again, we would just like to be able to read and write.
In the end, mimic provides an implementation of Mirage_flow.S
that is directly usable without functors. So we will start implementating a simple protocol, a ping-pong to show how to implement a protocol (like HTTP, SMTP or Smart) with mimic.
open Rresult
open Lwt.Infix
let ( >>? ) = Lwt_result.bind
let blit src src_off dst dst_off len =
Bigstringaf.blit_to_bytes src ~src_off dst ~dst_off ~len
let line_of_queue queue =
let exists ~p queue =
let pos = ref 0 and res = ref (-1) in
Ke.Rke.iter (fun chr -> if p chr && !res = -1 then res := !pos
; incr pos) queue ;
if !res = -1 then None else Some !res in
match exists ~p:((=) '\n') queue with
| None -> None
| Some 0 -> Ke.Rke.N.shift_exn queue 1 ; Some ""
| Some pos ->
let tmp = Bytes.create pos in
Ke.Rke.N.keep_exn queue ~blit ~length:Bytes.length ~off:0 ~len:pos tmp ;
Ke.Rke.N.shift_exn queue (pos + 1) ;
match Bytes.get tmp (pos - 1) with
| '\r' -> Some (Bytes.sub_string tmp 0 (pos - 1))
| _ -> Some (Bytes.unsafe_to_string tmp)
let blit src src_off dst dst_off len =
let src = Cstruct.to_bigarray src in
Bigstringaf.blit src ~src_off dst ~dst_off ~len
let rec getline flow queue = match line_of_queue queue with
| Some line -> Lwt.return_ok (`Line line)
| None ->
Mimic.read flow >>= function
| Ok `Eof -> Lwt.return_ok `Close
| Ok (`Data v) ->
Ke.Rke.N.push queue ~blit ~length:Cstruct.length ~off:0 v ;
getline flow queue
| Error err -> Lwt.return_error (R.msgf "%a" Mimic.pp_error err)
let sendline flow fmt =
let send str =
Mimic.write flow (Cstruct.of_string str) >>= function
| Ok _ as v -> Lwt.return v
| Error err -> Lwt.return_error (R.msgf "%a" Mimic.pp_write_error err) in
Fmt.kstr send (fmt ^^ "\r\n")
This code is quite simple. It implements logic that is usually available with a standard library. Of course, Mirage_flow.S
does not give us these functions (but Mirage_channel
does).
These logics are the protocol as we can define it. For example, SMTP or HTTP could be implemented with these functions. As for Smart, it's another matter since it uses another format - or rather, this protocol is not "line-directed".
But what is most important is the possibility to directly implement a protocol without using a functor to abstract the implementation of the transmission. In this sense, mimic could very well be TCP/IP than TLS or SSH. At this stage, we don't know and that's the point!
You can compile the code with:
$ ocamlfind opt -linkpkg -package rresult,lwt,mimic,bigstringaf,cstruct,ke \
main.ml
Once again, we can denote the dependencies necessary for compilation. There is no question of unix
. At the beginning of this explanation, we talked about <unistd.h>
as the common denominator to get our socket. Here we are saying that our socket is mimic. Of course, mimic is compatible with MirageOS.
So let's start implementing the client as it should be.
let client ~ctx ic =
let rec go flow queue = match input_line ic with
| line ->
if ic != stdin then Fmt.pr "> %s\n%!" line ;
sendline flow "%s" line >>? fun () ->
( getline flow queue >>? function
| `Close -> Lwt.return_ok ()
| `Line v ->
Fmt.pr "<- %s\n%!" v ;
if ic == stdin then Fmt.pr "> %!" ;
go flow queue )
| exception End_of_file -> Lwt.return_ok () in
Mimic.resolve ctx >>? fun flow ->
let queue = Ke.Rke.create ~capacity:0x1000 Bigarray.char in
if ic == stdin then Fmt.pr "> %!" ;
go flow queue >>= fun res ->
Mimic.close flow >>= fun () -> Lwt.return res
In this small piece of code, we see the appearance of a function that is not part of Mirage_flow.S
but strictly speaking of mimic. It's Mimic.resolve
.
It was said earlier that the instantiation of a socket is not the privilege of the protocol itself. Indeed, once again, as far as our ping-pong protocol is concerned, our "line-directed" protocol (just like SMTP or HTTP again) does not care how to initialize a transmission. It just wants to be able to read and write.
Thus, Mimic.resolve
seems a bit magical but the instantiation of a socket ultimately depends on a single value, the ctx
. The context is a representation of what is allowed to do according to the end user. It contains elements that allow the famous dynamic "dispatch" to instantiate a socket.
In other words, it is through the context that we determine the type of transmission: if it is a TCP/IP or TLS transmission for example.
We will then see how to define this context and how it works to choose this or that type of transmission. The important thing to keep in mind is that we have just done:
The logic of this code is very simple, it passes what it has from an in_channel
to the server and that's it! It is important to understand that what follows must be external to the implementation of the protocol itself, because we will start to explain to mimic the instantiation of the transmission techniques.
As a concrete example, ocaml-git
is separated in 3 where git
implements the Smart protocol, git-unix
registers the Unix specific transmission types and git-mirage
does the same with MirageOS compatible implementations (mirage-tcpip
, tls-mirage
or awa-ssh
).
This part will directly depend on the said transmission protocols like TCP/IP or TLS. These choices are therefore outside the implementation of the ping-pong protocol itself.
Mimic offers a way to "fill" the context with values. These values are needed to instantiate one of your transmission protocols. As we said, for TCP/IP, the instantiation of a socket requires to get an IP address and a port.
So, if we "fill" our context with these values, mimic can initialize a TCP/IP connection. More generally, 2 steps are necessary for mimic to establish a transmission:
The first step is quite unusual. It consists in "registering" a transmission protocol with mimic. This is a prerequisite for extending the protocols available through mimic - and of course, initially, mimic does not know any protocols (again, to be compatible with MirageOS).
It is accepted, as we said from the beginning, that a transmission protocol can described with Mirage_flow.S
. As for mirage-tcpip
, ocaml-tls
or awa-ssh
, all three implementations respect the Mirage_flow.S
interface.
And this is what mimic expects, a protocol that respects Mirage_flow.S
. However, mimic expects an extension to this interface. Indeed, beyond being able to relay the read
and write
of your implementations to the implementation of your ping-pong protocol, mimic also depends on an "instantiation" method. In other words, mimic requires a module respecting Mirage_flow.S
and a connect
function: Mimic.Mirage_protocol.S
.
Let's take mirage-tcpip
as an example. We need to tweak its implementation a bit in order to register it with mimic.
module TCP = struct
include Tcpip_stack_socket.V4V6.TCP
let pp_write_error ppf = function
| #write_error as err -> pp_write_error ppf err
| `Error err -> pp_error ppf err
type endpoint = t * Ipaddr.t * int
type nonrec write_error = [ write_error | `Error of error ]
let write flow cs = write flow cs >>= function
| Ok _ as v -> Lwt.return v
| Error err -> Lwt.return_error (err :> write_error)
let writev flow css = writev flow css >>= function
| Ok _ as v -> Lwt.return v
| Error err -> Lwt.return_error (err :> write_error)
let connect (stack, ipaddr, port) =
create_connection stack (ipaddr, port)
>|= R.reword_error (fun err -> `Error err)
end
let tcp_edn, tcp_protocol = Mimic.register ~name:"tcp" (module TCP)
We have just registered the TCP/IP transmission protocol and mimic has returned 2 values:
TCP
moduleTCP
implementationNOTE: it may be difficult to understand why we need to tweak mirage-tcpip
. In fact, if mimic really wants to be a means of abstracting transmission protocols, one must admit the idea that instantiating a protocol may require writing something. This is the case for TLS which performs a handshake with the server at instantiation. Thus, we must allow connect
to return a writing error.
tcp_edn
is a value that represents what is required from our connect
. Its type depends explicitely on the way our implementation instantiates our socket. In other words, in our example, its type is:
val tcp_edn : (TCP.t * Ipaddr.t * int) Mimic.value
This witness is useful to "fill in" a context that we could then pass to our client. The idea is that if a value of type tcp_edn
exists in the ctx
context used by Mimic.resolve
, mimic is able to instantiate a TCP/IP transmission and use your TCP
module instead of Mimic.{read,write,close}
.
So let's try to use our code. In a shell, it makes us launch a server with nc -l 8080
. Then we need to run out client.
let ctx00 stack ipaddr port =
Mimic.empty
|> Mimic.add tcp_edn (stack, ipaddr, port)
let run00 uri ic = match Uri.host uri, Uri.port uri with
| None, None
| Some _, None
| None, Some _ -> Fmt.failwith "Invalid uri: %a" Uri.pp uri
| Some host, Some port -> match Ipaddr.of_string host with
| Ok ipaddr ->
let open Tcpip_stack_socket.V4V6 in
TCP.connect ~ipv4_only:false ~ipv6_only:false Ipaddr.V4.Prefix.global
None >>= fun tcp ->
let ctx = ctx00 tcp ipaddr port in
client ~ctx ic
| Error _ -> Fmt.failwith "Invalid IP address: %s" host
let _0 () = match Sys.argv with
| [| _; uri; |] ->
Lwt_main.run (run00 (Uri.of_string uri) stdin)
|> R.reword_error (R.msgf "%a" Mimic.pp_error)
|> R.failwith_error_msg
| [| _; uri; filename; |] when Sys.file_exists filename ->
let ic = open_in filename in
let rs = Lwt_main.run (run00 (Uri.of_string uri) ic) in
close_in ic ;
R.failwith_error_msg (R.reword_error (R.msgf "%a" Mimic.pp_error) rs)
| _ -> Fmt.epr "%s <uri> [filename]\n%!" Sys.argv.(0)
The code is compilable with
$ ocamlfind opt -thread -package rresult,lwt,mimic,bigstringaf,cstruct,ke, \
tcpip.stack-socket,uri main.ml
Then we just need to execute our code such that $
is our client and #
is our server:
# nc -l 8080
$ ./a.out tcp://127.0.0.1:8080/
$> ping
#ping
#pong
$<- pong
We have several limitations here:
TCP.t * Ipaddr.t * int
However, we have something that works without having changed any of the code of our ping-pong protocol. Let's take the time to explain once again what just happened. Giving the client a context containing the information required to instantiate a TCP/IP socket causes mimic to be able to execute TCP.connect
with these arguments. Let's not forget that it is because we took care to use tcp_edn
that mimic
is able to do this.
Since the connect
works and returns a TCP.flow
(not an error), mimic can "hide" this value under the Mimic.flow
type used in our client code.
Finally, Mimic.read
and Mimic.write
, since they handle a Mimic.flow
, they have the ability to "introspect" the hidden TCP.flow
and call to TCP.read
and TCP.write
respectively. This possibility comes from the fact that we have "registered" our TCP
protocol with mimic (with Mimic.register
).
Now we can try to solve our limitations. Indeed, mimic provides an API to:
For the exemple, we will try to manage domain names rather than IP addresses. Thanks to this, we will be able to write "tcp://localhost/"
. Also, we will set a default value for the port.
Again, we need to remember to be compatible with MirageOS. It may be "simple" to manage the domain name "localhost", but behind this resolution, the process is more complexe than one might imagine. It can be similar to a DNS query on the network. Of course, this kind of mechanism does not exist - at least not without wish - with MirageOS. In our case, since we already depend on unix
, we can directly use Unix.gethostbyname
.
In the context of MirageOS, like ocaml-git
(see git-mirage
), we will use Functoria to add or not the DNS resolution.
let port : int Mimic.value = Mimic.make ~name:"port"
let ipaddr : Ipaddr.t Mimic.value = Mimic.make ~name:"ipaddr"
let domain_name : [ `host ] Domain_name.t Mimic.value =
Mimic.make ~name:"domain-name"
let stack : Tcpip_stack_socket.V4V6.TCP.t Mimic.value =
Mimic.make ~name:"stack"
let ctx01 =
let open Mimic in
let k0 v = match Unix.gethostbyname (Domain_name.to_string v) with
| { Unix.h_addr_list; _ } ->
if Array.length h_addr_list > 0
then Lwt.return_some (Ipaddr_unix.of_inet_addr h_addr_list.(0))
else Lwt.return_none
| exception _ -> Lwt.return_none in
let k1 stack ipaddr port = Lwt.return_some (stack, ipaddr, port) in
Mimic.empty
|> Mimic.fold ipaddr Fun.[ req domain_name ] ~k:k0
|> Mimic.fold tcp_edn Fun.[ req stack; req ipaddr; dft port 8080 ] ~k:k1
We have a new context that does not contain the values required to instantiate a TCP/IP transmission. However, it contains 2 important processes that allow to "resolve" some values into others.
This is the case more concretely with DNS resolution where we go from a domain name to an IP address. If we add a domain name to this context, mimis is smart enough to try to get an IP address using k0
.
Finally, the second process k1
allows to gather some values if they exist (except for the port which has a default value of 8080
) and to produce a value of type tcp_edn
.
Thus, we now that the ability to instantiate a TCP/IP socket by different means and different values:
We can thus make our deconstruction of the Uri.t
a little more complex.
let run01 uri ic =
let ctx = ctx01 in
let ctx = match Uri.port uri with
| Some v -> Mimic.add port v ctx
| None -> ctx in
let ctx = match Uri.host uri with
| None -> ctx
| Some v ->
match Rresult.(Domain_name.(of_string v >>= host)),
Ipaddr.of_string v with
| Ok v, _ -> Mimic.add domain_name v ctx
| _, Ok v -> Mimic.add ipaddr v ctx
| _ -> ctx in
let open Tcpip_stack_socket.V4V6 in
TCP.connect ~ipv4_only:false ~ipv6_only:false Ipaddr.V4.Prefix.global
None >>= fun tcp ->
let ctx = Mimic.add stack tcp ctx in
client ~ctx ic
let _1 () = match Sys.argv with
| [| _; uri; |] ->
Lwt_main.run (run01 (Uri.of_string uri) stdin)
|> R.reword_error (R.msgf "%a" Mimic.pp_error)
|> R.failwith_error_msg
| [| _; uri; filename; |] when Sys.file_exists filename ->
let ic = open_in filename in
let rs = Lwt_main.run (run01 (Uri.of_string uri) ic) in
close_in ic ;
R.failwith_error_msg (R.reword_error (R.msgf "%a" Mimic.pp_error) rs)
| _ -> Fmt.epr "%s <uri> [filename]\n%!" Sys.argv.(0)
We can say that we finally have a correct "endpoint" mangement using an Uri.t
. But what is important is the ability to choose the endpoint independently of the logic of our ping-pong protocol
This is another important aspect of mimic, it only recognizes the context ctx
which, in the end, is a heterogeneous set of values. These values can come from any canonical representation of your endpoin. In our case, we use an Uri.t
but another representation can be used.
This is the case between paf
(an HTTP/AF abstraction layer compatible with MirageOS) and Git. One requires an Uri.t
as a canonical representation of a target while the other defines its own Smart_git.Endpoint.t
type since the target can be represented by an email address (like git@github.com:mirage/ocaml-git
).
In short, all of this shows us a rather fine control of the "dispatch". mimic just tries to put the pieces together and find a way to create values respecting the prerequisite of your protocols in order to instantiate them.
We will now see how to upgrade all our code to use TLS.
module TLS = struct
include Tls_mirage.Make(Tcpip_stack_socket.V4V6.TCP)
type endpoint =
Tcpip_stack_socket.V4V6.TCP.t
* Tls.Config.client * [ `host ] Domain_name.t option
* Ipaddr.t * int
let connect (stack, tls, host, ipaddr, port) =
let open Tcpip_stack_socket.V4V6 in
TCP.create_connection stack (ipaddr, port)
>|= R.reword_error (fun err -> `Read err)
>>? fun flow ->
client_of_flow tls ?host flow
end
let tls_edn, tls_protocol = Mimic.register ~priority:10 ~name:"tls"
(module TLS)
let authenticator ?ip:_ ~host:_ _ = Ok None
let default = Tls.Config.client ~authenticator ()
let tls : Tls.Config.client Mimic.value = Mimic.make ~name:"tls-config"
let scheme : string Mimic.value = Mimic.make ~name:"scheme"
let ctx02 =
let open Mimic in
let k0 scheme stack tls domain_name ipaddr port = match scheme with
| "tls" -> Lwt.return_some (stack, tls, domain_name, ipaddr, port)
| _ -> Lwt.return_none in
let k1 scheme stack ipaddr port = match scheme with
| "tcp" -> Lwt.return_some (stack, ipaddr, port)
| _ -> Lwt.return_none in
Mimic.empty
|> Mimic.fold tls_edn
Fun.[ req scheme; req stack; dft tls default; opt domain_name
; req ipaddr; dft port 4343 ] ~k:k0
|> Mimic.fold tcp_edn
Fun.[ req scheme; req stack; req ipaddr; dft port 8080 ] ~k:k1
Here, the method remains the same as for TCP
. We create the module and then register it with mimic. We have two new values which allow us to better specify ths "dispatch" according to the scheme.
Finally, we have a new context allowing to instantiate a TLS socket according to some values, some of which have a default value. We can finally complete the deconstruction of our Uri.t
once again to manage all these parameters.
let run02 uri ic =
let ctx = Mimic.merge ctx01 ctx02 in
let ctx = match Uri.scheme uri with
| Some v -> Mimic.add scheme v ctx
| None -> ctx in
let ctx = match Uri.port uri with
| Some v -> Mimic.add port v ctx
| None -> ctx in
let ctx = match Uri.host uri with
| None -> ctx
| Some v ->
match Rresult.(Domain_name.(of_string v >>= host)),
Ipaddr.of_string v with
| Ok v, _ -> Mimic.add domain_name v ctx
| _, Ok v -> Mimic.add ipaddr v ctx
| _ -> ctx in
let open Tcpip_stack_socket.V4V6 in
TCP.connect ~ipv4_only:false ~ipv6_only:false Ipaddr.V4.Prefix.global
None >>= fun tcp ->
let ctx = Mimic.add stack tcp ctx in
client ~ctx ic
let () = Mirage_crypto_rng_unix.initialize ()
let _2 () = match Sys.argv with
| [| _; uri; |] ->
Lwt_main.run (run02 (Uri.of_string uri) stdin)
|> R.reword_error (R.msgf "%a" Mimic.pp_error)
|> R.failwith_error_msg
| [| _; uri; filename |] when Sys.file_exists filename ->
let ic = open_in filename in
let rs = Lwt_main.run (run02 (Uri.of_string uri) ic) in
close_in ic ;
R.failwith_error_msg (R.reword_error (R.msgf "%a" Mimic.pp_error) rs)
| _ -> Fmt.epr "%s <uri> [filename]\n%!" Sys.argv.(0)
These is a lot to say here and to do in order to test this code. First of all we note the use of Mimic.merge
which allows to merge 2 contexts to obtain only one. To avoid code repetition, we will reuse ctx01
which contains our DNS resolver.
Then we add the scheme from the given Uri.t
.
To launch a TLS server, nothing more than:
# openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem \
-days 365 -nodes
# openssl s_server -key key.pem -cert cert.pem -accept 4343
As for our client, we need to compile it with:
$ ocamlfind opt -thread -linkpkg -package \
rresult,lwt,\
mimic,bigstringaf,cstruct,ke,tcpip.stack-socket,uri,tls-mirage,\
mirage-crypto-rng.unix main.ml
$ ./a.out tls://localhost:4343/
> ping
#ping
#pong
<- pong
> ^D
#DONE
And that's it! Again, as an example, the extension from one protocol to another is completely transparent to the protocol ping-pong logic. As you can see, mimic is very minimal but it allows a lot of things. The ability to integrate complex processes into the context allows us to extend what we are able to handle.
Of course, the minimal aspect of mimic is in the spirit of MirageOS. In the end, mimic only allows one thing: re-implementing virtual methods for modules. The discrimination of available implementations in what is comparable to a vtable (in C++) is done for the context.
Finally the functions that are in the context can fail as well. In this case, mimic tries other solutions. This situation explains another parameter used in our exemple for TLS, the priority. This ensures that even if the required information for tcp_edn
exists, mimic will first try to instantiate a TLS transmission (if, again, all information is available).
We can finally apply to implement the server now.
Mimic make the choice to let the user the way the server is made. Indeed, there is a real difference between a client and a server. There is a dynamic part in the choice of the transmission protocol as a client but it is especially not the case for the server where we know exactly how to launch our server.
Indeed, everything that is initialization or the logic of the main loop remains outside mimic. However, mimic intervenes at one point. As a server, it has to manage clients that do both reading and writing. It can be interesting to implement the client management, the "handler" or the "callback" with mimic.
The goal is to implement this logic with mimic and we will explain the way to pass from a TCP/IP or TLS socket to a Mimic.flow
. We call this process: injection.
NOTE: we will redefine TCP
and TLS
to use the TCP/IP stack of the host system directly this time using Lwt_unix.file_descr
. Besides showing another example of how to "register" other protocols, we are required to do this for the simple reason that mirage-tcpip
offers a different server logic/interface. Indeed, for Unix/<unistrd.h>
, we are used to the triptik socket
/accept
/close
. mirage-tcpip
proposes a more "functional" interface with a listen function that registers your callback internally. Finally, mirage-tcpip
implements its own main loop. Of course, all this is required because we cannot switch from an Unix.file_descr
to a mirage-tcpip
socker.
In order to not lose anyone and to have a coherent understanding of what is usually done when implementing a server, we have to reimplement TCP
and TLS
with Unix.file_descr
and use these modules as our transmission protocols.
let handler flow =
let queue = Ke.Rke.create ~capacity:0x1000 Bigarray.char in
let rec go flow queue =
getline flow queue >>? function
| `Close -> Lwt.return_ok ()
| (`Line "ping") -> sendline flow "pong" >>? fun () -> go flow queue
| (`Line "pong") -> sendline flow "ping" >>? fun () -> go flow queue
| (`Line line) -> sendline flow "%s" line >>? fun () -> go flow queue in
go flow queue >>= fun res ->
Mimic.close flow >>= fun () -> Lwt.return res
let handler flow =
handler flow >>= function
| Ok () -> Lwt.return_unit
| Error err ->
Fmt.epr "Got an error: %a.\n%!" Mimic.pp_error err ;
Lwt.return_unit
module TCP' = struct
type flow = Lwt_unix.file_descr
type error = [ `Error of Unix.error * string * string ]
type write_error = [ `Closed | `Error of Unix.error * string * string ]
let pp_error ppf = function
| `Error (err, f, v) ->
Fmt.pf ppf "%s(%s) : %s" f v (Unix.error_message err)
let pp_write_error ppf = function
| #error as err -> pp_error ppf err
| `Closed -> Fmt.pf ppf "Connection closed by peer"
let read fd =
let tmp = Bytes.create 0x1000 in
let process () =
Lwt_unix.read fd tmp 0 (Bytes.length tmp) >>= function
| 0 -> Lwt.return_ok `Eof
| len -> Lwt.return_ok (`Data (Cstruct.of_bytes ~off:0 ~len tmp)) in
Lwt.catch process @@ function
| Unix.Unix_error (e, f, v) -> Lwt.return_error (`Error (e, f, v))
| exn -> Lwt.fail exn
let write fd ({ Cstruct.len; _ } as cs) =
let rec process buf off max =
Lwt_unix.write fd buf off max >>= fun len ->
if max - len = 0 then Lwt.return_ok ()
else process buf (off + len) (max - len) in
let buf = Cstruct.to_bytes cs in
Lwt.catch (fun () -> process buf 0 len) @@ function
| Unix.Unix_error (e, f, v) -> Lwt.return_error (`Error (e, f, v))
| exn -> Lwt.fail exn
let rec writev fd = function
| [] -> Lwt.return_ok ()
| x :: r -> write fd x >>? fun () -> writev fd r
let close fd = Lwt_unix.close fd
type endpoint = Lwt_unix.sockaddr
let connect sockaddr =
let process () =
let domain = Unix.domain_of_sockaddr sockaddr in
let socket = Lwt_unix.socket domain Unix.SOCK_STREAM 0 in
Lwt_unix.connect socket sockaddr >>= fun () ->
Lwt.return_ok socket in
Lwt.catch process @@ function
| Unix.Unix_error (e, f, v) -> Lwt.return_error (`Error (e, f, v))
| exn -> Lwt.fail exn
end
module TLS' = struct
include Tls_mirage.Make(TCP')
type endpoint =
Tls.Config.client * [ `host ] Domain_name.t option
* Unix.sockaddr
let connect (tls, host, sockaddr) =
TCP'.connect sockaddr
>|= R.reword_error (fun err -> `Read err)
>>? fun flow ->
client_of_flow tls ?host flow
end
let _, tcp_protocol = Mimic.register ~name:"tcp" (module TCP')
let _, tls_protocol = Mimic.register ~name:"tls" (module TLS')
module TCPRepr = (val (Mimic.repr tcp_protocol))
module TLSRepr = (val (Mimic.repr tls_protocol))
This time the values we are interested in are the protocols' witnesses. These allow us to create a module exposing the constructor that extends our Mimic.flow
type.
This constructor is obtained with the help of Mimic.repr
. In out example, we obtain modules that continue a type t
but above all, they expose a constructor T
that allows us to inject our socket as Mimic.flow
type.
Thus, we can create a Mimic.flow
value from our Lwt_unix.file_descr
socket by doing:
let flow : Mimic.flow = TCPRepr.T socket in
The same is true for TLS, which has a different type - and thus, a different constructor.
let flow : Mimic.flow = TLSRepr.T socket in
The rest of the code is the application part of what we just did. We can compile the code with:
$ ocamlfind opt -thread -linkpkg -package \
mimic,bigstringaf,cstruct,ke,tcpip.stack-socket,uri,tls-mirage,\
mirage-crypto-rng.unix main.ml
Finally, the server side runs with #
and the client side with $
:
# ./a.out server cert.pem key.pem 4343
# ./a.out 8080
$ ./a.out client tcp://localhost:8080/
$ ./a.out client tsl://localhost:4343/
type ('v, 'flow, 'err) service =
{ accept : 'v -> ('flow, 'err) result Lwt.t
; close : 'v -> unit Lwt.t }
constraint 'err = [> `Closed ]
let serve_when_ready ?stop ~handler { accept; close; } service =
`Initialized
(let switched_off =
let t, u = Lwt.wait () in
Lwt_switch.add_hook stop (fun () ->
Lwt.wakeup_later u (Ok `Stopped) ;
Lwt.return_unit) ;
t in
let rec loop () =
let accept =
accept service >>? fun flow -> Lwt.return_ok (`Flow flow) in
accept >>? function
| `Flow flow ->
Lwt.async (fun () -> handler flow) ;
Lwt.pause () >>= loop in
let stop_result =
Lwt.pick [ switched_off; loop () ] >>= function
| Ok `Stopped -> close service >>= fun () -> Lwt.return_ok ()
| Error _ as err -> close service >>= fun () -> Lwt.return err in
stop_result >>= function Ok () | Error _ -> Lwt.return_unit)
let tcp =
let accept t = Lwt_unix.accept t >>= fun (fd, _) ->
Lwt.return_ok (TCPRepr.T fd) in
let close t = Lwt_unix.close t in
{ accept; close; }
let tls cfg =
let accept t =
Lwt_unix.accept t >>= fun (fd, _) ->
TLS'.server_of_flow cfg fd >>? fun fd ->
Lwt.return_ok (TLSRepr.T fd) in
let close t = Lwt_unix.close t in
{ accept; close; }
let run03 v service =
let `Initialized th = serve_when_ready ~handler service v in th
let run03 = function
| `TCP sockaddr ->
let domain = Unix.domain_of_sockaddr sockaddr in
let socket = Lwt_unix.socket domain Unix.SOCK_STREAM 0 in
Lwt_unix.bind socket sockaddr >>= fun () ->
Lwt_unix.listen socket 40 ;
run03 socket tcp
| `TLS (cfg, sockaddr) ->
let domain = Unix.domain_of_sockaddr sockaddr in
let socket = Lwt_unix.socket domain Unix.SOCK_STREAM 0 in
Lwt_unix.bind socket sockaddr >>= fun () ->
Lwt_unix.listen socket 40 ;
run03 socket (tls cfg)
let load_file filename =
let ic = open_in filename in
let ln = in_channel_length ic in
let rs = Bytes.create ln in
really_input ic rs 0 ln ; close_in ic ;
Cstruct.of_bytes rs
let certificates_of_files cert key =
let cert = load_file cert in
let key = load_file key in
match X509.Certificate.decode_pem_multiple cert,
X509.Private_key.decode_pem key with
| Ok certs, Ok key -> `Single (certs, key)
| _ -> Fmt.failwith "Invalid key or certificate"
let () = match Sys.argv with
| [| _; "server"; port; |] ->
let sockaddr =
Unix.ADDR_INET (Unix.inet_addr_loopback, int_of_string port) in
Lwt_main.run (run03 (`TCP sockaddr))
| [| _; "server"; cert; key; port; |] ->
let sockaddr =
Unix.ADDR_INET (Unix.inet_addr_loopback, int_of_string port) in
let certificates = certificates_of_files cert key in
let tls = Tls.Config.server ~certificates () in
Lwt_main.run (run03 (`TLS (tls, sockaddr)))
| [| _; "client"; uri; |] ->
Lwt_main.run (run02 (Uri.of_string uri) stdin)
|> R.reword_error (R.msgf "%a" Mimic.pp_error)
|> R.failwith_error_msg
| [| _; "client"; uri; filename; |] when Sys.file_exists filename ->
let ic = open_in filename in
let rs = Lwt_main.run (run02 (Uri.of_string uri) ic) in
close_in ic ;
R.failwith_error_msg (R.reword_error (R.msgf "%a" Mimic.pp_error) rs)
| _ ->
Fmt.epr "%s server [cert.pem] [key.pem] <port>\n%!" Sys.argv.(0) ;
Fmt.epr "%s client <uri> [filename]\n%!" Sys.argv.(0)