From b0afd4401e02d6f698fb37521098ac8032cd6dbd Mon Sep 17 00:00:00 2001 From: Bikal Lem Date: Tue, 30 Aug 2022 13:00:03 +0100 Subject: [PATCH 1/2] net: add Net.with_tcp_connect `Net.with_tcp_connect` takes a domain-name/port/service and attempts to establish connection to it. IP addresses for the host are tried one after the other while preferring/trying IPv6 protocol addresses first if available. We also introduce two new exceptions: 1. Invalid_host - raised when a connection couldn't be established for any of the addresses defined for a given host/port/service. 2. Unix_error - introduced to mimic Unix.Unix_error without depending on "unix" library. --- lib_eio/net.ml | 37 ++++++++++++++++++++++++++++++++++++ lib_eio/net.mli | 30 ++++++++++++++++++++++++++++- lib_eio_linux/eio_linux.ml | 3 ++- tests/network.md | 39 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 2 deletions(-) diff --git a/lib_eio/net.ml b/lib_eio/net.ml index 8f98053ec..69a821fd9 100644 --- a/lib_eio/net.ml +++ b/lib_eio/net.ml @@ -2,6 +2,10 @@ exception Connection_reset of exn (** This is a wrapper for EPIPE, ECONNRESET and similar errors. It indicates that the flow has failed, and data may have been lost. *) +exception Unix_error of string * string * string +(* Unix.Unix_error without depending on "unix" *) + +exception Connection_failure of exn module Ipaddr = struct type 'a t = string (* = [Unix.inet_addr], but avoid a Unix dependency here *) @@ -208,3 +212,36 @@ let getaddrinfo_datagram ?service t hostname = let getnameinfo (t:#t) sockaddr = t#getnameinfo sockaddr let close = Flow.close + +type 'a timeout = (#Time.clock as 'a) * float + +let rec try_connection connect_fn = function + | [] -> None + | addr::addrs -> ( + match connect_fn addr with + | conn -> Some conn + | exception Unix_error(_,_,_) + | exception Time.Timeout -> try_connection connect_fn addrs) + +let with_tcp_connect ?timeout ~host ~service t f = + let addrs = + getaddrinfo_stream ~service t host + |> List.filter_map (function + | `Tcp _ as x -> Some x + | `Unix _ -> None) + in + Switch.run (fun sw -> + let conn = + match timeout with + | Some (clock, d) -> + let connect_fn addr = Time.with_timeout_exn clock d (fun () -> connect ~sw t addr) in + try_connection connect_fn addrs + | None -> + let connect_fn addr = connect ~sw t addr in + try_connection connect_fn addrs + in + match conn with + | Some conn -> f conn + | None -> + let exn = Failure (Printf.sprintf "Unable to connect to host:%s, service:%s" host service) in + raise @@ Connection_failure exn) diff --git a/lib_eio/net.mli b/lib_eio/net.mli index 2fa163b0b..265507ed2 100644 --- a/lib_eio/net.mli +++ b/lib_eio/net.mli @@ -12,6 +12,8 @@ *) exception Connection_reset of exn +exception Unix_error of string * string * string +exception Connection_failure of exn (** IP addresses. *) module Ipaddr : sig @@ -113,7 +115,33 @@ end val connect : sw:Switch.t -> #t -> Sockaddr.stream -> (** [connect ~sw t addr] is a new socket connected to remote address [addr]. - The new socket will be closed when [sw] finishes, unless closed manually first. *) + The new socket will be closed when [sw] finishes, unless closed manually first. + + @raise Unix_error if connection couldn't be established. *) + +type 'a timeout = (#Time.clock as 'a) * float + +val with_tcp_connect : + ?timeout:'a timeout -> + host:string -> + service:string -> + #t -> + ( -> 'b) -> + 'b +(** [with_tcp_connect ~host ~service t f] creates a tcp connection [conn] to [host] and [service] and executes + [f conn]. IPv6 connection is preferred over IPv4 addresses if [host] provides them. If a connection + can't be established for any of the addresses defined for [host], then [Connection_failure] exception is raised. + + [conn] is closed after [f] returns if it isn't already closed. + + [host] is either an IP address or a domain name, eg. "www.example.org", "www.ocaml.org" or "127.0.0.1". + + [service] is an IANA recognized service name or port number, eg. "http", "ftp", "8080" etc. + See https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml. + + [timeout] specifies the amount of seconds to wait for establishing the connection to host per host ip address. + + @raise Connection_failure. *) (** {2 Incoming Connections} *) diff --git a/lib_eio_linux/eio_linux.ml b/lib_eio_linux/eio_linux.ml index 60fb0007c..2b0f572a9 100644 --- a/lib_eio_linux/eio_linux.ml +++ b/lib_eio_linux/eio_linux.ml @@ -747,7 +747,8 @@ module Low_level = struct let res = enter (enqueue_connect fd addr) in Log.debug (fun l -> l "connect returned"); if res < 0 then ( - raise (Unix.Unix_error (Uring.error_of_errno res, "connect", "")) + let err = Uring.error_of_errno res |> Unix.error_message in + raise (Eio.Net.Unix_error (err, "connect", "")) ) let send_msg fd ?(fds=[]) ?dst buf = diff --git a/tests/network.md b/tests/network.md index 7a6f01e0e..c0afcce3c 100644 --- a/tests/network.md +++ b/tests/network.md @@ -546,3 +546,42 @@ EPIPE: Eio.Net.getnameinfo env#net sockaddr;; - : string * string = ("localhost", "http") ``` + +## with_tcp_connet + + +```ocaml +# Eio_main.run @@ fun env -> + Eio.Net.with_tcp_connect ~host:"www.example.com" ~service:"http" env#net (fun conn -> + let req = "GET / HTTP/1.1\r\nHost:www.example.com:80\r\n\r\n" in + Eio.Flow.copy_string req conn; + let cs = Cstruct.create 25 in + Eio.Flow.read_exact conn cs; + Eio.Flow.close conn; + Eio.traceln "%S" (Cstruct.to_string cs));; ++"HTTP/1.1 200 OK\r\nAge: 372" +- : unit = () + +# Eio_main.run @@ fun env -> + Eio.Net.with_tcp_connect ~timeout:(env#clock, 0.1) ~host:"www.example.com" ~service:"http" env#net (fun conn -> + let req = "GET / HTTP/1.1\r\nHost:www.example.com:80\r\n\r\n" in + Eio.Flow.copy_string req conn; + let cs = Cstruct.create 25 in + Eio.Flow.read_exact conn cs; + Eio.Flow.close conn; + Eio.traceln "%S" (Cstruct.to_string cs));; ++"HTTP/1.1 200 OK\r\nAge: 296" +- : unit = () + +# Eio_main.run @@ fun env -> + Eio.Net.with_tcp_connect ~timeout:(env#clock, 0.01) ~host:"www.example.com" ~service:"http" env#net (fun conn -> + let req = "GET / HTTP/1.1\r\nHost:www.example.com:80\r\n\r\n" in + Eio.Flow.copy_string req conn; + let cs = Cstruct.create 25 in + Eio.Flow.read_exact conn cs; + Eio.Flow.close conn; + Eio.traceln "%S" (Cstruct.to_string cs));; +Exception: +Eio__Net.Connection_failure + (Failure "Unable to connect to host:www.example.com, service:http"). +``` From bd160c91c2dd1110db157658c7a78b84ac04ada0 Mon Sep 17 00:00:00 2001 From: Thomas Leonard Date: Mon, 26 Sep 2022 10:38:21 +0100 Subject: [PATCH 2/2] Update to new timeout system --- lib_eio/net.ml | 53 +++++------- lib_eio/net.mli | 19 ++--- lib_eio_linux/eio_linux.ml | 4 +- lib_eio_luv/eio_luv.ml | 7 +- tests/network.md | 168 ++++++++++++++++++++++++++++++------- 5 files changed, 173 insertions(+), 78 deletions(-) diff --git a/lib_eio/net.ml b/lib_eio/net.ml index 69a821fd9..9267a20c2 100644 --- a/lib_eio/net.ml +++ b/lib_eio/net.ml @@ -2,9 +2,6 @@ exception Connection_reset of exn (** This is a wrapper for EPIPE, ECONNRESET and similar errors. It indicates that the flow has failed, and data may have been lost. *) -exception Unix_error of string * string * string -(* Unix.Unix_error without depending on "unix" *) - exception Connection_failure of exn module Ipaddr = struct @@ -213,35 +210,23 @@ let getnameinfo (t:#t) sockaddr = t#getnameinfo sockaddr let close = Flow.close -type 'a timeout = (#Time.clock as 'a) * float - -let rec try_connection connect_fn = function - | [] -> None - | addr::addrs -> ( - match connect_fn addr with - | conn -> Some conn - | exception Unix_error(_,_,_) - | exception Time.Timeout -> try_connection connect_fn addrs) - -let with_tcp_connect ?timeout ~host ~service t f = - let addrs = - getaddrinfo_stream ~service t host - |> List.filter_map (function - | `Tcp _ as x -> Some x - | `Unix _ -> None) +let with_tcp_connect ?(timeout=Time.Timeout.none) ~host ~service t f = + Switch.run @@ fun sw -> + let rec aux = function + | [] -> raise (Connection_failure (Failure (Fmt.str "No TCP addresses for %S" host))) + | addr :: addrs -> + match Time.Timeout.run_exn timeout (fun () -> connect ~sw t addr) with + | conn -> f conn + | exception (Time.Timeout | Connection_failure _) when addrs <> [] -> + aux addrs + | exception (Connection_failure _ as ex) -> + raise ex + | exception (Time.Timeout as ex) -> + raise (Connection_failure ex) in - Switch.run (fun sw -> - let conn = - match timeout with - | Some (clock, d) -> - let connect_fn addr = Time.with_timeout_exn clock d (fun () -> connect ~sw t addr) in - try_connection connect_fn addrs - | None -> - let connect_fn addr = connect ~sw t addr in - try_connection connect_fn addrs - in - match conn with - | Some conn -> f conn - | None -> - let exn = Failure (Printf.sprintf "Unable to connect to host:%s, service:%s" host service) in - raise @@ Connection_failure exn) + getaddrinfo_stream ~service t host + |> List.filter_map (function + | `Tcp _ as x -> Some x + | `Unix _ -> None + ) + |> aux diff --git a/lib_eio/net.mli b/lib_eio/net.mli index 265507ed2..2e8c77534 100644 --- a/lib_eio/net.mli +++ b/lib_eio/net.mli @@ -12,7 +12,6 @@ *) exception Connection_reset of exn -exception Unix_error of string * string * string exception Connection_failure of exn (** IP addresses. *) @@ -117,31 +116,31 @@ val connect : sw:Switch.t -> #t -> Sockaddr.stream -> + ?timeout:Time.Timeout.t -> host:string -> service:string -> #t -> ( -> 'b) -> 'b (** [with_tcp_connect ~host ~service t f] creates a tcp connection [conn] to [host] and [service] and executes - [f conn]. IPv6 connection is preferred over IPv4 addresses if [host] provides them. If a connection - can't be established for any of the addresses defined for [host], then [Connection_failure] exception is raised. + [f conn]. - [conn] is closed after [f] returns if it isn't already closed. + [conn] is closed after [f] returns (if it isn't already closed by then). [host] is either an IP address or a domain name, eg. "www.example.org", "www.ocaml.org" or "127.0.0.1". [service] is an IANA recognized service name or port number, eg. "http", "ftp", "8080" etc. See https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml. - [timeout] specifies the amount of seconds to wait for establishing the connection to host per host ip address. + Addresses are tried in the order they are returned by {!getaddrinfo}, until one succeeds. + + @param timeout Limits how long to wait for each connection attempt before moving on to the next. + By default there is no timeout (beyond what the underlying network does). - @raise Connection_failure. *) + @raise Connection_failure A connection couldn't be established for any of the addresses defined for [host]. *) (** {2 Incoming Connections} *) diff --git a/lib_eio_linux/eio_linux.ml b/lib_eio_linux/eio_linux.ml index 2b0f572a9..c6821e905 100644 --- a/lib_eio_linux/eio_linux.ml +++ b/lib_eio_linux/eio_linux.ml @@ -747,8 +747,8 @@ module Low_level = struct let res = enter (enqueue_connect fd addr) in Log.debug (fun l -> l "connect returned"); if res < 0 then ( - let err = Uring.error_of_errno res |> Unix.error_message in - raise (Eio.Net.Unix_error (err, "connect", "")) + let ex = Unix.Unix_error (Uring.error_of_errno res, "connect", "") in + raise (Eio.Net.Connection_failure ex) ) let send_msg fd ?(fds=[]) ?dst buf = diff --git a/lib_eio_luv/eio_luv.ml b/lib_eio_luv/eio_luv.ml index b3d3ca4ab..a562ed3ce 100644 --- a/lib_eio_luv/eio_luv.ml +++ b/lib_eio_luv/eio_luv.ml @@ -489,8 +489,9 @@ module Low_level = struct let connect_pipe ~sw path = let sock = Luv.Pipe.init ~loop:(get_loop ()) () |> or_raise |> Handle.of_luv ~sw in - await_exn (fun _loop _fiber -> Luv.Pipe.connect (Handle.get "connect" sock) path); - sock + match await (fun _loop _fiber -> Luv.Pipe.connect (Handle.get "connect" sock) path) with + | Ok () -> sock + | Error e -> raise (Eio.Net.Connection_failure (Luv_error e)) let connect_tcp ~sw addr = let sock = Luv.TCP.init ~loop:(get_loop ()) () |> or_raise in @@ -503,7 +504,7 @@ module Low_level = struct Luv.Handle.close sock ignore; match Fiber_context.get_error k.fiber with | Some ex -> enqueue_failed_thread st k ex - | None -> enqueue_failed_thread st k (Luv_error e) + | None -> enqueue_failed_thread st k (Eio.Net.Connection_failure (Luv_error e)) ); Fiber_context.set_cancel_fn k.fiber (fun _ex -> match Luv.Handle.fileno sock with diff --git a/tests/network.md b/tests/network.md index c0afcce3c..aceae0495 100644 --- a/tests/network.md +++ b/tests/network.md @@ -470,6 +470,20 @@ EPIPE: - : unit = () ``` +Connection refused: + +```ocaml +# Eio_main.run @@ fun env -> + Switch.run @@ fun sw -> + try + ignore (Eio.Net.connect ~sw env#net (`Unix "idontexist.sock")); + assert false + with Eio.Net.Connection_failure _ -> + traceln "Connection failure";; ++Connection failure +- : unit = () +``` + ## Shutdown ```ocaml @@ -549,39 +563,135 @@ EPIPE: ## with_tcp_connet - ```ocaml -# Eio_main.run @@ fun env -> - Eio.Net.with_tcp_connect ~host:"www.example.com" ~service:"http" env#net (fun conn -> - let req = "GET / HTTP/1.1\r\nHost:www.example.com:80\r\n\r\n" in - Eio.Flow.copy_string req conn; - let cs = Cstruct.create 25 in - Eio.Flow.read_exact conn cs; - Eio.Flow.close conn; - Eio.traceln "%S" (Cstruct.to_string cs));; -+"HTTP/1.1 200 OK\r\nAge: 372" +let net = Eio_mock.Net.make "mock-net" +let addr1 = `Tcp (Eio.Net.Ipaddr.V4.loopback, 80) +let addr2 = `Tcp (Eio.Net.Ipaddr.of_raw "\001\002\003\004", 8080) +let connection_failure = Eio.Net.Connection_failure (Failure "Simulated connection failure") +``` + +No usable addresses: + +```ocaml +# Eio_mock.Backend.run @@ fun () -> + Eio_mock.Net.on_getaddrinfo net [`Return [`Unix "/foo"]]; + Eio.Net.with_tcp_connect ~host:"www.example.com" ~service:"http" net (fun _ -> assert false);; ++mock-net: getaddrinfo ~service:http www.example.com +Exception: +Eio__Net.Connection_failure + (Failure "No TCP addresses for \"www.example.com\""). +``` + +First address works: + +```ocaml +# Eio_mock.Backend.run @@ fun () -> + Eio_mock.Net.on_getaddrinfo net [`Return [addr1; addr2]]; + let mock_flow = Eio_mock.Flow.make "flow" in + Eio_mock.Net.on_connect net [`Return mock_flow]; + Eio.Net.with_tcp_connect ~host:"www.example.com" ~service:"http" net (fun conn -> + let req = "GET / HTTP/1.1\r\nHost:www.example.com:80\r\n\r\n" in + Eio.Flow.copy_string req conn + );; ++mock-net: getaddrinfo ~service:http www.example.com ++mock-net: connect to tcp:127.0.0.1:80 ++flow: wrote "GET / HTTP/1.1\r\n" ++ "Host:www.example.com:80\r\n" ++ "\r\n" ++flow: closed - : unit = () +``` -# Eio_main.run @@ fun env -> - Eio.Net.with_tcp_connect ~timeout:(env#clock, 0.1) ~host:"www.example.com" ~service:"http" env#net (fun conn -> - let req = "GET / HTTP/1.1\r\nHost:www.example.com:80\r\n\r\n" in - Eio.Flow.copy_string req conn; - let cs = Cstruct.create 25 in - Eio.Flow.read_exact conn cs; - Eio.Flow.close conn; - Eio.traceln "%S" (Cstruct.to_string cs));; -+"HTTP/1.1 200 OK\r\nAge: 296" +Second address works: + +```ocaml +# Eio_mock.Backend.run @@ fun () -> + Eio_mock.Net.on_getaddrinfo net [`Return [addr1; addr2]]; + let mock_flow = Eio_mock.Flow.make "flow" in + Eio_mock.Net.on_connect net [`Raise connection_failure; + `Return mock_flow]; + Eio.Net.with_tcp_connect ~host:"www.example.com" ~service:"http" net (fun conn -> + let req = "GET / HTTP/1.1\r\nHost:www.example.com:80\r\n\r\n" in + Eio.Flow.copy_string req conn + );; ++mock-net: getaddrinfo ~service:http www.example.com ++mock-net: connect to tcp:127.0.0.1:80 ++mock-net: connect to tcp:1.2.3.4:8080 ++flow: wrote "GET / HTTP/1.1\r\n" ++ "Host:www.example.com:80\r\n" ++ "\r\n" ++flow: closed - : unit = () +``` -# Eio_main.run @@ fun env -> - Eio.Net.with_tcp_connect ~timeout:(env#clock, 0.01) ~host:"www.example.com" ~service:"http" env#net (fun conn -> - let req = "GET / HTTP/1.1\r\nHost:www.example.com:80\r\n\r\n" in - Eio.Flow.copy_string req conn; - let cs = Cstruct.create 25 in - Eio.Flow.read_exact conn cs; - Eio.Flow.close conn; - Eio.traceln "%S" (Cstruct.to_string cs));; +Both addresses fail: + +```ocaml +# Eio_mock.Backend.run @@ fun () -> + Eio_mock.Net.on_getaddrinfo net [`Return [addr1; addr2]]; + Eio_mock.Net.on_connect net [`Raise connection_failure; `Raise connection_failure]; + Eio.Net.with_tcp_connect ~host:"www.example.com" ~service:"http" net (fun _ -> assert false);; ++mock-net: getaddrinfo ~service:http www.example.com ++mock-net: connect to tcp:127.0.0.1:80 ++mock-net: connect to tcp:1.2.3.4:8080 Exception: -Eio__Net.Connection_failure - (Failure "Unable to connect to host:www.example.com, service:http"). +Eio__Net.Connection_failure (Failure "Simulated connection failure"). +``` + +First attempt times out: + +```ocaml +# Eio_mock.Backend.run @@ fun () -> + let clock = Eio_mock.Clock.make () in + let timeout = Eio.Time.Timeout.of_s clock 10. in + Eio_mock.Net.on_getaddrinfo net [`Return [addr1; addr2]]; + let mock_flow = Eio_mock.Flow.make "flow" in + Eio_mock.Net.on_connect net [`Run Fiber.await_cancel; `Return mock_flow]; + Fiber.both + (fun () -> + Eio.Net.with_tcp_connect ~timeout ~host:"www.example.com" ~service:"http" net (fun conn -> + let req = "GET / HTTP/1.1\r\nHost:www.example.com:80\r\n\r\n" in + Eio.Flow.copy_string req conn + ) + ) + (fun () -> + Eio_mock.Clock.advance clock + );; ++mock-net: getaddrinfo ~service:http www.example.com ++mock-net: connect to tcp:127.0.0.1:80 ++mock time is now 10 ++mock-net: connect to tcp:1.2.3.4:8080 ++flow: wrote "GET / HTTP/1.1\r\n" ++ "Host:www.example.com:80\r\n" ++ "\r\n" ++flow: closed +- : unit = () +``` + +Both attempts time out: + +```ocaml +# Eio_mock.Backend.run @@ fun () -> + let clock = Eio_mock.Clock.make () in + let timeout = Eio.Time.Timeout.of_s clock 10. in + Eio_mock.Net.on_getaddrinfo net [`Return [addr1; addr2]]; + Eio_mock.Net.on_connect net [`Run Fiber.await_cancel; `Run Fiber.await_cancel]; + Fiber.both + (fun () -> + Eio.Net.with_tcp_connect ~timeout ~host:"www.example.com" ~service:"http" net (fun _ -> + assert false + ) + ) + (fun () -> + Eio_mock.Clock.advance clock; + Fiber.yield (); + Fiber.yield (); + Eio_mock.Clock.advance clock + );; ++mock-net: getaddrinfo ~service:http www.example.com ++mock-net: connect to tcp:127.0.0.1:80 ++mock time is now 10 ++mock-net: connect to tcp:1.2.3.4:8080 ++mock time is now 20 +Exception: Eio__Net.Connection_failure Eio__Time.Timeout. ```