diff --git a/manifest/product_octez.ml b/manifest/product_octez.ml index 9df6148bc2ec72249f354065f9a2101aaf129610..fc95c603f93aa7e6e47cf840c728197b16af5c60 100644 --- a/manifest/product_octez.ml +++ b/manifest/product_octez.ml @@ -1802,6 +1802,13 @@ let octez_base_unix = "base.unix" ~internal_name:"tezos_base_unix" ~path:"src/lib_base/unix" + ~foreign_stubs: + { + language = C; + flags = [S ":standard"]; + include_dirs = []; + names = ["socket"]; + } ~deps: [ octez_error_monad |> open_; diff --git a/src/lib_base/unix/dune b/src/lib_base/unix/dune index 0e1cc8cfc18437bec0c24495092bf62f22aa9a27..4b27fa2f6097baabf381fcc956939ab3491695ba 100644 --- a/src/lib_base/unix/dune +++ b/src/lib_base/unix/dune @@ -26,4 +26,5 @@ -open Tezos_stdlib -open Tezos_stdlib_unix -open Data_encoding - -open Tezos_event_logging)) + -open Tezos_event_logging) + (foreign_stubs (language c) (flags (:standard)) (names socket))) diff --git a/src/lib_base/unix/socket.c b/src/lib_base/unix/socket.c new file mode 100644 index 0000000000000000000000000000000000000000..65b3ebd6d4e1c7a3cfac0b56e53686661cf5b6dc --- /dev/null +++ b/src/lib_base/unix/socket.c @@ -0,0 +1,28 @@ +/* tcp_user_timeout_stubs.c */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +CAMLprim value ocaml_set_tcp_user_timeout(value fd_val, value timeout_val) { + CAMLparam2(fd_val, timeout_val); + int fd = Int_val(fd_val); + socklen_t timeout = Int_val(timeout_val); + +// TCP_USER_TIMEOUT is not always defined such as in Mac OS/X +#ifdef TCP_USER_TIMEOUT + if (setsockopt(fd, IPPROTO_TCP, TCP_USER_TIMEOUT, &timeout, sizeof(timeout)) < 0) { + uerror("setsockopt(TCP_USER_TIMEOUT)", Nothing); + } + CAMLreturn(Val_unit); +#else + caml_failwith("TCP_USER_TIMEOUT not supported on this platform"); +#endif +} + diff --git a/src/lib_base/unix/socket.ml b/src/lib_base/unix/socket.ml index d4274f6088ce9558c92d1cbf60fda8b687cbbd78..f2d9a2f95e9757d32fd47bc6e75946946f3053b4 100644 --- a/src/lib_base/unix/socket.ml +++ b/src/lib_base/unix/socket.ml @@ -232,3 +232,11 @@ let get_temporary_socket_dir () = match Sys.getenv_opt "XDG_RUNTIME_DIR" with | Some xdg_runtime_dir when xdg_runtime_dir <> "" -> xdg_runtime_dir | Some _ | None -> Filename.get_temp_dir_name () + +external set_tcp_user_timeout : Unix.file_descr -> int -> unit + = "ocaml_set_tcp_user_timeout" + +let set_tcp_user_timeout fd ~ms = + try Ok (set_tcp_user_timeout fd ms) with + | Unix.Unix_error _ as exn -> Error (`Unix_error exn) + | Failure _ -> Error `Unsupported diff --git a/src/lib_base/unix/socket.mli b/src/lib_base/unix/socket.mli index 4fff6f542f7b1002405ce366931d60ee4496ccc0..98a2648b96faeb1ff59585f2e1a58e3b0f631fbd 100644 --- a/src/lib_base/unix/socket.mli +++ b/src/lib_base/unix/socket.mli @@ -72,3 +72,19 @@ val handshake : Lwt_unix.file_descr -> bytes -> unit tzresult Lwt.t environment variable is defined. Otherwise, the default temporary directory is used. *) val get_temporary_socket_dir : unit -> string + +(** [set_tcp_user_timeout fd ~ms] sets the TCP user timeout on socket [fd]. If a message sent + on this socket is not acknowledged within [ms] milliseconds, the connection is considered dead. + This function uses the [TCP_USER_TIMEOUT] socket option. + + @param fd the file descriptor of the socket. + @param ms the timeout value in milliseconds. + @return [Ok ()] if the option was successfully set, or [Error + (`Unix_error exn)] if a Unix error occurred, or [Error + `Unsupported] if the TCP_USER_TIMEOUT option is not supported on + this platform. +*) +val set_tcp_user_timeout : + Unix.file_descr -> + ms:int -> + (unit, [`Unix_error of exn | `Unsupported]) result diff --git a/src/lib_p2p/p2p_events.ml b/src/lib_p2p/p2p_events.ml index cca6bf5777c8eca8713cb30d4c41e83c5eb0c3dc..a555c1a3d23c3f82568bac14a75e349a0071fc31 100644 --- a/src/lib_p2p/p2p_events.ml +++ b/src/lib_p2p/p2p_events.ml @@ -535,6 +535,15 @@ module P2p_fd = struct ~level:Debug ("connection_id", Data_encoding.int31) ("socket", Data_encoding.string) + + let set_socket_option_tcp_user_timeout_failed = + declare_1 + ~section + ~name:"set_socket_option_tcp_user_timeout_failed" + ~msg:"Could not set the TCP_USER_TIMEOUT socket option: {error}" + ~level:Info + ~pp1:Error_monad.pp_print_trace + ("error", Error_monad.trace_encoding) end module P2p_maintainance = struct diff --git a/src/lib_p2p/p2p_fd.ml b/src/lib_p2p/p2p_fd.ml index bf8e078c0ef081cc8d638250a60635b6a05ff10b..bc169be97c8f8547553bf3b3c60d9f06480445a1 100644 --- a/src/lib_p2p/p2p_fd.ml +++ b/src/lib_p2p/p2p_fd.ml @@ -159,6 +159,7 @@ let string_of_sockaddr addr = let id t = t.id let raw_socket () = + let open Lwt_syntax in let sock = Lwt_unix.socket ~cloexec:true PF_INET6 SOCK_STREAM 0 in (* By setting [SO_KEEPALIVE] to [true], the socket is configured to send periodic keep-alive probes to verify that the connection is still @@ -167,16 +168,51 @@ let raw_socket () = It reset (send TCP RST message and close) if the peer is unresponsive. *) Lwt_unix.(setsockopt sock SO_KEEPALIVE true) ; - sock + (* By setting [TCP_USER_TIMEOUT], we ensure that a dead connection is reported + after at most [ms] milliseconds. This option allows the connection timeout + to be much shorter than the default behavior—which can last several minutes + (typically between 5 and 15 minutes) due to TCP retransmission timeouts (RTO). + + Below, we set this value to 15 seconds. This value should not be + too low otherwise we may drop valid connection that were + temporarily busy. The higher it is, the longer it is to detect a + dead connection. We believe 15 seconds is reasonable in practice + (especially this acknowledgement is done at the OS level and so + is quite independent of the Lwt scheduler). *) + let ms_opt = + let default = 15000 (* 15s *) in + try + match Sys.getenv_opt "OCTEZ_P2P_TCP_USER_TIMEOUT" with + | None -> Some default + | Some "0" -> None + | Some value -> Some (int_of_string value) + with _ -> Some default + in + match ms_opt with + | None -> (* The user opt-out from the socket option *) Lwt.return sock + | Some ms -> ( + match Socket.set_tcp_user_timeout (Lwt_unix.unix_file_descr sock) ~ms with + | Ok () | Error `Unsupported -> Lwt.return sock + | Error (`Unix_error exn) -> + (* Socket option [TCP_USER_TIMEOUT] is not mandatory, this is why we only emit an + event at [Info] level. *) + let* () = + Events.(emit set_socket_option_tcp_user_timeout_failed) + [Error_monad.error_of_exn exn] + in + Lwt.return sock) -let socket () = create (raw_socket ()) +let socket () = + let open Lwt_syntax in + let* socket = raw_socket () in + create socket let create_listening_socket ?(reuse_port = false) ~backlog ?(addr = Ipaddr.V6.unspecified) port = let open Lwt_result_syntax in Lwt.catch (fun () -> - let sock = raw_socket () in + let*! sock = raw_socket () in (if reuse_port then Lwt_unix.(setsockopt sock SO_REUSEPORT true)) ; Lwt_unix.(setsockopt sock SO_REUSEADDR true) ; let*! () =