From 14e8c65e41fa648dcfe25e80a971eb8c1f273ab2 Mon Sep 17 00:00:00 2001 From: David Scott Date: Mon, 26 Oct 2009 16:32:15 +0000 Subject: [PATCH] [refactoring] Move stunnel from xen-api.hg to xen-api-libs.hg Signed-off-by: Thomas Gazagnaire --- stunnel/META.in | 5 + stunnel/Makefile | 56 ++++++++ stunnel/stunnel.ml | 267 ++++++++++++++++++++++++++++++++++++++ stunnel/stunnel.mli | 53 ++++++++ stunnel/stunnel_cache.ml | 176 +++++++++++++++++++++++++ stunnel/stunnel_cache.mli | 41 ++++++ 6 files changed, 598 insertions(+) create mode 100644 stunnel/META.in create mode 100644 stunnel/Makefile create mode 100644 stunnel/stunnel.ml create mode 100644 stunnel/stunnel.mli create mode 100644 stunnel/stunnel_cache.ml create mode 100644 stunnel/stunnel_cache.mli diff --git a/stunnel/META.in b/stunnel/META.in new file mode 100644 index 0000000..5b38b05 --- /dev/null +++ b/stunnel/META.in @@ -0,0 +1,5 @@ +version = "@VERSION@" +description = "Secure Tunneling" +requires = "unix,stdext,log" +archive(byte) = "stunnel.cma" +archive(native) = "stunnel.cmxa" diff --git a/stunnel/Makefile b/stunnel/Makefile new file mode 100644 index 0000000..f160f1b --- /dev/null +++ b/stunnel/Makefile @@ -0,0 +1,56 @@ +CC = gcc +CFLAGS = -Wall -fPIC -O2 -I/opt/xensource/lib/ocaml +OCAMLC = ocamlc -g +OCAMLOPT = ocamlopt + +LDFLAGS = -cclib -L./ + +DESTDIR ?= / +VERSION := $(shell hg parents --template "{rev}" 2>/dev/null || echo 0.0) +OCAMLOPTFLAGS = -g -dtypes + +OCAMLABI := $(shell ocamlc -version) +OCAMLLIBDIR := $(shell ocamlc -where) +OCAMLDESTDIR ?= $(OCAMLLIBDIR) + +OBJS = stunnel stunnel_cache +INTF = $(foreach obj, $(OBJS),$(obj).cmi) +LIBS = stunnel.cma stunnel.cmxa + +all: $(INTF) $(LIBS) $(PROGRAMS) + +bins: $(PROGRAMS) + +libs: $(LIBS) + +stunnel.cmxa: $(foreach obj,$(OBJS),$(obj).cmx) + $(OCAMLOPT) $(OCAMLOPTFLAGS) -a -o $@ $(foreach obj,$(OBJS),$(obj).cmx) + +stunnel.cma: $(foreach obj,$(OBJS),$(obj).cmo) + $(OCAMLC) -a -o $@ $(foreach obj,$(OBJS),$(obj).cmo) + +%.cmo: %.ml + $(OCAMLC) -c -I ../stdext -I ../log -o $@ $< + +%.cmi: %.mli + $(OCAMLC) -c -o $@ $< + +%.cmx: %.ml + $(OCAMLOPT) $(OCAMLOPTFLAGS) -c -I ../stdext -I ../log -o $@ $< + +%.o: %.c + $(CC) $(CFLAGS) -c -o $@ $< + +META: META.in + sed 's/@VERSION@/$(VERSION)/g' < $< > $@ + +.PHONY: install +install: $(LIBS) META + ocamlfind install -destdir $(DESTDIR)$(shell ocamlfind printconf destdir) -ldconf ignore stunnel META $(INTF) $(LIBS) *.a *.cmx + +.PHONY: uninstall +uninstall: + ocamlfind remove stunnel + +clean: + rm -f *.o *.so *.a *.cmo *.cmi *.cma *.cmx *.cmxa *.annot $(LIBS) $(PROGRAMS) \ No newline at end of file diff --git a/stunnel/stunnel.ml b/stunnel/stunnel.ml new file mode 100644 index 0000000..c1191d9 --- /dev/null +++ b/stunnel/stunnel.ml @@ -0,0 +1,267 @@ +(* + * Copyright (C) 2006-2009 Citrix Systems Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; version 2.1 only. with the special + * exception on linking described in file LICENSE. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + *) +(* Copyright (C) 2007 XenSource Inc *) + +open Printf +open Pervasiveext +open Stringext + +exception Stunnel_binary_missing +exception Stunnel_error of string +exception Stunnel_verify_error of string + +let certificate_path = "/etc/stunnel/certs" +let crl_path = "/etc/stunnel/crls" + +let use_new_stunnel = ref false +let new_stunnel_path = "/usr/sbin/stunnelng" + +let cached_stunnel_path = ref None + +let init_stunnel_path () = + try cached_stunnel_path := Some (Unix.getenv "XE_STUNNEL") + with Not_found -> + if !use_new_stunnel then + cached_stunnel_path := Some new_stunnel_path + else ( + let choices = ["/opt/xensource/libexec/stunnel/stunnel"; + "/usr/sbin/stunnel4"; + "/usr/sbin/stunnel"; + "/usr/bin/stunnel4"; + "/usr/bin/stunnel"; + ] in + let rec choose l = + match l with + [] -> raise Stunnel_binary_missing + | (p::ps) -> + try Unix.access p [Unix.X_OK]; p + with _ -> choose ps in + let path = choose choices in + cached_stunnel_path := Some path + ) + +let stunnel_path() = + match !cached_stunnel_path with + | Some p -> p + | None -> raise Stunnel_binary_missing + +type t = { mutable pid: int; fd: Unix.file_descr; host: string; port: int; + connected_time: float; + unique_id: int option; + mutable logfile: string; + } + +let config_file verify_cert extended_diagnosis host port = + let lines = ["client=yes"; "foreground=yes"; "socket = r:TCP_NODELAY=1"; Printf.sprintf "connect=%s:%d" host port ] @ + (if extended_diagnosis then + ["debug=4"] + else + []) @ + (if verify_cert then + ["verify=2"; + sprintf "CApath=%s" certificate_path; + sprintf "CRLpath=%s" crl_path] + else + []) + in + String.concat "" (List.map (fun x -> x ^ "\n") lines) + +let ignore_exn f x = try f x with _ -> () + +let disconnect x = + List.iter (ignore_exn Unix.close) [ x.fd ]; + ignore_exn Forkhelpers.waitpid x.pid + +(* With some probability, stunnel fails during its startup code before it reads + the config data from us. Therefore we get a SIGPIPE writing the config data. + Assuming SIGPIPE has been ignored, catch the failing write and throw this + exception instead *) +exception Stunnel_initialisation_failed + +let attempt_one_connect_new ?unique_id ?(use_external_fd_wrapper = true) ?(write_to_log = fun _ -> ()) verify_cert extended_diagnosis host port = + assert (not verify_cert); (* !!! Unimplemented *) + assert (not extended_diagnosis); (* !!! Unimplemented *) + let data_out,data_in = Unix.socketpair Unix.PF_UNIX Unix.SOCK_STREAM 0 in + let args = [ "-m"; "client"; "-s"; "-"; "-d"; Printf.sprintf "%s:%d" host port ] in + let t = { pid = 0; fd = data_out; host = host; port = port; + connected_time = Unix.gettimeofday (); unique_id = unique_id; + logfile = "" } in + let to_close = ref [ data_in ] in + let result = Forkhelpers.with_logfile_fd "stunnel" (fun logfd -> + let fdops = [ + Forkhelpers.Dup2(data_in, Unix.stdin); + Forkhelpers.Dup2(data_in, Unix.stdout); + Forkhelpers.Dup2(logfd, Unix.stderr) + ] in + let fds_needed = [ Unix.stdin; Unix.stdout; Unix.stderr ] in + t.pid <- ( + if use_external_fd_wrapper then + Forkhelpers.safe_close_and_exec fdops fds_needed (stunnel_path ()) args + else + Forkhelpers.fork_and_exec ~pre_exec:(fun _ -> + List.iter Forkhelpers.do_fd_operation fdops; + Unixext.close_all_fds_except fds_needed + ) ((stunnel_path ()) :: args) + ); + List.iter Unix.close [ data_in ]; + ) in + List.iter Unix.close !to_close; + match result with + | Forkhelpers.Failure(log, exn) -> + write_to_log ("failed: Log from stunnel: [" ^ log ^ "]"); + disconnect t; + raise exn + | Forkhelpers.Success(log, _) -> + write_to_log ("success: Log from stunnel: [" ^ log ^ "]"); + t + +(* Internal function which may throw Stunnel_initialisation_failed *) +let attempt_one_connect ?unique_id ?(use_external_fd_wrapper = true) ?(write_to_log = fun _ -> ()) verify_cert extended_diagnosis host port = + let data_out,data_in = Unix.socketpair Unix.PF_UNIX Unix.SOCK_STREAM 0 + and config_out, config_in = Unix.pipe () + in + (* FDs we must close. NB stdin_in and stdout_out end up in our 't' record *) + let to_close = ref [ data_in; config_out; config_in ] in + let close fd = + if List.mem fd !to_close + then (Unix.close fd; to_close := List.filter (fun x -> x <> fd) !to_close) in + let t = { pid = 0; fd = data_out; host = host; port = port; + connected_time = Unix.gettimeofday (); unique_id = unique_id; + logfile = "" } in + let result = Forkhelpers.with_logfile_fd "stunnel" + ~delete:(not extended_diagnosis) + (fun logfd -> + let path = stunnel_path() in + let fdops = + [ Forkhelpers.Dup2(data_in, Unix.stdin); + Forkhelpers.Dup2(data_in, Unix.stdout); + Forkhelpers.Dup2(logfd, Unix.stderr) ] in + let fds_needed = [ Unix.stdin; Unix.stdout; Unix.stderr; config_out ] in + let args = [ "-fd"; string_of_int (Unixext.int_of_file_descr config_out) ] in + if use_external_fd_wrapper then begin + let cmdline = Printf.sprintf "Using commandline: %s\n" (String.concat " " (Forkhelpers.close_and_exec_cmdline fds_needed path args)) in + write_to_log cmdline; + end; + t.pid <- + (if use_external_fd_wrapper + (* Run thread-safe external wrapper *) + then Forkhelpers.safe_close_and_exec fdops fds_needed path args + (* or do it ourselves (safe if there are no threads) *) + else Forkhelpers.fork_and_exec ~pre_exec: + (fun _ -> + List.iter Forkhelpers.do_fd_operation fdops; + Unixext.close_all_fds_except fds_needed) + (path::args) ); + List.iter close [ data_in; config_out; ]; + (* Make sure we close config_in eventually *) + finally + (fun () -> + + let pidmsg = Printf.sprintf "stunnel has pid: %d\n" t.pid in + write_to_log pidmsg; + + let config = config_file verify_cert extended_diagnosis host port in + (* Catch the occasional initialisation failure of stunnel: *) + try + let n = Unix.write config_in config 0 (String.length config) in + if n < String.length config then raise Stunnel_initialisation_failed + with Unix.Unix_error(err, fn, arg) -> + write_to_log (Printf.sprintf "Caught Unix.Unix_error(%s, %s, %s); raising Stunnel_initialisation_failed" (Unix.error_message err) fn arg); + raise Stunnel_initialisation_failed) + (fun () -> close config_in)) in + (* Tidy up any remaining unclosed fds *) + List.iter Unix.close !to_close; + match result with + | Forkhelpers.Success(log, _) -> + if extended_diagnosis then + begin + write_to_log "success"; + t.logfile <- log + end + else + write_to_log ("success: Log from stunnel: [" ^ log ^ "]"); + t + | Forkhelpers.Failure(log, exn) -> + write_to_log ("failed: Log from stunnel: [" ^ log ^ "]"); + disconnect t; + raise exn + +(** To cope with a slightly unreliable stunnel, attempt to retry to make + the connection a number of times. *) +let rec retry f = function + | 0 -> raise Stunnel_initialisation_failed + | n -> + try f () + with Stunnel_initialisation_failed -> + (* Leave a few seconds between each attempt *) + ignore(Unix.select [] [] [] 3.); + retry f (n - 1) + +(** Establish a fresh stunnel to a (host, port) + @param extended_diagnosis If true, the stunnel log file will not be + deleted. Instead, it is the caller's responsibility to delete it. This + allows the caller to use diagnose_failure below if stunnel fails. *) +let connect ?unique_id ?use_external_fd_wrapper ?write_to_log + ?(verify_cert=false) ?(extended_diagnosis=false) host port = + let connect = if !use_new_stunnel then attempt_one_connect_new else attempt_one_connect in + retry (fun () -> connect ?unique_id ?use_external_fd_wrapper ?write_to_log verify_cert extended_diagnosis host port) 5 + +let sub_after i s = + let len = String.length s in + String.sub s i (len - i) + +let split_1 c s = + match String.split ~limit:1 c s with + | x :: _ -> x + | [] -> s + +let check_verify_error line = + match String.find_all "VERIFY ERROR: " line with + | p :: _ -> + begin + match String.find_all "error=" line with + | e :: _ -> + raise + (Stunnel_verify_error + (split_1 ',' + (sub_after (e + String.length "error=") line))) + | [] -> + raise (Stunnel_verify_error "") + end + | [] -> + () + +let check_error s line = + if (String.has_substr line s) then + raise (Stunnel_error s) + +let diagnose_failure st_proc = + let check_line line = + Printf.eprintf "stunnel_failure: %s\n" line; + check_verify_error line; + check_error "Connection refused" line; + check_error "No host resolved" line; + check_error "Invalid argument" line; + in + Unixext.readfile_line check_line st_proc.logfile; + raise (Stunnel_error (Unixext.read_whole_file_to_string st_proc.logfile)) + +let test host port = + let counter = ref 0 in + while true do + let c = connect ~write_to_log:print_endline host port in + disconnect c; + incr counter; + if !counter mod 100 = 0 then (Printf.printf "Ran stunnel %d times\n" !counter; flush stdout) + done diff --git a/stunnel/stunnel.mli b/stunnel/stunnel.mli new file mode 100644 index 0000000..4251bc6 --- /dev/null +++ b/stunnel/stunnel.mli @@ -0,0 +1,53 @@ +(* + * Copyright (C) 2006-2009 Citrix Systems Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; version 2.1 only. with the special + * exception on linking described in file LICENSE. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + *) +(** Thrown if we can't find the stunnel binary in the prescribed location *) +exception Stunnel_binary_missing +exception Stunnel_error of string +exception Stunnel_verify_error of string + +val certificate_path : string +val crl_path : string + +val use_new_stunnel : bool ref +val init_stunnel_path : unit -> unit + +(** Represents an active stunnel connection *) +type t = { mutable pid: int; + fd: Unix.file_descr; + host: string; + port: int; + connected_time: float; (** time when the connection opened, for 'early retirement' *) + unique_id: int option; + mutable logfile: string; + } + +(** Connects via stunnel (optionally via an external 'close fds' wrapper) to + a host and port. + NOTE: this does not guarantee the connection to the remote server actually works. + For server-side connections, use Xmlrpcclient.get_reusable_stunnel instead. + *) +val connect : + ?unique_id:int -> + ?use_external_fd_wrapper:bool -> + ?write_to_log:(string -> unit) -> + ?verify_cert:bool -> + ?extended_diagnosis:bool -> + string -> int -> t + +(** Disconnects from stunnel and cleans up *) +val disconnect : t -> unit + +val diagnose_failure : t -> unit + +val test : string -> int -> unit diff --git a/stunnel/stunnel_cache.ml b/stunnel/stunnel_cache.ml new file mode 100644 index 0000000..feb6dac --- /dev/null +++ b/stunnel/stunnel_cache.ml @@ -0,0 +1,176 @@ +(* + * Copyright (C) 2006-2009 Citrix Systems Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; version 2.1 only. with the special + * exception on linking described in file LICENSE. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + *) + +(** Operate a small cache of stunnels so we can re-use them for repeated calls *) + +(* Caveats: + * stunnel donators should only donate stunnels which they knows are connected + to the main HTTP request loop in the server -- HTTP 1.1 should be used and + the connection should be kept-alive + *) + +module D=Debug.Debugger(struct let name="stunnel_cache" end) +open D + +type endpoint = { host: string; port: int } + +(** An index of endpoints to stunnel IDs *) +let index : (endpoint, int list) Hashtbl.t ref = ref (Hashtbl.create 10) +(** A mapping of stunnel unique IDs to donation times *) +let times : (int, float) Hashtbl.t ref = ref (Hashtbl.create 10) +(** A mapping of stunnel unique ID to Stunnel.t *) +let stunnels : (int, Stunnel.t) Hashtbl.t ref = ref (Hashtbl.create 10) + +open Pervasiveext +open Threadext +open Listext + +let m = Mutex.create () + +(* Need to limit the absolute number of stunnels as well as the maximum age *) +let max_stunnel = 4 +let max_age = 5. *. 60. (* seconds *) +let max_idle = 2. *. 60. (* seconds *) + +let id_of_stunnel stunnel = + Opt.default "unknown" (Opt.map string_of_int stunnel.Stunnel.unique_id) + +let unlocked_gc () = + let now = Unix.gettimeofday () in + let string_of_id id = + let stunnel = Hashtbl.find !stunnels id in + Printf.sprintf "(id %s / idle %.2f age %.2f)" + (id_of_stunnel stunnel) + (now -. (Hashtbl.find !times id)) + (now -. stunnel.Stunnel.connected_time) in + let string_of_endpoint ep = Printf.sprintf "%s:%d" ep.host ep.port in + let string_of_index ep xs = Printf.sprintf "[ %s %s ]" (string_of_endpoint ep) (String.concat "; " (List.map string_of_id xs)) in + debug "Cache contents: %s" (Hashtbl.fold (fun ep xs acc -> string_of_index ep xs ^ " " ^ acc) !index ""); + + let all_ids = Hashtbl.fold (fun k _ acc -> k :: acc) !stunnels [] in + + let to_gc = ref [] in + (* Find the ones which are too old *) + let now = Unix.gettimeofday () in + Hashtbl.iter + (fun idx stunnel -> + let time = Hashtbl.find !times idx in + let idle = now -. time in + let age = now -. stunnel.Stunnel.connected_time in + if age > max_age then begin + debug "Expiring stunnel id %s; age (%.2f) > limit (%.2f)" (id_of_stunnel stunnel) age max_age; + to_gc := idx :: !to_gc + end else if idle > max_idle then begin + debug "Expiring stunnel id %s; idle (%.2f) > limit (%.2f)" (id_of_stunnel stunnel) age max_idle; + to_gc := idx :: !to_gc + end) !stunnels; + let num_remaining = List.length all_ids - (List.length !to_gc) in + if num_remaining > max_stunnel then begin + let times' = Hashtbl.fold (fun k v acc -> (k, v) :: acc) !times [] in + let times' = List.filter (fun (idx, _) -> not(List.mem idx !to_gc)) times' in + (* Sort into descending order of donation time, ie youngest first *) + let times' = List.sort (fun x y -> compare (fst y) (fst x)) times' in + let youngest, oldest = List.chop max_stunnel times' in + let oldest_ids = List.map fst oldest in + List.iter + (fun x -> + let stunnel = Hashtbl.find !stunnels x in + debug "Expiring stunnel id %s since we have too many cached tunnels (limit is %d)" + (id_of_stunnel stunnel) max_stunnel) oldest_ids; + to_gc := !to_gc @ oldest_ids + end; + (* Disconnect all stunnels we wish to GC *) + List.iter (fun id -> + let s = Hashtbl.find !stunnels id in + Stunnel.disconnect s) !to_gc; + (* Remove all reference to them from our cache hashtables *) + let index' = Hashtbl.create 10 in + Hashtbl.iter + (fun ep ids -> + Hashtbl.add index' ep (List.filter (fun id -> not(List.mem id !to_gc)) ids)) !index; + let times' = Hashtbl.copy !times in + List.iter (fun idx -> Hashtbl.remove times' idx) !to_gc; + let stunnels' = Hashtbl.copy !stunnels in + List.iter (fun idx -> Hashtbl.remove stunnels' idx) !to_gc; + + index := index'; + times := times'; + stunnels := stunnels' + +let gc () = Mutex.execute m unlocked_gc + +let counter = ref 0 + +let add (x: Stunnel.t) = + let now = Unix.gettimeofday () in + Mutex.execute m + (fun () -> + let idx = !counter in + incr counter; + Hashtbl.add !times idx now; + Hashtbl.add !stunnels idx x; + let ep = { host = x.Stunnel.host; port = x.Stunnel.port } in + let existing = + if Hashtbl.mem !index ep + then Hashtbl.find !index ep + else [] in + Hashtbl.replace !index ep (idx :: existing); + debug "Adding stunnel id %s (idle %.2f) to the cache" + (id_of_stunnel x) 0.; + unlocked_gc () + ) + +(** Returns an Stunnel.t for this endpoint (oldest first), raising Not_found + if none can be found *) +let remove host port = + let ep = { host = host; port = port } in + Mutex.execute m + (fun () -> + unlocked_gc (); + + let ids = Hashtbl.find !index ep in + let table = List.map (fun id -> id, Hashtbl.find !times id) ids in + let sorted = List.sort (fun a b -> compare (snd a) (snd b)) table in + match sorted with + | (id, time) :: _ -> + let stunnel = Hashtbl.find !stunnels id in + debug "Removing stunnel id %s (idle %.2f) from the cache" + (id_of_stunnel stunnel) (Unix.gettimeofday () -. time); + let stunnel = Hashtbl.find !stunnels id in + Hashtbl.remove !stunnels id; + Hashtbl.remove !times id; + Hashtbl.replace !index ep (List.filter (fun x -> x <> id) ids); + stunnel + | _ -> raise Not_found + ) + +(** Flush the cache - remove everything *) +let flush () = + Mutex.execute m + (fun () -> + info "Flushing cache"; + Hashtbl.iter (fun id st -> Stunnel.disconnect st) !stunnels; + Hashtbl.clear !stunnels; + Hashtbl.clear !times; + Hashtbl.clear !index; + info "Flushed!") + + +let connect ?use_external_fd_wrapper ?write_to_log host port = + try + remove host port + with Not_found -> + error "Failed to find stunnel in cache for endpoint %s:%d" host port; + Stunnel.connect ?use_external_fd_wrapper ?write_to_log host port + diff --git a/stunnel/stunnel_cache.mli b/stunnel/stunnel_cache.mli new file mode 100644 index 0000000..bb8cb57 --- /dev/null +++ b/stunnel/stunnel_cache.mli @@ -0,0 +1,41 @@ +(* + * Copyright (C) 2006-2009 Citrix Systems Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; version 2.1 only. with the special + * exception on linking described in file LICENSE. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + *) + +(** Operate a small cache of stunnels so we can re-use them for repeated calls. + + Caveats: + * stunnel donators should only donate stunnels which they knows are connected + to the main HTTP request loop in the server -- HTTP 1.1 should be used and + the connection should be kept-alive. +*) + + +(** Connects via stunnel (optionally via an external 'close fds' wrapper) to + a host and port. If there is a suitable stunnel in the cache then this + will be used, otherwise we make a fresh one. *) +val connect : + ?use_external_fd_wrapper:bool -> + ?write_to_log:(string -> unit) -> string -> int -> Stunnel.t + +(** Adds a reusable stunnel to the cache *) +val add : Stunnel.t -> unit + +(** Given a host and port return a cached stunnel, or throw Not_found *) +val remove : string -> int -> Stunnel.t + +(** Empty the cache of all stunnels *) +val flush : unit -> unit + +(** GCs old stunnels *) +val gc : unit -> unit -- 2.39.5