diff --git a/lib_eio/net.ml b/lib_eio/net.ml index 6094eed41..81423570c 100644 --- a/lib_eio/net.ml +++ b/lib_eio/net.ml @@ -198,6 +198,41 @@ let datagram_socket ?(reuse_addr=false) ?(reuse_port=false) ~sw (t:#t) addr = let addr = (addr :> [Sockaddr.datagram | `UdpV4 | `UdpV6]) in t#datagram_socket ~reuse_addr ~reuse_port ~sw addr +(* keep in sync with C stubs *) +type getaddrinfo_error = + | EAI_ADDRFAMILY + | EAI_AGAIN + | EAI_BADFLAGS + | EAI_BADHINTS + | EAI_FAIL + | EAI_FAMILY + | EAI_MEMORY + | EAI_NODATA + | EAI_NONAME + | EAI_OVERFLOW + | EAI_PROTOCOL + | EAI_SERVICE + | EAI_SOCKTYPE + | EAI_SYSTEM + +exception Getaddrinfo_error of getaddrinfo_error + +let getaddrinfo_error_to_string = function + | EAI_ADDRFAMILY -> "address family for name not supported" + | EAI_AGAIN -> "temporary failure in name resolution" + | EAI_BADFLAGS -> "invalid value for ai_flags" + | EAI_BADHINTS -> "invalid value for hints" + | EAI_FAIL -> "non-recoverable failure in name resolution" + | EAI_FAMILY -> "ai_family not supported" + | EAI_MEMORY -> "memory allocation failure" + | EAI_NODATA -> "no address associated with name" + | EAI_NONAME -> "name or service is not known" + | EAI_OVERFLOW -> "argument buffer overflow" + | EAI_PROTOCOL -> "resolved protocol is unknown" + | EAI_SERVICE -> "service not supported for ai_socktype" + | EAI_SOCKTYPE -> "ai_socktype not supported" + | EAI_SYSTEM -> "system error" + let getaddrinfo ?(service="") (t:#t) hostname = t#getaddrinfo ~service hostname let getaddrinfo_stream ?service t hostname = diff --git a/lib_eio/net.mli b/lib_eio/net.mli index eab8cf6ae..b8c7d46e7 100644 --- a/lib_eio/net.mli +++ b/lib_eio/net.mli @@ -220,11 +220,40 @@ val recv : #datagram_socket -> Cstruct.t -> Sockaddr.datagram * int returned along with the sender address and port. If the [buf] is too small then excess bytes may be discarded depending on the type of the socket the message is received from. *) -(** {2 DNS queries} *) +(** {2 Getaddrinfo queries} + + Note that unlike {!Unix.getaddrinfo}, EIO's [getaddrinfo] family + of functions raise an exception {!Getaddrinfo_error} with an error + code instead of returning an empty list. *) + +(* keep in sync with C stubs *) +type getaddrinfo_error = + | EAI_ADDRFAMILY + | EAI_AGAIN + | EAI_BADFLAGS + | EAI_BADHINTS + | EAI_FAIL + | EAI_FAMILY + | EAI_MEMORY + | EAI_NODATA + | EAI_NONAME + | EAI_OVERFLOW + | EAI_PROTOCOL + | EAI_SERVICE + | EAI_SOCKTYPE + | EAI_SYSTEM + (** Possible errors raised by getaddrinfo functions, check + getaddrinfo(3) and gai_strerror(3) for more information. *) + +exception Getaddrinfo_error of getaddrinfo_error + +val getaddrinfo_error_to_string : getaddrinfo_error -> string +(** [getaddrinfo_error_to_string e] returns a string representation of [e], like gai_strerror(3). *) val getaddrinfo: ?service:string -> #t -> string -> Sockaddr.t list (** [getaddrinfo ?service t node] returns a list of IP addresses for [node]. [node] is either a domain name or an IP address. + May raise {!Getaddrinfo_error} and never returns an empty list. @param service is a human friendly textual name for internet services assigned by IANA., eg. 'http', 'https', 'ftp', etc. @@ -232,10 +261,12 @@ val getaddrinfo: ?service:string -> #t -> string -> Sockaddr.t list For a more thorough treatment, see {{:https://man7.org/linux/man-pages/man3/getaddrinfo.3.html} getaddrinfo}. *) val getaddrinfo_stream: ?service:string -> #t -> string -> Sockaddr.stream list -(** [getaddrinfo_stream] is like {!getaddrinfo}, but filters out non-stream protocols. *) +(** [getaddrinfo_stream] is like {!getaddrinfo}, but filters out non-stream protocols. + May raise {!Getaddrinfo_error} and never returns an empty list. *) val getaddrinfo_datagram: ?service:string -> #t -> string -> Sockaddr.datagram list -(** [getaddrinfo_datagram] is like {!getaddrinfo}, but filters out non-datagram protocols. *) +(** [getaddrinfo_datagram] is like {!getaddrinfo}, but filters out non-datagram protocols. + May raise {!Getaddrinfo_error} and never returns an empty list. *) val getnameinfo : #t -> Sockaddr.t -> (string * string) (** [getnameinfo t sockaddr] is [(hostname, service)] corresponding to [sockaddr]. [hostname] is the diff --git a/lib_eio_linux/dune b/lib_eio_linux/dune index afdc13566..e43853e3e 100644 --- a/lib_eio_linux/dune +++ b/lib_eio_linux/dune @@ -6,4 +6,7 @@ (language c) (flags :standard -D_LARGEFILE64_SOURCE) (names eio_stubs)) + (foreign_stubs + (language c) + (names getaddrinfo_stubs)) (libraries eio eio.utils eio.unix uring logs fmt)) diff --git a/lib_eio_linux/eio_linux.ml b/lib_eio_linux/eio_linux.ml index 9ffa440fd..0902c374f 100644 --- a/lib_eio_linux/eio_linux.ml +++ b/lib_eio_linux/eio_linux.ml @@ -858,6 +858,10 @@ module Low_level = struct external eio_getdents : Unix.file_descr -> string list = "caml_eio_getdents" + external eio_getaddrinfo : string -> string -> Unix.getaddrinfo_option list -> + (Unix.addr_info list, Eio.Net.getaddrinfo_error) result + = "caml_eio_getaddrinfo" + let getrandom { Cstruct.buffer; off; len } = let rec loop n = if n = len then @@ -954,7 +958,9 @@ module Low_level = struct | _ -> None in Eio_unix.run_in_systhread @@ fun () -> - Unix.getaddrinfo node service [] + (match (eio_getaddrinfo node service []) with + | Ok l -> l + | Error e -> raise (Eio.Net.Getaddrinfo_error e)) |> List.filter_map to_eio_sockaddr_t end diff --git a/lib_eio_linux/eio_linux.mli b/lib_eio_linux/eio_linux.mli index 659797e9c..a3bfd7424 100644 --- a/lib_eio_linux/eio_linux.mli +++ b/lib_eio_linux/eio_linux.mli @@ -251,4 +251,7 @@ module Low_level : sig (** [getaddrinfo host] returns a list of IP addresses for [host]. [host] is either a domain name or an ipaddress. *) + val eio_getaddrinfo : string -> string -> Unix.getaddrinfo_option list -> + (Unix.addr_info list, Eio.Net.getaddrinfo_error) result + end diff --git a/lib_eio_linux/getaddrinfo_stubs.c b/lib_eio_linux/getaddrinfo_stubs.c new file mode 100644 index 000000000..3cbed9f12 --- /dev/null +++ b/lib_eio_linux/getaddrinfo_stubs.c @@ -0,0 +1,181 @@ +/**************************************************************************/ +/* */ +/* OCaml */ +/* */ +/* Xavier Leroy, projet Cristal, INRIA Rocquencourt, */ +/* Christiano Haesbaert, Tarides */ +/* Copyright 2004 Institut National de Recherche en Informatique et */ +/* en Automatique. */ +/* Copyright 2022 Tarides */ +/* */ +/* All rights reserved. This file is distributed under the terms of */ +/* the GNU Lesser General Public License version 2.1, with the */ +/* special exception on linking described in the file LICENSE. */ +/* */ +/**************************************************************************/ + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +extern value caml_unix_cst_to_constr(int, int *, int, int); +extern int caml_unix_socket_domain_table[]; /* from socket.c */ +extern int caml_unix_socket_type_table[]; /* from socket.c */ + +static value convert_addrinfo(struct addrinfo * a) +{ + CAMLparam0(); + CAMLlocal3(vres,vaddr,vcanonname); + union sock_addr_union sa; + socklen_param_type len; + + len = a->ai_addrlen; + if (len > sizeof(sa)) len = sizeof(sa); + memcpy(&sa.s_gen, a->ai_addr, len); + vaddr = caml_unix_alloc_sockaddr(&sa, len, -1); + vcanonname = caml_copy_string(a->ai_canonname == NULL ? "" : a->ai_canonname); + vres = caml_alloc_small(5, 0); + Field(vres, 0) = + caml_unix_cst_to_constr(a->ai_family, caml_unix_socket_domain_table, 3, 0); + Field(vres, 1) = + caml_unix_cst_to_constr(a->ai_socktype, caml_unix_socket_type_table, 4, 0); + Field(vres, 2) = Val_int(a->ai_protocol); + Field(vres, 3) = vaddr; + Field(vres, 4) = vcanonname; + CAMLreturn(vres); +} + +/* glibc doesn't define a bunch of EAI_, so fake one since code gets copied around */ + +#ifndef EAI_ADDRFAMILY +#define EAI_ADDRFAMILY (-3000) +#endif /* EAI_ADDRFAMILY */ + +#ifndef EAI_BADHINTS +#define EAI_BADHINTS (-3013) +#endif /* EAI_BADHINTS */ + +#ifndef EAI_NODATA +#define EAI_NODATA (-3007) +#endif /* EAI_NODATA */ + +#ifndef EAI_OVERFLOW +#define EAI_OVERFLOW (-3009) +#endif /* EAI_OVERFLOW */ + +#ifndef EAI_PROTOCOL +#define EAI_PROTOCOL (-3014) +#endif /* EAI_PROTOCOL */ + +static int gai_errors[] = { + EAI_ADDRFAMILY, + EAI_AGAIN, + EAI_BADFLAGS, + EAI_BADHINTS, + EAI_FAIL, + EAI_FAMILY, + EAI_MEMORY, + EAI_NODATA, + EAI_NONAME, + EAI_OVERFLOW, + EAI_PROTOCOL, + EAI_SERVICE, + EAI_SOCKTYPE, + EAI_SYSTEM /* NOTE: must be last */ +}; + +#define nmemb_gai_errors (sizeof(gai_errors) / sizeof(int)) + +CAMLprim value caml_eio_getaddrinfo(value vnode, value vserv, value vopts) +{ + CAMLparam3(vnode, vserv, vopts); + CAMLlocal4(vres, v, e, vret); + char * node, * serv; + struct addrinfo hints; + struct addrinfo * res, * r; + int retcode, i; + + if (! (caml_string_is_c_safe(vnode) && caml_string_is_c_safe(vserv))) + CAMLreturn (Val_emptylist); + + /* Extract "node" parameter */ + if (caml_string_length(vnode) == 0) { + node = NULL; + } else { + node = caml_stat_strdup(String_val(vnode)); + } + /* Extract "service" parameter */ + if (caml_string_length(vserv) == 0) { + serv = NULL; + } else { + serv = caml_stat_strdup(String_val(vserv)); + } + /* Parse options, set hints */ + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + for (/*nothing*/; vopts != Val_emptylist; vopts = Field(vopts, 1)) { + v = Field(vopts, 0); + if (Is_block(v)) + switch (Tag_val(v)) { + case 0: /* AI_FAMILY of socket_domain */ + hints.ai_family = caml_unix_socket_domain_table[Int_val(Field(v, 0))]; + break; + case 1: /* AI_SOCKTYPE of socket_type */ + hints.ai_socktype = caml_unix_socket_type_table[Int_val(Field(v, 0))]; + break; + case 2: /* AI_PROTOCOL of int */ + hints.ai_protocol = Int_val(Field(v, 0)); + break; + } + else + switch (Int_val(v)) { + case 0: /* AI_NUMERICHOST */ + hints.ai_flags |= AI_NUMERICHOST; break; + case 1: /* AI_CANONNAME */ + hints.ai_flags |= AI_CANONNAME; break; + case 2: /* AI_PASSIVE */ + hints.ai_flags |= AI_PASSIVE; break; + } + } + /* Do the call */ + caml_enter_blocking_section(); + retcode = getaddrinfo(node, serv, &hints, &res); + caml_leave_blocking_section(); + if (node != NULL) caml_stat_free(node); + if (serv != NULL) caml_stat_free(serv); + /* Convert result */ + vres = Val_emptylist; + if (retcode == 0) { + for (r = res; r != NULL; r = r->ai_next) { + e = convert_addrinfo(r); + v = caml_alloc_small(2, Tag_cons); + Field(v, 0) = e; + Field(v, 1) = vres; + vres = v; + } + vret = caml_alloc_small(1, 0); /* 0 = Ok */ + Field(vret, 0) = vres; + freeaddrinfo(res); + } else { + for (i = 0; i < nmemb_gai_errors; i++) + if (gai_errors[i] == retcode) + break; + /* Paranoia keeps the world spinning */ + if (i == nmemb_gai_errors) { + errno = EINVAL; + i = gai_errors[nmemb_gai_errors - 1]; /* EAI_SYSTEM */ + } + vret = caml_alloc_small(1, 1); /* 1 = Error */ + Field(vret, 0) = Val_int(i); + } + + CAMLreturn(vret); +} diff --git a/lib_eio_luv/eio_luv.ml b/lib_eio_luv/eio_luv.ml index 738e7aa23..3b3f3621e 100644 --- a/lib_eio_luv/eio_luv.ml +++ b/lib_eio_luv/eio_luv.ml @@ -654,10 +654,26 @@ module Low_level = struct | _ -> None in let request = Luv.DNS.Addr_info.Request.make () in - await_with_cancel ~request (fun loop -> Luv.DNS.getaddrinfo ~loop ~request ~service ~node ()) - |> or_raise - |> List.filter_map to_eio_sockaddr_t - + let r e = raise (Eio.Net.Getaddrinfo_error e) in + match (await_with_cancel ~request + (fun loop -> Luv.DNS.getaddrinfo ~loop ~request ~service ~node ())) + with + | Ok nl -> List.filter_map to_eio_sockaddr_t nl + | Error `EAI_ADDRFAMILY -> r EAI_ADDRFAMILY + | Error `EAI_AGAIN -> r EAI_AGAIN + | Error `EAI_BADFLAGS -> r EAI_BADFLAGS + | Error `EAI_BADHINTS -> r EAI_BADHINTS + | Error `EAI_CANCELED -> r EAI_FAIL (* note *) + | Error `EAI_FAIL -> r EAI_FAIL + | Error `EAI_FAMILY -> r EAI_FAMILY + | Error `EAI_MEMORY -> r EAI_MEMORY + | Error `EAI_NODATA -> r EAI_NODATA + | Error `EAI_NONAME -> r EAI_NONAME + | Error `EAI_OVERFLOW -> r EAI_OVERFLOW + | Error `EAI_PROTOCOL -> r EAI_PROTOCOL + | Error `EAI_SERVICE -> r EAI_SERVICE + | Error `EAI_SOCKTYPE -> r EAI_SOCKTYPE + | Error e -> raise (Luv_error e) end open Low_level diff --git a/tests/network.md b/tests/network.md index b2860026e..5694cb82d 100644 --- a/tests/network.md +++ b/tests/network.md @@ -545,6 +545,12 @@ Connection refused: ## Getaddrinfo +```ocaml +let guard_getaddrinfo f = + try f () + with Eio.Net.Getaddrinfo_error _ -> []; +``` + ```ocaml # Eio_main.run @@ fun env -> Eio.Net.getaddrinfo_stream env#net "127.0.0.1";; @@ -572,6 +578,7 @@ Connection refused: ```ocaml # Eio_main.run @@ fun env -> + guard_getaddrinfo @@ fun () -> Eio.Net.getaddrinfo ~service:"http" env#net "127.0.0.1";; - : Eio.Net.Sockaddr.t list = [`Tcp ("\127\000\000\001", 80); `Udp ("\127\000\000\001", 80)] @@ -580,6 +587,7 @@ Connection refused: ```ocaml # Eio_main.run @@ fun env -> + guard_getaddrinfo @@ fun () -> Eio.Net.getaddrinfo ~service:"ftp" env#net "127.0.0.1";; - : Eio.Net.Sockaddr.t list = [`Tcp ("\127\000\000\001", 21); `Udp ("\127\000\000\001", 21)] @@ -588,6 +596,7 @@ Connection refused: ```ocaml # Eio_main.run @@ fun env -> + guard_getaddrinfo @@ fun () -> Eio.Net.getaddrinfo ~service:"https" env#net "google.com";; - : Eio.Net.Sockaddr.t list = [`Tcp ("Ø:ÔÎ", 443); `Udp ("Ø:ÔÎ", 443); @@ -595,6 +604,17 @@ Connection refused: `Udp ("*\000\020P@\t\b \000\000\000\000\000\000 \014", 443)] ``` +getaddrinfo raises instead of returning an empty list: + +```ocaml +# Eio_main.run @@ fun env -> + try + Eio.Net.getaddrinfo env#net "el.dud.er.in.no" |> ignore; + traceln "getaddrinfo did not raise as expected" + with Eio.Net.Getaddrinfo_error _ -> ();; +- : unit = () +``` + ## getnameinfo ```ocaml