diff --git a/CMakeLists.txt b/CMakeLists.txt index 01c1c5d1..2e0e7ff9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -224,8 +224,11 @@ CHECK_INCLUDE_FILES (sys/stat.h HAVE_SYS_STAT_H) CHECK_INCLUDE_FILES (sys/time.h HAVE_SYS_TIME_H) CHECK_INCLUDE_FILES (sys/uio.h HAVE_SYS_UIO_H) CHECK_INCLUDE_FILES (sys/random.h HAVE_SYS_RANDOM_H) +CHECK_INCLUDE_FILES (sys/event.h HAVE_SYS_EVENT_H) +CHECK_INCLUDE_FILES (sys/epoll.h HAVE_SYS_EPOLL_H) CHECK_INCLUDE_FILES (ifaddrs.h HAVE_IFADDRS_H) CHECK_INCLUDE_FILES (time.h HAVE_TIME_H) +CHECK_INCLUDE_FILES (poll.h HAVE_POLL_H) CHECK_INCLUDE_FILES (dlfcn.h HAVE_DLFCN_H) CHECK_INCLUDE_FILES (unistd.h HAVE_UNISTD_H) # On OpenBSD, you must include sys/types.h before netinet/tcp.h @@ -243,8 +246,14 @@ CHECK_INCLUDE_FILES ("winsock2.h;windows.h" HAVE_WINSOCK2_H) CHECK_INCLUDE_FILES ("winsock2.h;ws2tcpip.h;windows.h" HAVE_WS2TCPIP_H) CHECK_INCLUDE_FILES ("winsock2.h;iphlpapi.h;windows.h" HAVE_IPHLPAPI_H) CHECK_INCLUDE_FILES ("winsock2.h;netioapi.h;windows.h" HAVE_NETIOAPI_H) +CHECK_INCLUDE_FILES ("winsock2.h;mswsock.h;windows.h" HAVE_MSWSOCK_H) CHECK_INCLUDE_FILES ("winsock.h;windows.h" HAVE_WINSOCK_H) CHECK_INCLUDE_FILES (windows.h HAVE_WINDOWS_H) +CHECK_INCLUDE_FILES ("windows.h;winternl.h" HAVE_WINTERNL_H) +CHECK_INCLUDE_FILES ("windows.h;ntdef.h" HAVE_NTDEF_H) +CHECK_INCLUDE_FILES ("windows.h;ntdef.h;ntstatus.h" HAVE_NTSTATUS_H) + + ENDIF () # Set system-specific compiler flags @@ -324,7 +333,10 @@ CARES_EXTRAINCLUDE_IFSET (HAVE_SYS_TIME_H sys/time.h) CARES_EXTRAINCLUDE_IFSET (HAVE_SYS_STAT_H sys/stat.h) CARES_EXTRAINCLUDE_IFSET (HAVE_SYS_UIO_H sys/uio.h) CARES_EXTRAINCLUDE_IFSET (HAVE_SYS_RANDOM_H sys/random.h) +CARES_EXTRAINCLUDE_IFSET (HAVE_SYS_EVENT_H sys/event.h) +CARES_EXTRAINCLUDE_IFSET (HAVE_SYS_EPOLL_H sys/epoll.h) CARES_EXTRAINCLUDE_IFSET (HAVE_TIME_H time.h) +CARES_EXTRAINCLUDE_IFSET (HAVE_POLL_H poll.h) CARES_EXTRAINCLUDE_IFSET (HAVE_FCNTL_H fcntl.h) CARES_EXTRAINCLUDE_IFSET (HAVE_UNISTD_H unistd.h) CARES_EXTRAINCLUDE_IFSET (HAVE_WINSOCK2_H winsock2.h) @@ -431,6 +443,12 @@ CHECK_SYMBOL_EXISTS (writev "${CMAKE_EXTRA_INCLUDE_FILES}" HAVE_WRITEV) CHECK_SYMBOL_EXISTS (arc4random_buf "${CMAKE_EXTRA_INCLUDE_FILES}" HAVE_ARC4RANDOM_BUF) CHECK_SYMBOL_EXISTS (stat "${CMAKE_EXTRA_INCLUDE_FILES}" HAVE_STAT) CHECK_SYMBOL_EXISTS (getifaddrs "${CMAKE_EXTRA_INCLUDE_FILES}" HAVE_GETIFADDRS) +CHECK_SYMBOL_EXISTS (poll "${CMAKE_EXTRA_INCLUDE_FILES}" HAVE_POLL) +CHECK_SYMBOL_EXISTS (pipe "${CMAKE_EXTRA_INCLUDE_FILES}" HAVE_PIPE) +CHECK_SYMBOL_EXISTS (pipe2 "${CMAKE_EXTRA_INCLUDE_FILES}" HAVE_PIPE2) +CHECK_SYMBOL_EXISTS (kqueue "${CMAKE_EXTRA_INCLUDE_FILES}" HAVE_KQUEUE) +CHECK_SYMBOL_EXISTS (epoll_create1 "${CMAKE_EXTRA_INCLUDE_FILES}" HAVE_EPOLL) + # On Android, the system headers may define __system_property_get(), but excluded # from libc. We need to perform a link test instead of a header/symbol test. diff --git a/configure.ac b/configure.ac index 65ced501..4ffc3b16 100644 --- a/configure.ac +++ b/configure.ac @@ -299,7 +299,11 @@ if test "$ac_cv_native_windows" = "yes" ; then ws2tcpip.h \ iphlpapi.h \ netioapi.h \ - ws2ipdef.h, + ws2ipdef.h \ + winternl.h \ + ntdef.h \ + ntstatus.h \ + mswsock.h, [], [], [-]) dnl Windows builds require linking to iphlpapi @@ -424,6 +428,8 @@ AC_CHECK_HEADERS( sys/param.h \ sys/uio.h \ sys/random.h \ + sys/event.h \ + sys/epoll.h \ assert.h \ iphlpapi.h \ netioapi.h \ @@ -438,6 +444,7 @@ AC_CHECK_HEADERS( strings.h \ stdbool.h \ time.h \ + poll.h \ limits.h \ arpa/nameser.h \ arpa/nameser_compat.h \ @@ -517,6 +524,12 @@ cares_all_includes=" #ifdef HAVE_SYS_RANDOM_H # include #endif +#ifdef HAVE_SYS_EVENT_H +# include +#endif +#ifdef HAVE_SYS_EPOLL_H +# include +#endif #ifdef HAVE_SYS_SOCKET_H # include #endif @@ -526,6 +539,9 @@ cares_all_includes=" #ifdef HAVE_FCNTL_H # include #endif +#ifdef HAVE_POLL_H +# include +#endif #ifdef HAVE_NET_IF_H # include #endif @@ -634,6 +650,11 @@ AC_CHECK_DECL(clock_gettime, [AC_DEFINE([HAVE_CLOCK_GETTIME], 1, [Define t AC_CHECK_DECL(if_indextoname, [AC_DEFINE([HAVE_IF_INDEXTONAME], 1, [Define to 1 if you have `if_indextoname`] )], [], $cares_all_includes) AC_CHECK_DECL(if_nametoindex, [AC_DEFINE([HAVE_IF_NAMETOINDEX], 1, [Define to 1 if you have `if_nametoindex`] )], [], $cares_all_includes) AC_CHECK_DECL(getifaddrs, [AC_DEFINE([HAVE_GETIFADDRS], 1, [Define to 1 if you have `getifaddrs`] )], [], $cares_all_includes) +AC_CHECK_DECL(poll, [AC_DEFINE([HAVE_POLL], 1, [Define to 1 if you have `poll`] )], [], $cares_all_includes) +AC_CHECK_DECL(pipe, [AC_DEFINE([HAVE_PIPE], 1, [Define to 1 if you have `pipe`] )], [], $cares_all_includes) +AC_CHECK_DECL(pipe2, [AC_DEFINE([HAVE_PIPE2], 1, [Define to 1 if you have `pipe2`] )], [], $cares_all_includes) +AC_CHECK_DECL(kqueue, [AC_DEFINE([HAVE_KQUEUE], 1, [Define to 1 if you have `kqueue`] )], [], $cares_all_includes) +AC_CHECK_DECL(epoll_create1, [AC_DEFINE([HAVE_EPOLL], 1, [Define to 1 if you have `epoll_{create1,ctl,wait}`])], [], $cares_all_includes) AC_CHECK_DECL(ConvertInterfaceIndexToLuid, [AC_DEFINE([HAVE_CONVERTINTERFACEINDEXTOLUID], 1, [Define to 1 if you have `ConvertInterfaceIndexToLuid`])], [], $cares_all_includes) AC_CHECK_DECL(ConvertInterfaceLuidToNameA, [AC_DEFINE([HAVE_CONVERTINTERFACELUIDTONAMEA], 1, [Define to 1 if you have `ConvertInterfaceLuidToNameA`])], [], $cares_all_includes) AC_CHECK_DECL(__system_property_get, [AC_DEFINE([HAVE___SYSTEM_PROPERTY_GET], 1, [Define to 1 if you have `__system_property_get`] )], [], $cares_all_includes) diff --git a/docs/ares_fds.3 b/docs/ares_fds.3 index 8e22aa04..5871be5a 100644 --- a/docs/ares_fds.3 +++ b/docs/ares_fds.3 @@ -4,7 +4,7 @@ .\" .TH ARES_FDS 3 "23 July 1998" .SH NAME -ares_fds \- return file descriptors to select on. Deprecated. +ares_fds \- return file descriptors to select on (deprecated) .SH SYNOPSIS .nf #include @@ -19,7 +19,8 @@ See the \fBNOTES\fP section on issues with this function and alternatives. The \fBares_fds(3)\fP function retrieves the set of file descriptors which the calling application should \fBselect(2)\fP on for reading and writing for the processing of name service queries pending on the name service channel -identified by \fIchannel\fP. +identified by \fIchannel\fP. Should not be used with \fBARES_OPT_EVENT_THREAD\fP +is passed to \fIares_init_options(3)\fP. File descriptors will be set in the file descriptor sets pointed to by \fIread_fds\fP and \fIwrite_fds\fP as appropriate. File descriptors already @@ -45,11 +46,13 @@ c-ares does not attempt to detect this condition to prevent crashes due to both implementation-defined behavior in the OS as well as integrator-controllable tunables which may impact the limits. -It is recommended to use socket state callbacks (\fBARES_OPT_SOCK_STATE_CB\fP) -registered via \fBares_init_options(3)\fP, and use more modern methods to -check for socket readable/writable state such as \fBpoll(2)\fP, \fBepoll(2)\fP, -or \fBkqueue(2)\fP. +It is recommended to use \fBARES_OPT_EVENT_THREAD\fP passed to +\fIares_init_options(3)\fP, or socket state callbacks +(\fBARES_OPT_SOCK_STATE_CB\fP) registered via \fIares_init_options(3)\fP and use +more modern methods to check for socket readable/writable state such as +\fIpoll(2)\fP, \fIepoll(2)\fP, or \fIkqueue(2)\fP. .SH SEE ALSO +.BR ares_init_options (3), .BR ares_timeout (3), .BR ares_process (3) .SH AUTHOR diff --git a/docs/ares_getaddrinfo.3 b/docs/ares_getaddrinfo.3 index 27585aa3..234e6568 100644 --- a/docs/ares_getaddrinfo.3 +++ b/docs/ares_getaddrinfo.3 @@ -1,18 +1,5 @@ .\" .\" Copyright 1998 by the Massachusetts Institute of Technology. -.\" -.\" Permission to use, copy, modify, and distribute this -.\" software and its documentation for any purpose and without -.\" fee is hereby granted, provided that the above copyright -.\" notice appear in all copies and that both that copyright -.\" notice and this permission notice appear in supporting -.\" documentation, and that the name of M.I.T. not be used in -.\" advertising or publicity pertaining to distribution of the -.\" software without specific, written prior permission. -.\" M.I.T. makes no representations about the suitability of -.\" this software for any purpose. It is provided "as is" -.\" without express or implied warranty. -.\" .\" SPDX-License-Identifier: MIT .\" .TH ARES_GETADDRINFO 3 "4 November 2018" @@ -91,11 +78,15 @@ Completion or failure of the query may happen immediately, or may happen during a later call to \fIares_process(3)\fP, \fIares_destroy(3)\fP or \fIares_cancel(3)\fP. .PP -If this is called from a thread other than which the main program event loop is -running, care needs to be taken to ensure any file descriptor lists are updated -immediately within the eventloop. When the associated callback is called, -it is called with a channel lock so care must be taken to ensure any processing -is minimal to prevent DNS channel stalls. +When the associated callback is called, it is called with a channel lock so care +must be taken to ensure any processing is minimal to prevent DNS channel stalls. + +The callback may be triggered from a different thread than the one which +called \fIares_getaddrinfo(3)\fP. + +For integrators running their own event loops and not using \fBARES_OPT_EVENT_THREAD\fP, +care needs to be taken to ensure any file descriptor lists are updated immediately +within the eventloop when notified. .PP The callback argument .I arg diff --git a/docs/ares_gethostbyaddr.3 b/docs/ares_gethostbyaddr.3 index 4a250505..8d79d903 100644 --- a/docs/ares_gethostbyaddr.3 +++ b/docs/ares_gethostbyaddr.3 @@ -1,18 +1,5 @@ .\" .\" Copyright 1998 by the Massachusetts Institute of Technology. -.\" -.\" Permission to use, copy, modify, and distribute this -.\" software and its documentation for any purpose and without -.\" fee is hereby granted, provided that the above copyright -.\" notice appear in all copies and that both that copyright -.\" notice and this permission notice appear in supporting -.\" documentation, and that the name of M.I.T. not be used in -.\" advertising or publicity pertaining to distribution of the -.\" software without specific, written prior permission. -.\" M.I.T. makes no representations about the suitability of -.\" this software for any purpose. It is provided "as is" -.\" without express or implied warranty. -.\" .\" SPDX-License-Identifier: MIT .\" .TH ARES_GETHOSTBYADDR 3 "24 July 1998" @@ -99,11 +86,15 @@ did not complete successfully, will be .BR NULL . .PP -If this is called from a thread other than which the main program event loop is -running, care needs to be taken to ensure any file descriptor lists are updated -immediately within the eventloop. When the associated callback is called, -it is called with a channel lock so care must be taken to ensure any processing -is minimal to prevent DNS channel stalls. +When the associated callback is called, it is called with a channel lock so care +must be taken to ensure any processing is minimal to prevent DNS channel stalls. + +The callback may be triggered from a different thread than the one which +called \fIares_gethostbyaddr(3)\fP. + +For integrators running their own event loops and not using \fBARES_OPT_EVENT_THREAD\fP, +care needs to be taken to ensure any file descriptor lists are updated immediately +within the eventloop when notified. .SH SEE ALSO .BR ares_process (3), .BR ares_gethostbyname (3) diff --git a/docs/ares_gethostbyname.3 b/docs/ares_gethostbyname.3 index e6d302d0..1067ac11 100644 --- a/docs/ares_gethostbyname.3 +++ b/docs/ares_gethostbyname.3 @@ -1,18 +1,5 @@ .\" .\" Copyright 1998 by the Massachusetts Institute of Technology. -.\" -.\" Permission to use, copy, modify, and distribute this -.\" software and its documentation for any purpose and without -.\" fee is hereby granted, provided that the above copyright -.\" notice appear in all copies and that both that copyright -.\" notice and this permission notice appear in supporting -.\" documentation, and that the name of M.I.T. not be used in -.\" advertising or publicity pertaining to distribution of the -.\" software without specific, written prior permission. -.\" M.I.T. makes no representations about the suitability of -.\" this software for any purpose. It is provided "as is" -.\" without express or implied warranty. -.\" .\" SPDX-License-Identifier: MIT .\" .TH ARES_GETHOSTBYNAME 3 "25 July 1998" @@ -107,11 +94,15 @@ did not complete successfully, will be .BR NULL . .PP -If this is called from a thread other than which the main program event loop is -running, care needs to be taken to ensure any file descriptor lists are updated -immediately within the eventloop. When the associated callback is called, -it is called with a channel lock so care must be taken to ensure any processing -is minimal to prevent DNS channel stalls. +When the associated callback is called, it is called with a channel lock so care +must be taken to ensure any processing is minimal to prevent DNS channel stalls. + +The callback may be triggered from a different thread than the one which +called \fIares_gethostbyname(3)\fP. + +For integrators running their own event loops and not using \fBARES_OPT_EVENT_THREAD\fP, +care needs to be taken to ensure any file descriptor lists are updated immediately +within the eventloop when notified. .SH SEE ALSO .BR ares_process (3), .BR ares_gethostbyaddr (3) diff --git a/docs/ares_getnameinfo.3 b/docs/ares_getnameinfo.3 index c941a68f..b4161d40 100644 --- a/docs/ares_getnameinfo.3 +++ b/docs/ares_getnameinfo.3 @@ -1,18 +1,5 @@ .\" .\" Copyright 2005 by Dominick Meglio. -.\" -.\" Permission to use, copy, modify, and distribute this -.\" software and its documentation for any purpose and without -.\" fee is hereby granted, provided that the above copyright -.\" notice appear in all copies and that both that copyright -.\" notice and this permission notice appear in supporting -.\" documentation, and that the name of M.I.T. not be used in -.\" advertising or publicity pertaining to distribution of the -.\" software without specific, written prior permission. -.\" M.I.T. makes no representations about the suitability of -.\" this software for any purpose. It is provided "as is" -.\" without express or implied warranty. -.\" .\" SPDX-License-Identifier: MIT .\" .TH ARES_GETNAMEINFO 3 "1 May 2009" @@ -86,11 +73,15 @@ failed, the ares library will invoke \fIcallback\fP. Completion or failure of the query may happen immediately, or may happen during a later call to \fIares_process(3)\fP, \fIares_destroy(3)\fP or \fIares_cancel(3)\fP. .PP -If this is called from a thread other than which the main program event loop is -running, care needs to be taken to ensure any file descriptor lists are updated -immediately within the eventloop. When the associated callback is called, -it is called with a channel lock so care must be taken to ensure any processing -is minimal to prevent DNS channel stalls. +When the associated callback is called, it is called with a channel lock so care +must be taken to ensure any processing is minimal to prevent DNS channel stalls. + +The callback may be triggered from a different thread than the one which +called \fIares_getnameinfo(3)\fP. + +For integrators running their own event loops and not using \fBARES_OPT_EVENT_THREAD\fP, +care needs to be taken to ensure any file descriptor lists are updated immediately +within the eventloop when notified. .PP The callback argument .I arg diff --git a/docs/ares_getsock.3 b/docs/ares_getsock.3 index 6d040c03..126d7de6 100644 --- a/docs/ares_getsock.3 +++ b/docs/ares_getsock.3 @@ -1,18 +1,5 @@ .\" .\" Copyright 1998 by Daniel Stenberg -.\" -.\" Permission to use, copy, modify, and distribute this -.\" software and its documentation for any purpose and without -.\" fee is hereby granted, provided that the above copyright -.\" notice appear in all copies and that both that copyright -.\" notice and this permission notice appear in supporting -.\" documentation, and that the name of M.I.T. not be used in -.\" advertising or publicity pertaining to distribution of the -.\" software without specific, written prior permission. -.\" M.I.T. makes no representations about the suitability of -.\" this software for any purpose. It is provided "as is" -.\" without express or implied warranty. -.\" .\" SPDX-License-Identifier: MIT .\" .TH ARES_GETSOCK 3 "11 March 2010" @@ -56,9 +43,11 @@ This function was added in c-ares 1.3.1 and deprecated in c-ares 1.20.0 due to the implementation of \fBARES_OPT_MAX_UDP_QUERIES\fP which makes it likely to exceed 16 open file descriptors. -It is recommended to use socket state callbacks (\fBARES_OPT_SOCK_STATE_CB\fP) -registered via \fBares_init_options(3)\fP. +It is recommended to use \fBARES_OPT_EVENT_THREAD\fP passed to +\fIares_init_options(3)\fP or to use socket state callbacks +(\fBARES_OPT_SOCK_STATE_CB\fP) registered via \fBares_init_options(3)\fP. .SH SEE ALSO +.BR ares_init_options (3), .BR ares_timeout (3), .BR ares_fds (3), .BR ares_process (3) diff --git a/docs/ares_init_options.3 b/docs/ares_init_options.3 index 942c1462..7700defd 100644 --- a/docs/ares_init_options.3 +++ b/docs/ares_init_options.3 @@ -35,6 +35,7 @@ struct ares_options { int udp_max_queries; int maxtimeout; /* in milliseconds */ unsigned int qcache_max_ttl; /* in seconds */ + ares_evsys_t evsys; }; int ares_init_options(ares_channel_t **\fIchannelptr\fP, @@ -217,6 +218,8 @@ The value of will be passed as the .I data argument. + +Cannot be used with \fBARES_OPT_EVENT_THREAD\fP. .TP 18 .B ARES_OPT_SORTLIST .B struct apattern *\fIsortlist\fP; @@ -286,6 +289,29 @@ message. This must be a non-zero value otherwise the cache will be disabled. Choose a reasonable value for your application such as 300 (5 minutes) or 3600 (1 hour). .br +.TP 18 +.B ARES_OPT_EVENT_THREAD +.B ares_evsys_t \fIevsys\fP; +.br +Enable the built-in event thread (Recommended). Introduced in c-ares 1.26.0. +Set the \fIevsys\fP parameter to \fBARES_EVSYS_DEFAULT\fP (0). Other values are +reserved for testing and should not be used by integrators. + +This option cannot be used with the \fBARES_OPT_SOCK_STATE_CB\fP option, nor the +\fIares_set_socket_functions(3)\fP or +\fIares_set_socket_configure_callback(3)\fP functions. + +When enabled, the integrator is no longer responsible for notifying c-ares of +any events on the file descriptors, so \fIares_process(3)\fP nor +\fIares_process_fd(3)\fP should ever be called when this option is enabled. + +Use \fIares_threadsafety(3)\fP to determine if this option is available to be +used. + +Returns \fBARES_ENOTIMP\fP if this option is passed but not available, and +\fBARES_ESERVFAIL\fP if there is a critical failure during initialization of +the event thread. +.br .PP The \fIoptmask\fP parameter also includes options without a corresponding field in the diff --git a/docs/ares_send.3 b/docs/ares_send.3 index 83ddb7ee..8126647f 100644 --- a/docs/ares_send.3 +++ b/docs/ares_send.3 @@ -1,18 +1,5 @@ .\" .\" Copyright 1998 by the Massachusetts Institute of Technology. -.\" -.\" Permission to use, copy, modify, and distribute this -.\" software and its documentation for any purpose and without -.\" fee is hereby granted, provided that the above copyright -.\" notice appear in all copies and that both that copyright -.\" notice and this permission notice appear in supporting -.\" documentation, and that the name of M.I.T. not be used in -.\" advertising or publicity pertaining to distribution of the -.\" software without specific, written prior permission. -.\" M.I.T. makes no representations about the suitability of -.\" this software for any purpose. It is provided "as is" -.\" without express or implied warranty. -.\" .\" SPDX-License-Identifier: MIT .\" .TH ARES_SEND 3 "25 July 1998" @@ -44,16 +31,17 @@ to the DNS protocol. When the query is complete or has failed, the ares library will invoke .IR callback . Completion or failure of the query may happen immediately, or may -happen during a later call to -.BR ares_process (3) -or -.BR ares_destroy (3). +happen later as network events are processed. .PP -If this is called from a thread other than which the main program event loop is -running, care needs to be taken to ensure any file descriptor lists are updated -immediately within the eventloop. When the associated callback is called, -it is called with a channel lock so care must be taken to ensure any processing -is minimal to prevent DNS channel stalls. +When the associated callback is called, it is called with a channel lock so care +must be taken to ensure any processing is minimal to prevent DNS channel stalls. + +The callback may be triggered from a different thread than the one which +called \fIares_send(3)\fP. + +For integrators running their own event loops and not using \fBARES_OPT_EVENT_THREAD\fP, +care needs to be taken to ensure any file descriptor lists are updated immediately +within the eventloop when notified. .PP The callback argument .I arg diff --git a/docs/ares_set_socket_configure_callback.3 b/docs/ares_set_socket_configure_callback.3 index 1f44ae3c..f5d7bb5d 100644 --- a/docs/ares_set_socket_configure_callback.3 +++ b/docs/ares_set_socket_configure_callback.3 @@ -18,8 +18,10 @@ void ares_set_socket_configure_callback(ares_channel_t *\fIchannel\fP, .fi .SH DESCRIPTION .PP -This function sets a \fIcallback\fP in the given ares channel handle. This -callback function will be invoked after the socket has been created, but +This function sets a \fIcallback\fP in the given ares channel handle. Cannot be +used when \fBARES_OPT_EVENT_THREAD\fP is passed to \fIares_init_options(3)\fP. + +This callback function will be invoked after the socket has been created, but before it has been connected to the remote server, which is an ideal time to configure various socket options. The callback must return ARES_SUCCESS if things are fine, or return -1 to signal an error. A returned error will diff --git a/docs/ares_set_socket_functions.3 b/docs/ares_set_socket_functions.3 index a1d9f774..c92934ba 100644 --- a/docs/ares_set_socket_functions.3 +++ b/docs/ares_set_socket_functions.3 @@ -23,6 +23,8 @@ void ares_set_socket_functions(ares_channel_t *\fIchannel\fP, .SH DESCRIPTION .PP This function sets a set of callback \fIfunctions\fP in the given ares channel handle. +Cannot be used when \fBARES_OPT_EVENT_THREAD\fP is passed to \fIares_init_options(3)\fP. + These callback functions will be invoked to create/destroy socket objects and perform io, instead of the normal system calls. A client application can override normal network operation fully through this functionality, and provide its own transport layer. You diff --git a/docs/ares_threadsafety.3 b/docs/ares_threadsafety.3 index 782893a5..119c79ff 100644 --- a/docs/ares_threadsafety.3 +++ b/docs/ares_threadsafety.3 @@ -17,8 +17,10 @@ thread safety enabled or not. As of c-ares 1.23.0, this simply means that every public function which references an \fIares_channel_t\fP object will lock the channel on entry and release the lock on exit of the function. This will prevent concurrent -thread access to the channel, thus ensuring no corruption can occur. Future -versions will likely implement more threading-specific features. +thread access to the channel, thus ensuring no corruption can occur. + +As of c-ares 1.26.0, this also indicates if \fBARES_OPT_EVENT_THREAD\fP can +be passed to \fIares_init_options(3)\fP. .SH RETURN VALUES \fIares_threadsafety(3)\fP can return any of the following values: diff --git a/include/ares.h b/include/ares.h index f0b3add5..99c4ec50 100644 --- a/include/ares.h +++ b/include/ares.h @@ -170,6 +170,22 @@ typedef enum { ARES_TRUE = 1 } ares_bool_t; +/*! Values for ARES_OPT_EVENT_THREAD */ +typedef enum { + /*! Default (best choice) event system */ + ARES_EVSYS_DEFAULT = 0, + /*! Win32 IOCP/AFD_POLL event system */ + ARES_EVSYS_WIN32 = 1, + /*! Linux epoll */ + ARES_EVSYS_EPOLL = 2, + /*! BSD/MacOS kqueue */ + ARES_EVSYS_KQUEUE = 3, + /*! POSIX poll() */ + ARES_EVSYS_POLL = 4, + /*! last fallback on Unix-like systems, select() */ + ARES_EVSYS_SELECT = 5 +} ares_evsys_t; + /* Flag values */ #define ARES_FLAG_USEVC (1 << 0) #define ARES_FLAG_PRIMARY (1 << 1) @@ -204,6 +220,7 @@ typedef enum { #define ARES_OPT_UDP_MAX_QUERIES (1 << 19) #define ARES_OPT_MAXTIMEOUTMS (1 << 20) #define ARES_OPT_QUERY_CACHE (1 << 21) +#define ARES_OPT_EVENT_THREAD (1 << 22) /* Nameinfo flag values */ #define ARES_NI_NOFQDN (1 << 0) @@ -316,6 +333,7 @@ struct ares_options { int udp_max_queries; int maxtimeout; /* in milliseconds */ unsigned int qcache_max_ttl; /* Maximum TTL for query cache, 0=disabled */ + ares_evsys_t evsys; }; struct hostent; diff --git a/src/lib/Makefile.inc b/src/lib/Makefile.inc index f178f1ff..29a65fd3 100644 --- a/src/lib/Makefile.inc +++ b/src/lib/Makefile.inc @@ -28,6 +28,13 @@ CSOURCES = ares__addrinfo2hostent.c \ ares_dns_parse.c \ ares_dns_record.c \ ares_dns_write.c \ + ares_event_epoll.c \ + ares_event_kqueue.c \ + ares_event_poll.c \ + ares_event_select.c \ + ares_event_thread.c \ + ares_event_wake_pipe.c \ + ares_event_win32.c \ ares_expand_name.c \ ares_expand_string.c \ ares_fds.c \ @@ -85,9 +92,12 @@ HHEADERS = ares__buf.h \ ares__iface_ips.h \ ares__llist.h \ ares__slist.h \ + ares__threads.h \ ares_android.h \ ares_data.h \ ares_dns_private.h \ + ares_event.h \ + ares_event_win32.h \ ares_getenv.h \ ares_inet_net_pton.h \ ares_ipv6.h \ diff --git a/src/lib/ares__htable.c b/src/lib/ares__htable.c index 47d0c98e..04bd8fdf 100644 --- a/src/lib/ares__htable.c +++ b/src/lib/ares__htable.c @@ -135,6 +135,35 @@ fail: return NULL; } + +const void **ares__htable_all_buckets(const ares__htable_t *htable, size_t *num) +{ + const void **out = NULL; + size_t cnt = 0; + size_t i; + + if (htable == NULL || num == NULL) + return NULL; + + *num = 0; + + out = ares_malloc_zero(sizeof(*out) * htable->num_keys); + if (out == NULL) + return NULL; + + for (i=0; isize; i++) { + ares__llist_node_t *node; + for (node = ares__llist_node_first(htable->buckets[i]); node != NULL; + node = ares__llist_node_next(node)) { + out[cnt++] = ares__llist_node_val(node); + } + } + + *num = cnt; + return out; +} + + /*! Grabs the Hashtable index from the key and length. The h index is * the hash of the function reduced to the size of the bucket list. * We are doing "hash & (size - 1)" since we are guaranteeing a power of diff --git a/src/lib/ares__htable.h b/src/lib/ares__htable.h index 89103bdd..0e83c24e 100644 --- a/src/lib/ares__htable.h +++ b/src/lib/ares__htable.h @@ -110,6 +110,18 @@ ares__htable_t *ares__htable_create(ares__htable_hashfunc_t hash_func, */ size_t ares__htable_num_keys(const ares__htable_t *htable); +/*! Retrieve an array of buckets from the hashtable. This is mainly used as + * a helper for retrieving an array of keys. + * + * \param[in] htable Initialized hashtable + * \param[out] num Count of returned buckets + * \return Array of pointers to the buckets. These are internal pointers + * to data within the hashtable, so if the key is removed, there + * will be a dangling pointer. It is expected wrappers will make + * such values safe by duplicating them. + */ +const void **ares__htable_all_buckets(const ares__htable_t *htable, size_t *num); + /*! Insert bucket into hashtable * * \param[in] htable Initialized hashtable. diff --git a/src/lib/ares__htable_asvp.c b/src/lib/ares__htable_asvp.c index f6991c3b..66c9955f 100644 --- a/src/lib/ares__htable_asvp.c +++ b/src/lib/ares__htable_asvp.c @@ -112,6 +112,39 @@ fail: return NULL; } +ares_socket_t *ares__htable_asvp_keys(const ares__htable_asvp_t *htable, size_t *num) +{ + const void **buckets = NULL; + size_t cnt = 0; + ares_socket_t *out = NULL; + size_t i; + + if (htable == NULL || num == NULL) + return NULL; + + *num = 0; + + buckets = ares__htable_all_buckets(htable->hash, &cnt); + if (buckets == NULL || cnt == 0) { + return NULL; + } + + out = ares_malloc_zero(sizeof(*out) * cnt); + if (out == NULL) { + ares_free(buckets); + return NULL; + } + + for (i=0; ikey; + } + + ares_free(buckets); + *num = cnt; + return out; +} + + ares_bool_t ares__htable_asvp_insert(ares__htable_asvp_t *htable, ares_socket_t key, void *val) { diff --git a/src/lib/ares__htable_asvp.h b/src/lib/ares__htable_asvp.h index 1ee505a2..9de81c09 100644 --- a/src/lib/ares__htable_asvp.h +++ b/src/lib/ares__htable_asvp.h @@ -68,6 +68,15 @@ void ares__htable_asvp_destroy(ares__htable_asvp_t *htable); ares__htable_asvp_t * ares__htable_asvp_create(ares__htable_asvp_val_free_t val_free); +/*! Retrieve an array of keys from the hashtable. + * + * \param[in] htable Initialized hashtable + * \param[out] num_keys Count of returned keys + * \return Array of keys in the hashtable. Must be free'd with ares_free(). + */ +ares_socket_t *ares__htable_asvp_keys(const ares__htable_asvp_t *htable, size_t *num); + + /*! Insert key/value into hash table * * \param[in] htable Initialized hash table diff --git a/src/lib/ares__socket.c b/src/lib/ares__socket.c index c379f605..ec6b745c 100644 --- a/src/lib/ares__socket.c +++ b/src/lib/ares__socket.c @@ -450,6 +450,8 @@ ares_ssize_t ares__socket_write(ares_channel_t *channel, ares_socket_t s, void ares_set_socket_callback(ares_channel_t *channel, ares_sock_create_callback cb, void *data) { + if (channel == NULL) + return; channel->sock_create_cb = cb; channel->sock_create_cb_data = data; } @@ -458,6 +460,8 @@ void ares_set_socket_configure_callback(ares_channel_t *channel, ares_sock_config_callback cb, void *data) { + if (channel == NULL || channel->optmask & ARES_OPT_EVENT_THREAD) + return; channel->sock_config_cb = cb; channel->sock_config_cb_data = data; } @@ -466,6 +470,8 @@ void ares_set_socket_functions(ares_channel_t *channel, const struct ares_socket_functions *funcs, void *data) { + if (channel == NULL || channel->optmask & ARES_OPT_EVENT_THREAD) + return; channel->sock_funcs = funcs; channel->sock_func_cb_data = data; } diff --git a/src/lib/ares__threads.c b/src/lib/ares__threads.c index fe924785..d5182535 100644 --- a/src/lib/ares__threads.c +++ b/src/lib/ares__threads.c @@ -34,7 +34,7 @@ struct ares__thread_mutex { CRITICAL_SECTION mutex; }; -static ares__thread_mutex_t *ares__thread_mutex_create(void) +ares__thread_mutex_t *ares__thread_mutex_create(void) { ares__thread_mutex_t *mut = ares_malloc_zero(sizeof(*mut)); if (mut == NULL) { @@ -45,7 +45,7 @@ static ares__thread_mutex_t *ares__thread_mutex_create(void) return mut; } -static void ares__thread_mutex_destroy(ares__thread_mutex_t *mut) +void ares__thread_mutex_destroy(ares__thread_mutex_t *mut) { if (mut == NULL) { return; @@ -54,7 +54,7 @@ static void ares__thread_mutex_destroy(ares__thread_mutex_t *mut) ares_free(mut); } -static void ares__thread_mutex_lock(ares__thread_mutex_t *mut) +void ares__thread_mutex_lock(ares__thread_mutex_t *mut) { if (mut == NULL) { return; @@ -62,7 +62,7 @@ static void ares__thread_mutex_lock(ares__thread_mutex_t *mut) EnterCriticalSection(&mut->mutex); } -static void ares__thread_mutex_unlock(ares__thread_mutex_t *mut) +void ares__thread_mutex_unlock(ares__thread_mutex_t *mut) { if (mut == NULL) { return; @@ -70,14 +70,80 @@ static void ares__thread_mutex_unlock(ares__thread_mutex_t *mut) LeaveCriticalSection(&mut->mutex); } -# else +struct ares__thread { + HANDLE thread; + DWORD id; + + void *(*func)(void *arg); + void *arg; + void *rv; +}; + +/* Wrap for pthread compatibility */ +static DWORD WINAPI ares__thread_func(LPVOID lpParameter) +{ + ares__thread_t *thread = lpParameter; + + thread->rv = thread->func(thread->arg); + return 0; +} + +ares_status_t ares__thread_create(ares__thread_t **thread, + ares__thread_func_t func, void *arg) +{ + ares__thread_t *thr = NULL; + + if (func == NULL || thread == NULL) { + return ARES_EFORMERR; + } + + thr = ares_malloc_zero(sizeof(*thr)); + if (thr == NULL) { + return ARES_ENOMEM; + } + + thr->func = func; + thr->arg = arg; + thr->thread = CreateThread(NULL, 0, ares__thread_func, thr, 0, &thr->id); + if (thr->thread == NULL) { + ares_free(thr); + return ARES_ESERVFAIL; + } + + *thread = thr; + return ARES_SUCCESS; +} + +ares_status_t ares__thread_join(ares__thread_t *thread, void **rv) +{ + ares_status_t status = ARES_SUCCESS; + + if (thread == NULL) { + return ARES_EFORMERR; + } + + if (WaitForSingleObject(thread->thread, INFINITE) != WAIT_OBJECT_0) { + status = ARES_ENOTFOUND; + } else { + CloseHandle(thread->thread); + } + + if (status == ARES_SUCCESS && rv != NULL) { + *rv = thread->rv; + } + ares_free(thread); + + return status; +} + +# else /* !WIN32 == PTHREAD */ # include struct ares__thread_mutex { pthread_mutex_t mutex; }; -static ares__thread_mutex_t *ares__thread_mutex_create(void) +ares__thread_mutex_t *ares__thread_mutex_create(void) { pthread_mutexattr_t attr; ares__thread_mutex_t *mut = ares_malloc_zero(sizeof(*mut)); @@ -107,7 +173,7 @@ fail: return NULL; } -static void ares__thread_mutex_destroy(ares__thread_mutex_t *mut) +void ares__thread_mutex_destroy(ares__thread_mutex_t *mut) { if (mut == NULL) { return; @@ -116,7 +182,7 @@ static void ares__thread_mutex_destroy(ares__thread_mutex_t *mut) ares_free(mut); } -static void ares__thread_mutex_lock(ares__thread_mutex_t *mut) +void ares__thread_mutex_lock(ares__thread_mutex_t *mut) { if (mut == NULL) { return; @@ -124,66 +190,104 @@ static void ares__thread_mutex_lock(ares__thread_mutex_t *mut) pthread_mutex_lock(&mut->mutex); } -static void ares__thread_mutex_unlock(ares__thread_mutex_t *mut) +void ares__thread_mutex_unlock(ares__thread_mutex_t *mut) { if (mut == NULL) { return; } pthread_mutex_unlock(&mut->mutex); } -# endif -ares_status_t ares__channel_threading_init(ares_channel_t *channel) +struct ares__thread { + pthread_t thread; +}; + +ares_status_t ares__thread_create(ares__thread_t **thread, + ares__thread_func_t func, void *arg) { - channel->lock = ares__thread_mutex_create(); - if (channel->lock == NULL) { + ares__thread_t *thr = NULL; + + if (func == NULL || thread == NULL) { + return ARES_EFORMERR; + } + + thr = ares_malloc_zero(sizeof(*thr)); + if (thr == NULL) { return ARES_ENOMEM; } + if (pthread_create(&thr->thread, NULL, func, arg) != 0) { + ares_free(thr); + return ARES_ESERVFAIL; + } + + *thread = thr; return ARES_SUCCESS; } -void ares__channel_threading_destroy(ares_channel_t *channel) +ares_status_t ares__thread_join(ares__thread_t *thread, void **rv) { - ares__thread_mutex_destroy(channel->lock); - channel->lock = NULL; + void *ret = NULL; + ares_status_t status = ARES_SUCCESS; + + if (thread == NULL) { + return ARES_EFORMERR; + } + + if (pthread_join(thread->thread, &ret) != 0) { + status = ARES_ENOTFOUND; + } + ares_free(thread); + + if (status == ARES_SUCCESS && rv != NULL) { + *rv = ret; + } + return status; } -void ares__channel_lock(ares_channel_t *channel) +# endif + +ares_bool_t ares_threadsafety(void) { - ares__thread_mutex_lock(channel->lock); + return ARES_TRUE; } -void ares__channel_unlock(ares_channel_t *channel) +#else /* !CARES_THREADS */ + +/* NoOp */ +ares__thread_mutex_t *ares__thread_mutex_create(void) { - ares__thread_mutex_unlock(channel->lock); + return NULL; } -ares_bool_t ares_threadsafety(void) +void ares__thread_mutex_destroy(ares__thread_mutex_t *mut) { - return ARES_TRUE; + (void)mut; } -#else -/* NoOp */ -ares_status_t ares__channel_threading_init(ares_channel_t *channel) +void ares__thread_mutex_lock(ares__thread_mutex_t *mut) { - (void)channel; - return ARES_SUCCESS; + (void)mut; } -void ares__channel_threading_destroy(ares_channel_t *channel) +void ares__thread_mutex_unlock(ares__thread_mutex_t *mut) { - (void)channel; + (void)mut; } -void ares__channel_lock(ares_channel_t *channel) +ares_status_t ares__thread_create(ares__thread_t **thread, + ares__thread_func_t func, void *arg) { - (void)channel; + (void)thread; + (void)func; + (void)arg; + return ARES_ENOTIMP; } -void ares__channel_unlock(ares_channel_t *channel) +ares_status_t ares__thread_join(ares__thread_t *thread, void **rv) { - (void)channel; + (void)thread; + (void)rv; + return ARES_ENOTIMP; } ares_bool_t ares_threadsafety(void) @@ -191,3 +295,33 @@ ares_bool_t ares_threadsafety(void) return ARES_FALSE; } #endif + + +ares_status_t ares__channel_threading_init(ares_channel_t *channel) +{ + if (!ares_threadsafety()) { + return ARES_ENOTIMP; + } + + channel->lock = ares__thread_mutex_create(); + if (channel->lock == NULL) { + return ARES_ENOMEM; + } + return ARES_SUCCESS; +} + +void ares__channel_threading_destroy(ares_channel_t *channel) +{ + ares__thread_mutex_destroy(channel->lock); + channel->lock = NULL; +} + +void ares__channel_lock(ares_channel_t *channel) +{ + ares__thread_mutex_lock(channel->lock); +} + +void ares__channel_unlock(ares_channel_t *channel) +{ + ares__thread_mutex_unlock(channel->lock); +} diff --git a/src/lib/ares__threads.h b/src/lib/ares__threads.h new file mode 100644 index 00000000..03f67f08 --- /dev/null +++ b/src/lib/ares__threads.h @@ -0,0 +1,45 @@ +/* MIT License + * + * Copyright (c) 2023 Brad House + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * SPDX-License-Identifier: MIT + */ +#ifndef __ARES__THREADS_H +#define __ARES__THREADS_H + +struct ares__thread_mutex; +typedef struct ares__thread_mutex ares__thread_mutex_t; + +ares__thread_mutex_t *ares__thread_mutex_create(void); +void ares__thread_mutex_destroy(ares__thread_mutex_t *mut); +void ares__thread_mutex_lock(ares__thread_mutex_t *mut); +void ares__thread_mutex_unlock(ares__thread_mutex_t *mut); + +struct ares__thread; +typedef struct ares__thread ares__thread_t; + +typedef void *(*ares__thread_func_t)(void *arg); +ares_status_t ares__thread_create(ares__thread_t **thread, + ares__thread_func_t func, void *arg); +ares_status_t ares__thread_join(ares__thread_t *thread, void **rv); + +#endif diff --git a/src/lib/ares_config.h.cmake b/src/lib/ares_config.h.cmake index da64de04..01dcccaa 100644 --- a/src/lib/ares_config.h.cmake +++ b/src/lib/ares_config.h.cmake @@ -76,6 +76,24 @@ /* Define to 1 if you have the header file. */ #cmakedefine HAVE_ERRNO_H +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_POLL_H + +/* Define to 1 if you have the poll function. */ +#cmakedefine HAVE_POLL + +/* Define to 1 if you have the pipe function. */ +#cmakedefine HAVE_PIPE + +/* Define to 1 if you have the pipe2 function. */ +#cmakedefine HAVE_PIPE2 + +/* Define to 1 if you have the kqueue function. */ +#cmakedefine HAVE_KQUEUE + +/* Define to 1 if you have the epoll{_create,ctl,wait} functions. */ +#cmakedefine HAVE_EPOLL + /* Define to 1 if you have the fcntl function. */ #cmakedefine HAVE_FCNTL @@ -293,6 +311,12 @@ /* Define to 1 if you have the header file. */ #cmakedefine HAVE_SYS_RANDOM_H +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_SYS_EVENT_H + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_SYS_EPOLL_H + /* Define to 1 if you have the header file. */ #cmakedefine HAVE_SYS_SELECT_H @@ -329,6 +353,18 @@ /* Define to 1 if you have the winsock.h header file. */ #cmakedefine HAVE_WINSOCK_H +/* Define to 1 if you have the mswsock.h header file. */ +#cmakedefine HAVE_MSWSOCK_H + +/* Define to 1 if you have the winternl.h header file. */ +#cmakedefine HAVE_WINTERNL_H + +/* Define to 1 if you have the ntstatus.h header file. */ +#cmakedefine HAVE_NTSTATUS_H + +/* Define to 1 if you have the ntdef.h header file. */ +#cmakedefine HAVE_NTDEF_H + /* Define to 1 if you have the writev function. */ #cmakedefine HAVE_WRITEV diff --git a/src/lib/ares_destroy.c b/src/lib/ares_destroy.c index d7cfb3bb..f2f0d9a7 100644 --- a/src/lib/ares_destroy.c +++ b/src/lib/ares_destroy.c @@ -75,6 +75,11 @@ void ares_destroy(ares_channel_t *channel) /* No more callbacks will be triggered after this point, unlock */ ares__channel_unlock(channel); + /* Shut down the event thread */ + if (channel->optmask & ARES_OPT_EVENT_THREAD) { + ares_event_thread_destroy(channel); + } + if (channel->domains) { for (i = 0; i < channel->ndomains; i++) { ares_free(channel->domains[i]); diff --git a/src/lib/ares_event.h b/src/lib/ares_event.h new file mode 100644 index 00000000..9d01d75f --- /dev/null +++ b/src/lib/ares_event.h @@ -0,0 +1,174 @@ +/* MIT License + * + * Copyright (c) 2023 Brad House + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * SPDX-License-Identifier: MIT + */ +#ifndef __ARES__EVENT_H +#define __ARES__EVENT_H + +#include "ares_setup.h" + +struct ares_event; +typedef struct ares_event ares_event_t; + +typedef enum { + ARES_EVENT_FLAG_NONE = 0, + ARES_EVENT_FLAG_READ = 1 << 0, + ARES_EVENT_FLAG_WRITE = 1 << 1, + ARES_EVENT_FLAG_OTHER = 1 << 2 +} ares_event_flags_t; + +typedef void (*ares_event_cb_t)(ares_event_thread_t *e, ares_socket_t fd, + void *data, ares_event_flags_t flags); + +typedef void (*ares_event_free_data_t)(void *data); + +typedef void (*ares_event_signal_cb_t)(const ares_event_t *event); + +struct ares_event { + /*! Registered event thread this event is bound to */ + ares_event_thread_t *e; + /*! Flags to monitor. OTHER is only allowed if the socket is ARES_SOCKET_BAD. + */ + ares_event_flags_t flags; + /*! Callback to be called when event is triggered */ + ares_event_cb_t cb; + /*! Socket to monitor, allowed to be ARES_SOCKET_BAD if not monitoring a + * socket. */ + ares_socket_t fd; + /*! Data associated with event handle that will be passed to the callback. + * Typically OS/event subsystem specific data. + * Optional, may be NULL. */ + /*! Data to be passed to callback. Optional, may be NULL. */ + void *data; + /*! When cleaning up the registered event (either when removed or during + * shutdown), this function will be called to clean up the user-supplied + * data. Optional, May be NULL. */ + ares_event_free_data_t free_data_cb; + /*! Callback to call to trigger an event. */ + ares_event_signal_cb_t signal_cb; +}; + +typedef struct { + const char *name; + ares_bool_t (*init)(ares_event_thread_t *e); + void (*destroy)(ares_event_thread_t *e); + ares_bool_t (*event_add)(ares_event_t *event); + void (*event_del)(ares_event_t *event); + void (*event_mod)(ares_event_t *event, ares_event_flags_t new_flags); + size_t (*wait)(ares_event_thread_t *e, unsigned long timeout_ms); +} ares_event_sys_t; + +struct ares_event_thread { + /*! Whether the event thread should be online or not. Checked on every wake + * event before sleeping. */ + ares_bool_t isup; + /*! Handle to the thread for joining during shutdown */ + ares__thread_t *thread; + /*! Lock to protect the data contained within the event thread itself */ + ares__thread_mutex_t *mutex; + /*! Reference to the ares channel, for being able to call things like + * ares_timeout() and ares_process_fd(). */ + ares_channel_t *channel; + /*! Not-yet-processed event handle updates. These will get enqueued by a + * thread other than the event thread itself. The event thread will then + * be woken then process these updates itself */ + ares__llist_t *ev_updates; + /*! Registered event handles. */ + ares__htable_asvp_t *ev_handles; + /*! Pointer to the event handle which is used to signal and wake the event + * thread itself. This is needed to be able to do things like update the + * file descriptors being waited on and to wake the event subsystem during + * shutdown */ + ares_event_t *ev_signal; + /* Event subsystem callbacks */ + const ares_event_sys_t *ev_sys; + /* Event subsystem private data */ + void *ev_sys_data; +}; + +/*! Queue an update for the event handle. + * + * Will search by the fd passed if not ARES_SOCKET_BAD to find a match and + * perform an update or delete (depending on flags). Otherwise will add. + * Do not use the event handle returned if its not guaranteed to be an add + * operation. + * + * \param[out] event Event handle. Optional, can be NULL. This handle + * will be invalidate quickly if the result of the + * operation is not an ADD. + * \param[in] e pointer to event thread handle + * \param[in] flags flags for the event handle. Use + * ARES_EVENT_FLAG_NONE if removing a socket from + * queue (not valid if socket is ARES_SOCKET_BAD). + * Non-socket events cannot be removed, and must have + * ARES_EVENT_FLAG_OTHER set. + * \param[in] cb Callback to call when + * event is triggered. Required. Not allowed to be + * changed, ignored on modification. + * \param[in] fd File descriptor/socket to monitor. May + * be ARES_SOCKET_BAD if not monitoring file + * descriptor. + * \param[in] data Optional. Caller-supplied data to be passed to + * callback. Only allowed on initial add, cannot be + * modified later, ignored on modification. + * \param[in] free_data_cb Optional. Callback to clean up caller-supplied + * data. Only allowed on initial add, cannot be + * modified later, ignored on modification. + * \param[in] signal_cb Optional. Callback to call to trigger an event. + * \return ARES_SUCCESS on success + */ +ares_status_t ares_event_update(ares_event_t **event, ares_event_thread_t *e, + ares_event_flags_t flags, ares_event_cb_t cb, + ares_socket_t fd, void *data, + ares_event_free_data_t free_data_cb, + ares_event_signal_cb_t signal_cb); + + +#ifdef HAVE_PIPE +ares_event_t *ares_pipeevent_create(ares_event_thread_t *e); +#endif + +#ifdef HAVE_POLL +extern const ares_event_sys_t ares_evsys_poll; +#endif + +#ifdef HAVE_KQUEUE +extern const ares_event_sys_t ares_evsys_kqueue; +#endif + +#ifdef HAVE_EPOLL +extern const ares_event_sys_t ares_evsys_epoll; +#endif + +#ifdef _WIN32 +extern const ares_event_sys_t ares_evsys_win32; +#endif + +/* All systems have select(), but not all have a way to wake, so we require + * pipe() to wake the select() */ +#ifdef HAVE_PIPE +extern const ares_event_sys_t ares_evsys_select; +#endif + +#endif diff --git a/src/lib/ares_event_epoll.c b/src/lib/ares_event_epoll.c new file mode 100644 index 00000000..3c9ccbbb --- /dev/null +++ b/src/lib/ares_event_epoll.c @@ -0,0 +1,198 @@ +/* MIT License + * + * Copyright (c) 2024 Brad House + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * SPDX-License-Identifier: MIT + */ +#include "ares_setup.h" +#include "ares.h" +#include "ares_private.h" +#include "ares_event.h" + +#ifdef HAVE_SYS_EPOLL_H +# include +#endif +#ifdef HAVE_FCNTL_H +# include +#endif + +#ifdef HAVE_EPOLL + +typedef struct { + int epoll_fd; +} ares_evsys_epoll_t; + +static void ares_evsys_epoll_destroy(ares_event_thread_t *e) +{ + ares_evsys_epoll_t *ep = NULL; + + if (e == NULL) { + return; + } + + ep = e->ev_sys_data; + if (ep == NULL) { + return; + } + + if (ep->epoll_fd != -1) { + close(ep->epoll_fd); + } + + ares_free(ep); + e->ev_sys_data = NULL; +} + +static ares_bool_t ares_evsys_epoll_init(ares_event_thread_t *e) +{ + ares_evsys_epoll_t *ep = NULL; + + ep = ares_malloc_zero(sizeof(*ep)); + if (ep == NULL) { + return ARES_FALSE; + } + + e->ev_sys_data = ep; + + ep->epoll_fd = epoll_create1(0); + if (ep->epoll_fd == -1) { + ares_evsys_epoll_destroy(e); + return ARES_FALSE; + } + +# ifdef FD_CLOEXEC + fcntl(ep->epoll_fd, F_SETFD, FD_CLOEXEC); +# endif + + e->ev_signal = ares_pipeevent_create(e); + if (e->ev_signal == NULL) { + ares_evsys_epoll_destroy(e); + return ARES_FALSE; + } + + return ARES_TRUE; +} + +static ares_bool_t ares_evsys_epoll_event_add(ares_event_t *event) +{ + ares_event_thread_t *e = event->e; + ares_evsys_epoll_t *ep = e->ev_sys_data; + struct epoll_event epev; + + memset(&epev, 0, sizeof(epev)); + epev.data.fd = event->fd; + epev.events = EPOLLRDHUP | EPOLLERR | EPOLLHUP; + if (event->flags & ARES_EVENT_FLAG_READ) { + epev.events |= EPOLLIN; + } + if (event->flags & ARES_EVENT_FLAG_WRITE) { + epev.events |= EPOLLOUT; + } + if (epoll_ctl(ep->epoll_fd, EPOLL_CTL_ADD, event->fd, &epev) != 0) { + return ARES_FALSE; + } + return ARES_TRUE; +} + +static void ares_evsys_epoll_event_del(ares_event_t *event) +{ + ares_event_thread_t *e = event->e; + ares_evsys_epoll_t *ep = e->ev_sys_data; + struct epoll_event epev; + + memset(&epev, 0, sizeof(epev)); + epev.data.fd = event->fd; + epoll_ctl(ep->epoll_fd, EPOLL_CTL_DEL, event->fd, &epev); +} + +static void ares_evsys_epoll_event_mod(ares_event_t *event, + ares_event_flags_t new_flags) +{ + ares_event_thread_t *e = event->e; + ares_evsys_epoll_t *ep = e->ev_sys_data; + struct epoll_event epev; + + memset(&epev, 0, sizeof(epev)); + epev.data.fd = event->fd; + epev.events = EPOLLRDHUP | EPOLLERR | EPOLLHUP; + if (new_flags & ARES_EVENT_FLAG_READ) { + epev.events |= EPOLLIN; + } + if (new_flags & ARES_EVENT_FLAG_WRITE) { + epev.events |= EPOLLOUT; + } + epoll_ctl(ep->epoll_fd, EPOLL_CTL_MOD, event->fd, &epev); +} + +static size_t ares_evsys_epoll_wait(ares_event_thread_t *e, + unsigned long timeout_ms) +{ + struct epoll_event events[8]; + size_t nevents = sizeof(events) / sizeof(*events); + ares_evsys_epoll_t *ep = e->ev_sys_data; + int rv; + size_t i; + size_t cnt = 0; + + memset(events, 0, sizeof(events)); + + rv = epoll_wait(ep->epoll_fd, events, (int)nevents, + (timeout_ms == 0) ? -1 : (int)timeout_ms); + if (rv < 0) { + return 0; + } + + nevents = (size_t)rv; + + for (i = 0; i < nevents; i++) { + ares_event_t *ev; + ares_event_flags_t flags = 0; + + ev = ares__htable_asvp_get_direct(e->ev_handles, + (ares_socket_t)events[i].data.fd); + if (ev == NULL || ev->cb == NULL) { + continue; + } + + cnt++; + + if (events[i].events & (EPOLLIN | EPOLLRDHUP | EPOLLHUP | EPOLLERR)) { + flags |= ARES_EVENT_FLAG_READ; + } + if (events[i].events & EPOLLOUT) { + flags |= ARES_EVENT_FLAG_WRITE; + } + + ev->cb(e, ev->fd, ev->data, flags); + } + + return cnt; +} + +const ares_event_sys_t ares_evsys_epoll = { "epoll", + ares_evsys_epoll_init, + ares_evsys_epoll_destroy, + ares_evsys_epoll_event_add, + ares_evsys_epoll_event_del, + ares_evsys_epoll_event_mod, + ares_evsys_epoll_wait }; +#endif diff --git a/src/lib/ares_event_kqueue.c b/src/lib/ares_event_kqueue.c new file mode 100644 index 00000000..944c4b00 --- /dev/null +++ b/src/lib/ares_event_kqueue.c @@ -0,0 +1,249 @@ +/* MIT License + * + * Copyright (c) 2024 Brad House + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * SPDX-License-Identifier: MIT + */ +#include "ares_setup.h" +#include "ares.h" +#include "ares_private.h" +#include "ares_event.h" + +#ifdef HAVE_SYS_TYPES_H +# include +#endif +#ifdef HAVE_SYS_EVENT_H +# include +#endif +#ifdef HAVE_SYS_TIME_H +# include +#endif +#ifdef HAVE_FCNTL_H +# include +#endif + +#ifdef HAVE_KQUEUE + +typedef struct { + int kqueue_fd; + struct kevent *changelist; + size_t nchanges; + size_t nchanges_alloc; +} ares_evsys_kqueue_t; + +static void ares_evsys_kqueue_destroy(ares_event_thread_t *e) +{ + ares_evsys_kqueue_t *kq = NULL; + + if (e == NULL) { + return; + } + + kq = e->ev_sys_data; + if (kq == NULL) { + return; + } + + if (kq->kqueue_fd != -1) { + close(kq->kqueue_fd); + } + + ares_free(kq->changelist); + ares_free(kq); + e->ev_sys_data = NULL; +} + +static ares_bool_t ares_evsys_kqueue_init(ares_event_thread_t *e) +{ + ares_evsys_kqueue_t *kq = NULL; + + kq = ares_malloc_zero(sizeof(*kq)); + if (kq == NULL) { + return ARES_FALSE; + } + + e->ev_sys_data = kq; + + kq->kqueue_fd = kqueue(); + if (kq->kqueue_fd == -1) { + ares_evsys_kqueue_destroy(e); + return ARES_FALSE; + } + +# ifdef FD_CLOEXEC + fcntl(kq->kqueue_fd, F_SETFD, FD_CLOEXEC); +# endif + + kq->nchanges_alloc = 8; + kq->changelist = + ares_malloc_zero(sizeof(*kq->changelist) * kq->nchanges_alloc); + if (kq->changelist == NULL) { + ares_evsys_kqueue_destroy(e); + return ARES_FALSE; + } + + e->ev_signal = ares_pipeevent_create(e); + if (e->ev_signal == NULL) { + ares_evsys_kqueue_destroy(e); + return ARES_FALSE; + } + + return ARES_TRUE; +} + +static void ares_evsys_kqueue_enqueue(ares_evsys_kqueue_t *kq, int fd, + int16_t filter, uint16_t flags) +{ + size_t idx; + + if (kq == NULL) { + return; + } + + idx = kq->nchanges; + + kq->nchanges++; + + if (kq->nchanges > kq->nchanges_alloc) { + kq->nchanges_alloc <<= 1; + kq->changelist = ares_realloc_zero(kq->changelist, kq->nchanges_alloc >> 1, + kq->nchanges_alloc); + } + + EV_SET(&kq->changelist[idx], fd, filter, flags, 0, 0, 0); +} + +static void ares_evsys_kqueue_event_process(ares_event_t *event, + ares_event_flags_t old_flags, + ares_event_flags_t new_flags) +{ + ares_event_thread_t *e = event->e; + ares_evsys_kqueue_t *kq; + + if (e == NULL) { + return; + } + + kq = e->ev_sys_data; + if (kq == NULL) { + return; + } + + if (new_flags & ARES_EVENT_FLAG_READ && !(old_flags & ARES_EVENT_FLAG_READ)) { + ares_evsys_kqueue_enqueue(kq, event->fd, EVFILT_READ, EV_ADD | EV_ENABLE); + } + + if (!(new_flags & ARES_EVENT_FLAG_READ) && old_flags & ARES_EVENT_FLAG_READ) { + ares_evsys_kqueue_enqueue(kq, event->fd, EVFILT_READ, EV_DELETE); + } + + if (new_flags & ARES_EVENT_FLAG_WRITE && + !(old_flags & ARES_EVENT_FLAG_WRITE)) { + ares_evsys_kqueue_enqueue(kq, event->fd, EVFILT_WRITE, EV_ADD | EV_ENABLE); + } + + if (!(new_flags & ARES_EVENT_FLAG_WRITE) && + old_flags & ARES_EVENT_FLAG_WRITE) { + ares_evsys_kqueue_enqueue(kq, event->fd, EVFILT_WRITE, EV_DELETE); + } +} + +static ares_bool_t ares_evsys_kqueue_event_add(ares_event_t *event) +{ + ares_evsys_kqueue_event_process(event, 0, event->flags); + return ARES_TRUE; +} + +static void ares_evsys_kqueue_event_del(ares_event_t *event) +{ + ares_evsys_kqueue_event_process(event, event->flags, 0); +} + +static void ares_evsys_kqueue_event_mod(ares_event_t *event, + ares_event_flags_t new_flags) +{ + ares_evsys_kqueue_event_process(event, event->flags, new_flags); +} + +static size_t ares_evsys_kqueue_wait(ares_event_thread_t *e, + unsigned long timeout_ms) +{ + struct kevent events[8]; + size_t nevents = sizeof(events) / sizeof(*events); + ares_evsys_kqueue_t *kq = e->ev_sys_data; + int rv; + size_t i; + struct timespec ts; + struct timespec *timeout = NULL; + size_t cnt = 0; + + if (timeout_ms != 0) { + ts.tv_sec = timeout_ms / 1000; + ts.tv_nsec = (timeout_ms % 1000) * 1000 * 1000; + timeout = &ts; + } + + memset(events, 0, sizeof(events)); + + rv = kevent(kq->kqueue_fd, kq->changelist, (int)kq->nchanges, events, + (int)nevents, timeout); + if (rv < 0) { + return 0; + } + + /* Changelist was consumed */ + kq->nchanges = 0; + nevents = (size_t)rv; + + for (i = 0; i < nevents; i++) { + ares_event_t *ev; + ares_event_flags_t flags = 0; + + ev = ares__htable_asvp_get_direct(e->ev_handles, + (ares_socket_t)events[i].ident); + if (ev == NULL || ev->cb == NULL) { + continue; + } + + cnt++; + + if (events[i].filter == EVFILT_READ || + events[i].flags & (EV_EOF | EV_ERROR)) { + flags |= ARES_EVENT_FLAG_READ; + } else { + flags |= ARES_EVENT_FLAG_WRITE; + } + + ev->cb(e, ev->fd, ev->data, flags); + } + + return cnt; +} + +const ares_event_sys_t ares_evsys_kqueue = { "kqueue", + ares_evsys_kqueue_init, + ares_evsys_kqueue_destroy, + ares_evsys_kqueue_event_add, + ares_evsys_kqueue_event_del, + ares_evsys_kqueue_event_mod, + ares_evsys_kqueue_wait }; +#endif diff --git a/src/lib/ares_event_poll.c b/src/lib/ares_event_poll.c new file mode 100644 index 00000000..e2b1db12 --- /dev/null +++ b/src/lib/ares_event_poll.c @@ -0,0 +1,137 @@ +/* MIT License + * + * Copyright (c) 2024 Brad House + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * SPDX-License-Identifier: MIT + */ +#include "ares_setup.h" +#include "ares.h" +#include "ares_private.h" +#include "ares_event.h" +#ifdef HAVE_POLL_H +# include +#endif + +#if defined(HAVE_POLL) + +static ares_bool_t ares_evsys_poll_init(ares_event_thread_t *e) +{ + e->ev_signal = ares_pipeevent_create(e); + if (e->ev_signal == NULL) { + return ARES_FALSE; + } + return ARES_TRUE; +} + +static void ares_evsys_poll_destroy(ares_event_thread_t *e) +{ + (void)e; +} + +static ares_bool_t ares_evsys_poll_event_add(ares_event_t *event) +{ + (void)event; + return ARES_TRUE; +} + +static void ares_evsys_poll_event_del(ares_event_t *event) +{ + (void)event; +} + +static void ares_evsys_poll_event_mod(ares_event_t *event, + ares_event_flags_t new_flags) +{ + (void)event; + (void)new_flags; +} + +static size_t ares_evsys_poll_wait(ares_event_thread_t *e, + unsigned long timeout_ms) +{ + size_t num_fds = 0; + ares_socket_t *fdlist = ares__htable_asvp_keys(e->ev_handles, &num_fds); + struct pollfd *pollfd = NULL; + int rv; + size_t cnt = 0; + size_t i; + + if (num_fds) { + pollfd = ares_malloc_zero(sizeof(*pollfd) * num_fds); + for (i = 0; i < num_fds; i++) { + ares_event_t *ev = ares__htable_asvp_get_direct(e->ev_handles, fdlist[i]); + pollfd[i].fd = ev->fd; + if (ev->flags & ARES_EVENT_FLAG_READ) { + pollfd[i].events |= POLLIN; + } + if (ev->flags & ARES_EVENT_FLAG_WRITE) { + pollfd[i].events |= POLLOUT; + } + } + } + ares_free(fdlist); + + rv = poll(pollfd, (nfds_t)num_fds, (timeout_ms == 0) ? -1 : (int)timeout_ms); + if (rv <= 0) { + goto done; + } + + for (i = 0; i < num_fds; i++) { + ares_event_t *ev; + ares_event_flags_t flags = 0; + + if (pollfd[i].revents == 0) { + continue; + } + + cnt++; + + ev = ares__htable_asvp_get_direct(e->ev_handles, pollfd[i].fd); + if (ev == NULL || ev->cb == NULL) { + continue; + } + + if (pollfd[i].revents & (POLLERR | POLLHUP | POLLIN)) { + flags |= ARES_EVENT_FLAG_READ; + } + + if (pollfd[i].revents & POLLOUT) { + flags |= ARES_EVENT_FLAG_WRITE; + } + + ev->cb(e, pollfd[i].fd, ev->data, flags); + } + +done: + ares_free(pollfd); + return cnt; +} + +const ares_event_sys_t ares_evsys_poll = { "poll", + ares_evsys_poll_init, + ares_evsys_poll_destroy, /* NoOp */ + ares_evsys_poll_event_add, /* NoOp */ + ares_evsys_poll_event_del, /* NoOp */ + ares_evsys_poll_event_mod, /* NoOp */ + ares_evsys_poll_wait }; + +#endif diff --git a/src/lib/ares_event_select.c b/src/lib/ares_event_select.c new file mode 100644 index 00000000..b73ac747 --- /dev/null +++ b/src/lib/ares_event_select.c @@ -0,0 +1,150 @@ +/* MIT License + * + * Copyright (c) 2024 Brad House + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * SPDX-License-Identifier: MIT + */ +#include "ares_setup.h" +#include "ares.h" +#include "ares_private.h" +#include "ares_event.h" +#ifdef HAVE_SYS_SELECT_H +# include +#endif + +/* All systems have select(), but not all have a way to wake, so we require + * pipe() to wake the select() */ +#if defined(HAVE_PIPE) + +static ares_bool_t ares_evsys_select_init(ares_event_thread_t *e) +{ + e->ev_signal = ares_pipeevent_create(e); + if (e->ev_signal == NULL) { + return ARES_FALSE; + } + return ARES_TRUE; +} + +static void ares_evsys_select_destroy(ares_event_thread_t *e) +{ + (void)e; +} + +static ares_bool_t ares_evsys_select_event_add(ares_event_t *event) +{ + (void)event; + return ARES_TRUE; +} + +static void ares_evsys_select_event_del(ares_event_t *event) +{ + (void)event; +} + +static void ares_evsys_select_event_mod(ares_event_t *event, + ares_event_flags_t new_flags) +{ + (void)event; + (void)new_flags; +} + +static size_t ares_evsys_select_wait(ares_event_thread_t *e, + unsigned long timeout_ms) +{ + size_t num_fds = 0; + ares_socket_t *fdlist = ares__htable_asvp_keys(e->ev_handles, &num_fds); + int rv; + size_t cnt = 0; + size_t i; + fd_set read_fds; + fd_set write_fds; + int nfds = 0; + struct timeval tv; + struct timeval *tout = NULL; + + FD_ZERO(&read_fds); + FD_ZERO(&write_fds); + + for (i = 0; i < num_fds; i++) { + ares_event_t *ev = ares__htable_asvp_get_direct(e->ev_handles, fdlist[i]); + if (ev->flags & ARES_EVENT_FLAG_READ) { + FD_SET(ev->fd, &read_fds); + } + if (ev->flags & ARES_EVENT_FLAG_WRITE) { + FD_SET(ev->fd, &write_fds); + } + if (ev->fd + 1 > nfds) { + nfds = ev->fd + 1; + } + } + + if (timeout_ms) { + tv.tv_sec = (int)(timeout_ms / 1000); + tv.tv_usec = (int)((timeout_ms % 1000) * 1000); + tout = &tv; + } + + rv = select(nfds, &read_fds, &write_fds, NULL, tout); + if (rv > 0) { + for (i = 0; i < num_fds; i++) { + ares_event_t *ev; + ares_event_flags_t flags = 0; + + ev = ares__htable_asvp_get_direct(e->ev_handles, fdlist[i]); + if (ev == NULL || ev->cb == NULL) { + continue; + } + + if (FD_ISSET(fdlist[i], &read_fds)) { + flags |= ARES_EVENT_FLAG_READ; + } + + if (FD_ISSET(fdlist[i], &write_fds)) { + flags |= ARES_EVENT_FLAG_WRITE; + } + + if (flags == 0) { + continue; + } + + cnt++; + + ev->cb(e, fdlist[i], ev->data, flags); + } + } + + ares_free(fdlist); + + return cnt; +} + +const ares_event_sys_t ares_evsys_select = { + "select", + ares_evsys_select_init, + ares_evsys_select_destroy, /* NoOp */ + ares_evsys_select_event_add, /* NoOp */ + ares_evsys_select_event_del, /* NoOp */ + ares_evsys_select_event_mod, /* NoOp */ + ares_evsys_select_wait +}; + +#endif diff --git a/src/lib/ares_event_thread.c b/src/lib/ares_event_thread.c new file mode 100644 index 00000000..6a69c8e4 --- /dev/null +++ b/src/lib/ares_event_thread.c @@ -0,0 +1,447 @@ +/* MIT License + * + * Copyright (c) 2024 Brad House + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * SPDX-License-Identifier: MIT + */ +#include "ares_setup.h" +#include "ares.h" +#include "ares_private.h" +#include "ares_event.h" + +static void ares_event_destroy_cb(void *arg) +{ + ares_event_t *event = arg; + if (event == NULL) { + return; + } + + /* Unregister from the event thread if it was registered with one */ + if (event->e) { + ares_event_thread_t *e = event->e; + e->ev_sys->event_del(event); + event->e = NULL; + } + + if (event->free_data_cb && event->data) { + event->free_data_cb(event->data); + } + + ares_free(event); +} + +/* See if a pending update already exists. We don't want to enqueue multiple + * updates for the same event handle. Right now this is O(n) based on number + * of updates already enqueued. In the future, it might make sense to make + * this O(1) with a hashtable. */ +static ares_event_t *ares_event_update_find(ares_event_thread_t *e, + ares_socket_t fd, void *data) +{ + ares__llist_node_t *node; + + for (node = ares__llist_node_first(e->ev_updates); node != NULL; + node = ares__llist_node_next(node)) { + ares_event_t *ev = ares__llist_node_val(node); + + if (fd != ARES_SOCKET_BAD && fd == ev->fd) { + return ev; + } + + if (fd == ARES_SOCKET_BAD && ev->fd == ARES_SOCKET_BAD && + data == ev->data) { + return ev; + } + } + + return NULL; +} + +ares_status_t ares_event_update(ares_event_t **event, ares_event_thread_t *e, + ares_event_flags_t flags, ares_event_cb_t cb, + ares_socket_t fd, void *data, + ares_event_free_data_t free_data_cb, + ares_event_signal_cb_t signal_cb) +{ + ares_event_t *ev = NULL; + + if (e == NULL || cb == NULL) { + return ARES_EFORMERR; + } + + if (event != NULL) { + *event = NULL; + } + + /* Validate flags */ + if (fd == ARES_SOCKET_BAD) { + if (flags & (ARES_EVENT_FLAG_READ | ARES_EVENT_FLAG_WRITE)) { + return ARES_EFORMERR; + } + if (!(flags & ARES_EVENT_FLAG_OTHER)) { + return ARES_EFORMERR; + } + } else { + if (flags & ARES_EVENT_FLAG_OTHER) { + return ARES_EFORMERR; + } + } + + /* That's all the validation we can really do */ + + /* See if we have a queued update already */ + ev = ares_event_update_find(e, fd, data); + if (ev == NULL) { + /* Allocate a new one */ + ev = ares_malloc_zero(sizeof(*ev)); + if (ev == NULL) { + return ARES_ENOMEM; + } + + if (ares__llist_insert_last(e->ev_updates, ev) == NULL) { + ares_free(ev); + return ARES_ENOMEM; + } + } + + ev->flags = flags; + ev->fd = fd; + if (ev->cb == NULL) { + ev->cb = cb; + } + if (ev->data == NULL) { + ev->data = data; + } + if (ev->free_data_cb == NULL) { + ev->free_data_cb = free_data_cb; + } + if (ev->signal_cb == NULL) { + ev->signal_cb = signal_cb; + } + + if (event != NULL) { + *event = ev; + } + + return ARES_SUCCESS; +} + +static void ares_event_signal(const ares_event_t *event) +{ + if (event == NULL || event->signal_cb == NULL) { + return; + } + event->signal_cb(event); +} + +static void ares_event_thread_wake(ares_event_thread_t *e) +{ + if (e == NULL) { + return; + } + + ares_event_signal(e->ev_signal); +} + +static void ares_event_thread_process_fd(ares_event_thread_t *e, + ares_socket_t fd, void *data, + ares_event_flags_t flags) +{ + (void)data; + + ares_process_fd(e->channel, + (flags & ARES_EVENT_FLAG_READ) ? fd : ARES_SOCKET_BAD, + (flags & ARES_EVENT_FLAG_WRITE) ? fd : ARES_SOCKET_BAD); +} + +static void ares_event_thread_sockstate_cb(void *data, ares_socket_t socket_fd, + int readable, int writable) +{ + ares_event_thread_t *e = data; + ares_event_flags_t flags = ARES_EVENT_FLAG_NONE; + + if (readable) { + flags |= ARES_EVENT_FLAG_READ; + } + + if (writable) { + flags |= ARES_EVENT_FLAG_WRITE; + } + + /* Update channel fd */ + ares__thread_mutex_lock(e->mutex); + + ares_event_update(NULL, e, flags, ares_event_thread_process_fd, socket_fd, + NULL, NULL, NULL); + + /* Wake the event thread so it properly enqueues any updates */ + ares_event_thread_wake(e); + + ares__thread_mutex_unlock(e->mutex); +} + +static void ares_event_process_updates(ares_event_thread_t *e) +{ + ares__llist_node_t *node; + + /* Iterate across all updates and apply to internal list, removing from update + * list */ + while ((node = ares__llist_node_first(e->ev_updates)) != NULL) { + ares_event_t *newev = ares__llist_node_claim(node); + ares_event_t *oldev = + ares__htable_asvp_get_direct(e->ev_handles, newev->fd); + + /* Adding new */ + if (oldev == NULL) { + newev->e = e; + /* Don't try to add a new event if all flags are cleared, that's basically + * someone trying to delete something already deleted. Also if it fails + * to add, cleanup. */ + if (newev->flags == ARES_EVENT_FLAG_NONE || + !e->ev_sys->event_add(newev)) { + newev->e = NULL; + ares_event_destroy_cb(newev); + } else { + ares__htable_asvp_insert(e->ev_handles, newev->fd, newev); + } + continue; + } + + /* Removal request */ + if (newev->flags == ARES_EVENT_FLAG_NONE) { + /* the callback for the removal will call e->ev_sys->event_del(e, event) + */ + ares__htable_asvp_remove(e->ev_handles, newev->fd); + ares_free(newev); + continue; + } + + /* Modify request -- only flags can be changed */ + e->ev_sys->event_mod(oldev, newev->flags); + oldev->flags = newev->flags; + ares_free(newev); + } +} + +static void *ares_event_thread(void *arg) +{ + ares_event_thread_t *e = arg; + ares__thread_mutex_lock(e->mutex); + + while (e->isup) { + struct timeval tv; + struct timeval *tvout; + unsigned long timeout_ms = 0; /* 0 = unlimited */ + + tvout = ares_timeout(e->channel, NULL, &tv); + if (tvout != NULL) { + timeout_ms = + (unsigned long)((tvout->tv_sec * 1000) + (tvout->tv_usec / 1000) + 1); + } + + ares_event_process_updates(e); + + /* Don't hold a mutex while waiting on events */ + ares__thread_mutex_unlock(e->mutex); + e->ev_sys->wait(e, timeout_ms); + + /* Each iteration should do timeout processing */ + if (e->isup) { + ares_process_fd(e->channel, ARES_SOCKET_BAD, ARES_SOCKET_BAD); + } + + /* Relock before we loop again */ + ares__thread_mutex_lock(e->mutex); + } + + ares__thread_mutex_unlock(e->mutex); + return NULL; +} + +static void ares_event_thread_destroy_int(ares_event_thread_t *e) +{ + ares__llist_node_t *node; + + /* Wake thread and tell it to shutdown if it exists */ + ares__thread_mutex_lock(e->mutex); + if (e->isup) { + e->isup = ARES_FALSE; + ares_event_thread_wake(e); + } + ares__thread_mutex_unlock(e->mutex); + + /* Wait for thread to shutdown */ + if (e->thread) { + ares__thread_join(e->thread, NULL); + e->thread = NULL; + } + + /* Manually free any updates that weren't processed */ + while ((node = ares__llist_node_first(e->ev_updates)) != NULL) { + ares_event_destroy_cb(ares__llist_node_claim(node)); + } + ares__llist_destroy(e->ev_updates); + e->ev_updates = NULL; + + ares__htable_asvp_destroy(e->ev_handles); + e->ev_handles = NULL; + + if (e->ev_sys->destroy) { + e->ev_sys->destroy(e); + } + + ares__thread_mutex_destroy(e->mutex); + e->mutex = NULL; + + ares_free(e); +} + +void ares_event_thread_destroy(ares_channel_t *channel) +{ + ares_event_thread_t *e = channel->sock_state_cb_data; + + if (e == NULL) { + return; + } + + ares_event_thread_destroy_int(e); +} + +static const ares_event_sys_t *ares_event_fetch_sys(ares_evsys_t evsys) +{ + switch (evsys) { + case ARES_EVSYS_WIN32: +#if defined(_WIN32) + return &ares_evsys_win32; +#else + return NULL; +#endif + + case ARES_EVSYS_EPOLL: +#if defined(HAVE_EPOLL) + return &ares_evsys_epoll; +#else + return NULL; +#endif + + case ARES_EVSYS_KQUEUE: +#if defined(HAVE_KQUEUE) + return &ares_evsys_kqueue; +#else + return NULL; +#endif + + case ARES_EVSYS_POLL: +#if defined(HAVE_POLL) + return &ares_evsys_poll; +#else + return NULL; +#endif + + case ARES_EVSYS_SELECT: +#if defined(HAVE_PIPE) + return &ares_evsys_select; +#else + return NULL; +#endif + + case ARES_EVSYS_DEFAULT: + default: +#if defined(_WIN32) + return &ares_evsys_win32; +#elif defined(HAVE_KQUEUE) + return &ares_evsys_kqueue; +#elif defined(HAVE_EPOLL) + return &ares_evsys_epoll; +#elif defined(HAVE_POLL) + return &ares_evsys_poll; +#elif defined(HAVE_PIPE) + return &ares_evsys_select; +#else + break; +#endif + } + + return NULL; +} + +ares_status_t ares_event_thread_init(ares_channel_t *channel) +{ + ares_event_thread_t *e; + + e = ares_malloc_zero(sizeof(*e)); + if (e == NULL) { + return ARES_ENOMEM; + } + + e->mutex = ares__thread_mutex_create(); + if (e->mutex == NULL) { + ares_event_thread_destroy_int(e); + return ARES_ENOMEM; + } + + e->ev_updates = ares__llist_create(NULL); + if (e->ev_updates == NULL) { + ares_event_thread_destroy_int(e); + return ARES_ENOMEM; + } + + e->ev_handles = ares__htable_asvp_create(ares_event_destroy_cb); + if (e->ev_handles == NULL) { + ares_event_thread_destroy_int(e); + return ARES_ENOMEM; + } + + e->channel = channel; + e->isup = ARES_TRUE; + e->ev_sys = ares_event_fetch_sys(channel->evsys); + if (e->ev_sys == NULL) { + ares_event_thread_destroy_int(e); + return ARES_ENOTIMP; + } + + channel->sock_state_cb = ares_event_thread_sockstate_cb; + channel->sock_state_cb_data = e; + + if (!e->ev_sys->init(e)) { + ares_event_thread_destroy_int(e); + channel->sock_state_cb = NULL; + channel->sock_state_cb_data = NULL; + return ARES_ESERVFAIL; + } + + /* Before starting the thread, process any possible events the initialization + * might have enqueued as we may actually depend on these being valid + * immediately upon return, which may mean before the thread is fully spawned + * and processed the list itself. We don't want any sort of race conditions + * (like the event system wake handle itself). */ + ares_event_process_updates(e); + + /* Start thread */ + if (ares__thread_create(&e->thread, ares_event_thread, e) != ARES_SUCCESS) { + ares_event_thread_destroy_int(e); + channel->sock_state_cb = NULL; + channel->sock_state_cb_data = NULL; + return ARES_ESERVFAIL; + } + + return ARES_SUCCESS; +} diff --git a/src/lib/ares_event_wake_pipe.c b/src/lib/ares_event_wake_pipe.c new file mode 100644 index 00000000..eca7539a --- /dev/null +++ b/src/lib/ares_event_wake_pipe.c @@ -0,0 +1,166 @@ +/* MIT License + * + * Copyright (c) 2024 Brad House + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * SPDX-License-Identifier: MIT + */ +#include "ares_setup.h" +#include "ares.h" +#include "ares_private.h" +#include "ares_event.h" +#ifdef HAVE_UNISTD_H +# include +#endif +#ifdef HAVE_FCNTL_H +# include +#endif + +#ifdef HAVE_PIPE +typedef struct { + int filedes[2]; +} ares_pipeevent_t; + +static void ares_pipeevent_destroy(ares_pipeevent_t *p) +{ + if (p->filedes[0] != -1) { + close(p->filedes[0]); + } + if (p->filedes[1] != -1) { + close(p->filedes[1]); + } + + ares_free(p); +} + +static void ares_pipeevent_destroy_cb(void *arg) +{ + ares_pipeevent_destroy(arg); +} + +static ares_pipeevent_t *ares_pipeevent_init(void) +{ + ares_pipeevent_t *p = ares_malloc_zero(sizeof(*p)); + if (p == NULL) { + return NULL; + } + + p->filedes[0] = -1; + p->filedes[1] = -1; + +# ifdef HAVE_PIPE2 + if (pipe2(p->filedes, O_NONBLOCK | O_CLOEXEC) != 0) { + ares_pipeevent_destroy(p); + return NULL; + } +# else + if (pipe(p->filedes) != 0) { + ares_pipeevent_destroy(p); + return NULL; + } + +# ifdef O_NONBLOCK + { + int val; + val = fcntl(p->filedes[0], F_GETFL, 0); + if (val >= 0) { + val |= O_NONBLOCK; + } + fcntl(p->filedes[0], F_SETFL, val); + + val = fcntl(p->filedes[1], F_GETFL, 0); + if (val >= 0) { + val |= O_NONBLOCK; + } + fcntl(p->filedes[1], F_SETFL, val); + } +# endif + +# ifdef O_CLOEXEC + fcntl(p->filedes[0], F_SETFD, O_CLOEXEC); + fcntl(p->filedes[1], F_SETFD, O_CLOEXEC); +# endif +# endif + +# ifdef F_SETNOSIGPIPE + fcntl(p->filedes[0], F_SETNOSIGPIPE, 1); + fcntl(p->filedes[1], F_SETNOSIGPIPE, 1); +# endif + + return p; +} + +static void ares_pipeevent_signal(const ares_event_t *e) +{ + ares_pipeevent_t *p; + + if (e == NULL || e->data == NULL) { + return; + } + + p = e->data; + write(p->filedes[1], "1", 1); +} + +static void ares_pipeevent_cb(ares_event_thread_t *e, ares_socket_t fd, + void *data, ares_event_flags_t flags) +{ + unsigned char buf[32]; + ares_pipeevent_t *p = NULL; + + (void)e; + (void)fd; + (void)flags; + + if (data == NULL) { + return; + } + + p = data; + + while (read(p->filedes[0], buf, sizeof(buf)) == sizeof(buf)) { + /* Do nothing */ + } +} + +ares_event_t *ares_pipeevent_create(ares_event_thread_t *e) +{ + ares_event_t *event = NULL; + ares_pipeevent_t *p = NULL; + ares_status_t status; + + p = ares_pipeevent_init(); + if (p == NULL) { + return NULL; + } + + status = ares_event_update(&event, e, ARES_EVENT_FLAG_READ, ares_pipeevent_cb, + p->filedes[0], p, ares_pipeevent_destroy_cb, + ares_pipeevent_signal); + if (status != ARES_SUCCESS) { + ares_pipeevent_destroy(p); + return NULL; + } + + return event; +} + +#endif diff --git a/src/lib/ares_event_win32.c b/src/lib/ares_event_win32.c new file mode 100644 index 00000000..718e8650 --- /dev/null +++ b/src/lib/ares_event_win32.c @@ -0,0 +1,601 @@ +/* MIT License + * + * Copyright (c) 2024 Brad House + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * SPDX-License-Identifier: MIT + */ + +#include "ares_setup.h" +#include "ares.h" +#include "ares_private.h" +#include "ares_event.h" +#include "ares_event_win32.h" +#ifdef HAVE_LIMITS_H +# include +#endif + +#ifdef _WIN32 + +/* IMPLEMENTATION NOTES + * ==================== + * + * This implementation uses some undocumented functionality within Windows for + * monitoring sockets. The Ancillary Function Driver (AFD) is the low level + * implementation that Winsock2 sits on top of. Winsock2 unfortunately does + * not expose the equivalent of epoll() or kqueue(), but it is possible to + * access AFD directly and use along with IOCP to simulate the functionality. + * We want to use IOCP if possible as it gives us the ability to monitor more + * than just sockets (WSAPoll is not an option), and perform arbitrary callbacks + * which means we can hook in non-socket related events. + * + * The information for this implementation was gathered from "wepoll" and + * "libuv" which both use slight variants on this, but this implementation + * doesn't directly follow either methodology. + * + * Initialization: + * 1. Dynamically load the NtDeviceIoControlFile and NtCancelIoFileEx internal + * symbols from ntdll.dll. These functions are used to submit the AFD POLL + * request and to cancel a prior request, respectively. + * 2. Create an IO Completion Port base handle via CreateIoCompletionPort() + * that all socket events will be delivered through. + * 3. Create a callback to be used to be able to interrupt waiting for IOCP + * events, this may be called for allowing enqueuing of additional socket + * events or removing socket events. PostQueuedCompletionStatus() is the + * obvious choice. Use the same container structure as used with a Socket + * but tagged indicating it is not as the CompletionKey (important!). + * + * Socket Add: + * 1. Create/Allocate a container for holding metadata about a socket: + * - SOCKET base_socket; + * - SOCKET peer_socket; + * - OVERLAPPED overlapped; -- Used by AFD POLL + * - AFD_POLL_INFO afd_poll_info; -- Used by AFD POLL + * 2. Call WSAIoctl(..., SIO_BASE_HANDLE, ...) to unwrap the SOCKET and get + * the "base socket" we can use for polling. It appears this may fail so + * we should call WSAIoctl(..., SIO_BSP_HANDLE_POLL, ...) as a fallback. + * 3. The SOCKET handle we have is most likely not capable of supporting + * OVERLAPPED, and we need to have a way to unbind a socket from IOCP + * (which is done via a simple closesocket()) so we need to duplicate the + * "base socket" using WSADuplicateSocketW() followed by + * WSASocketW(..., WSA_FLAG_OVERLAPPED) to create this "peer socket" for + * submitting AFD POLL requests. + * 4. Bind to IOCP using CreateIoCompletionPort() referencing the "peer + * socket" and the base IOCP handle from "Initialization". Use the + * pointer to the socket container as the "CompletionKey" which will be + * returned when an event occurs. + * 5. Submit AFD POLL request (see "AFD POLL Request" section) + * + * Socket Delete: + * 1. Call "AFD Poll Cancel" (see Section of same name) + * 2. If a cancel was requested (not bypassed due to no events, etc), tag the + * "container" for the socket as pending delete, and when the next IOCP + * event for the socket is dequeued, cleanup. + * 3. Otherwise, call closesocket(peer_socket) then free() the container + * which will officially delete it. + * NOTE: Deferring delete may be completely unnecessary. In theory closing + * the peer_socket() should guarantee no additional events will be + * delivered. But maybe if there's a pending event that hasn't been + * read yet but already trigggered it would be an issue, so this is + * "safer" unless we can prove its not necessary. + * + * Socket Modify: + * 1. Call "AFD Poll Cancel" (see Section of same name) + * 2. If a cancel was not enqueued because there is no pending request, + * submit AFD POLL request (see "AFD POLL Request" section), otherwise + * defer until next socket event. + * + * Event Wait: + * 1. Call GetQueuedCompletionStatusEx() with the base IOCP handle, a + * stack allocated array of OVERLAPPED_ENTRY's, and an appropriate + * timeout. + * 2. Iterate across returned events, the CompletionKey is a pointer to the + * container registered with CreateIoCompletionPort() or + * PostQueuedCompletionStatus() + * 3. If object indicates it is pending delete, go ahead and + * closesocket(peer_socket) and free() the container. Go to the next event. + * 4. Submit AFD POLL Request (see "AFD POLL Request"). We must re-enable + * the request each time we receive a response, it is not persistent. + * 5. Notify of any events received as indicated in the AFD_POLL_INFO + * Handles[0].Events (NOTE: check NumberOfHandles first, make sure it is + * > 0, otherwise we might not have events such as if our last request + * was cancelled). + * + * AFD Poll Request: + * 1. Initialize the AFD_POLL_INFO structure: + * Exclusive = TRUE; // Auto cancel duplicates for same socket + * NumberOfHandles = 1; + * Timeout.QuadPart = LLONG_MAX; + * Handles[0].Handle = (HANDLE)base_socket; + * Handles[0].Status = 0; + * Handles[0].Events = ... set as appropriate AFD_POLL_RECEIVE, etc; + * 2. Zero out the OVERLAPPED structure + * 3. Create an IO_STATUS_BLOCK pointer (iosb) and set it to the address of + * the OVERLAPPED "Internal" member. + * 4. Set the "Status" member of IO_STATUS_BLOCK to STATUS_PENDING + * 5. Call + * NtDeviceIoControlFile((HANDLE)peer_socket, NULL, NULL, &overlapped, + * iosb, IOCTL_AFD_POLL + * &afd_poll_info, sizeof(afd_poll_info), + * &afd_poll_info, sizeof(afd_poll_info)); + * NOTE: Its not clear to me if the IO_STATUS_BLOCK pointing to OVERLAPPED + * is for efficiency or if its a requirement for AFD. This is what + * libuv does, so I'm doing it here too. + * + * AFD Poll Cancel: + * 1. Check to see if the IO_STATUS_BLOCK "Status" member for the socket + * is still STATUS_PENDING, if not, no cancel request is necessary. + * 2. Call + * NtCancelIoFileEx((HANDLE)peer_socket, iosb, &temp_iosb); + * + * + * References: + * - https://github.com/piscisaureus/wepoll/ + * - https://github.com/libuv/libuv/ + */ + +typedef struct { + /* Dynamically loaded symbols */ + NtDeviceIoControlFile_t NtDeviceIoControlFile; + NtCancelIoFileEx_t NtCancelIoFileEx; + + /* Implementation details */ + HANDLE iocp_handle; +} ares_evsys_win32_t; + +typedef struct { + /*! Pointer to parent event container */ + ares_event_t *event; + /*! Socket passed in to monitor */ + SOCKET socket; + /*! Base socket derived from provided socket */ + SOCKET base_socket; + /*! New socket (duplicate base_socket handle) supporting OVERLAPPED operation + */ + SOCKET peer_socket; + /*! Structure for submitting AFD POLL requests (Internals!) */ + AFD_POLL_INFO afd_poll_info; + /*! Overlapped structure submitted with AFD POLL requests and returned with + * IOCP results */ + OVERLAPPED overlapped; +} ares_evsys_win32_eventdata_t; + +static void ares_iocpevent_signal(const ares_event_t *event) +{ + ares_event_thread_t *e = event->e; + ares_evsys_win32_t *ew = e->ev_sys_data; + + if (e == NULL) { + return; + } + + PostQueuedCompletionStatus(ew->iocp_handle, 0, (ULONG_PTR)event->data, NULL); +} + +static void ares_iocpevent_cb(ares_event_thread_t *e, ares_socket_t fd, + void *data, ares_event_flags_t flags) +{ + (void)e; + (void)data; + (void)fd; + (void)flags; +} + +static ares_event_t *ares_iocpevent_create(ares_event_thread_t *e) +{ + ares_event_t *event = NULL; + ares_status_t status; + + status = + ares_event_update(&event, e, ARES_EVENT_FLAG_OTHER, ares_iocpevent_cb, + ARES_SOCKET_BAD, NULL, NULL, ares_iocpevent_signal); + if (status != ARES_SUCCESS) { + return NULL; + } + + return event; +} + +static void ares_evsys_win32_destroy(ares_event_thread_t *e) +{ + ares_evsys_win32_t *ew = NULL; + + if (e == NULL) { + return; + } + + ew = e->ev_sys_data; + if (ew == NULL) { + return; + } + + if (ew->iocp_handle != NULL) { + CloseHandle(ew->iocp_handle); + } + + ares_free(ew); + e->ev_sys_data = NULL; +} + +static ares_bool_t ares_evsys_win32_init(ares_event_thread_t *e) +{ + ares_evsys_win32_t *ew = NULL; + HMODULE ntdll; + + ew = ares_malloc_zero(sizeof(*ew)); + if (ew == NULL) { + return ARES_FALSE; + } + + e->ev_sys_data = ew; + + /* All apps should have ntdll.dll already loaded, so just get a handle to + * this */ + ntdll = GetModuleHandleA("ntdll.dll"); + if (ntdll == NULL) { + goto fail; + } + + /* Load Internal symbols not typically accessible */ + ew->NtDeviceIoControlFile = (NtDeviceIoControlFile_t)(void *)GetProcAddress( + ntdll, "NtDeviceIoControlFile"); + ew->NtCancelIoFileEx = + (NtCancelIoFileEx_t)(void *)GetProcAddress(ntdll, "NtCancelIoFileEx"); + + if (ew->NtCancelIoFileEx == NULL || ew->NtDeviceIoControlFile == NULL) { + goto fail; + } + + ew->iocp_handle = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0); + if (ew->iocp_handle == NULL) { + goto fail; + } + + e->ev_signal = ares_iocpevent_create(e); + if (e->ev_signal == NULL) { + goto fail; + } + + return ARES_TRUE; + +fail: + ares_evsys_win32_destroy(e); + return ARES_FALSE; +} + +static ares_socket_t ares_evsys_win32_basesocket(ares_socket_t socket) +{ + while (1) { + DWORD bytes; /* Not used */ + ares_socket_t base_socket = ARES_SOCKET_BAD; + int rv; + + rv = WSAIoctl(socket, SIO_BASE_HANDLE, NULL, 0, &base_socket, + sizeof(base_socket), &bytes, NULL, NULL); + if (rv != SOCKET_ERROR && base_socket != ARES_SOCKET_BAD) { + socket = base_socket; + break; + } + + /* If we're here, an error occurred */ + if (GetLastError() == WSAENOTSOCK) { + /* This is critical, exit */ + return ARES_SOCKET_BAD; + } + + /* Work around known bug in Komodia based LSPs, use ARES_BSP_HANDLE_POLL + * to retrieve the underlying socket to then loop and get the base socket: + * https://docs.microsoft.com/en-us/windows/win32/winsock/winsock-ioctls + * https://www.komodia.com/newwiki/index.php?title=Komodia%27s_Redirector_bug_fixes#Version_2.2.2.6 + */ + base_socket = ARES_SOCKET_BAD; + rv = WSAIoctl(socket, SIO_BSP_HANDLE_POLL, NULL, 0, &base_socket, + sizeof(base_socket), &bytes, NULL, NULL); + + if (rv != SOCKET_ERROR && base_socket != ARES_SOCKET_BAD && + base_socket != socket) { + socket = base_socket; + continue; /* loop! */ + } + + return ARES_SOCKET_BAD; + } + + return socket; +} + +static ares_bool_t ares_evsys_win32_afd_enqueue(ares_event_t *event, + ares_event_flags_t flags) +{ + ares_event_thread_t *e = event->e; + ares_evsys_win32_t *ew = e->ev_sys_data; + ares_evsys_win32_eventdata_t *ed = event->data; + NTSTATUS status; + IO_STATUS_BLOCK *iosb_ptr; + + if (e == NULL || ed == NULL || ew == NULL) { + return ARES_FALSE; + } + + /* Enqueue AFD Poll */ + ed->afd_poll_info.Exclusive = TRUE; + ed->afd_poll_info.NumberOfHandles = 1; + ed->afd_poll_info.Timeout.QuadPart = LLONG_MAX; + ed->afd_poll_info.Handles[0].Handle = (HANDLE)ed->base_socket; + ed->afd_poll_info.Handles[0].Status = 0; + ed->afd_poll_info.Handles[0].Events = 0; + + if (flags & ARES_EVENT_FLAG_READ) { + ed->afd_poll_info.Handles[0].Events |= + (AFD_POLL_RECEIVE | AFD_POLL_DISCONNECT | AFD_POLL_ACCEPT | + AFD_POLL_ABORT); + } + if (flags & ARES_EVENT_FLAG_WRITE) { + ed->afd_poll_info.Handles[0].Events |= + (AFD_POLL_SEND | AFD_POLL_CONNECT_FAIL); + } + if (flags == 0) { + ed->afd_poll_info.Handles[0].Events |= AFD_POLL_DISCONNECT; + } + + memset(&ed->overlapped, 0, sizeof(ed->overlapped)); + iosb_ptr = (IO_STATUS_BLOCK *)&ed->overlapped.Internal; + iosb_ptr->Status = STATUS_PENDING; + + status = ew->NtDeviceIoControlFile( + (HANDLE)ed->peer_socket, NULL, NULL, &ed->overlapped, iosb_ptr, + IOCTL_AFD_POLL, &ed->afd_poll_info, sizeof(ed->afd_poll_info), + &ed->afd_poll_info, sizeof(ed->afd_poll_info)); + if (status != STATUS_SUCCESS && status != STATUS_PENDING) { + printf("%s(): failed to perform IOCTL_AFD_POLL operation\n", __FUNCTION__); + fflush(stdout); + return ARES_FALSE; + } + + return ARES_TRUE; +} + +static ares_bool_t ares_evsys_win32_afd_cancel(ares_evsys_win32_eventdata_t *ed) +{ + IO_STATUS_BLOCK *iosb_ptr; + IO_STATUS_BLOCK cancel_iosb; + ares_evsys_win32_t *ew; + NTSTATUS status; + + /* Detached due to destroy */ + if (ed->event == NULL) { + return ARES_FALSE; + } + + iosb_ptr = (IO_STATUS_BLOCK *)&ed->overlapped.Internal; + /* Not pending, nothing to do */ + if (iosb_ptr->Status != STATUS_PENDING) { + return ARES_FALSE; + } + + ew = ed->event->e->ev_sys_data; + status = + ew->NtCancelIoFileEx((HANDLE)ed->peer_socket, iosb_ptr, &cancel_iosb); + + /* NtCancelIoFileEx() may return STATUS_NOT_FOUND if the operation completed + * just before calling NtCancelIoFileEx(), but we have not yet received the + * notifiction (but it should be queued for the next IOCP event). */ + if (status == STATUS_SUCCESS || status == STATUS_NOT_FOUND) { + return ARES_TRUE; + } + + return ARES_FALSE; +} + +static void ares_evsys_win32_eventdata_destroy(ares_evsys_win32_eventdata_t *ed) +{ + if (ed == NULL) { + return; + } + + if (ed->peer_socket != ARES_SOCKET_BAD) { + closesocket(ed->peer_socket); + } + + ares_free(ed); +} + +static ares_bool_t ares_evsys_win32_event_add(ares_event_t *event) +{ + ares_event_thread_t *e = event->e; + ares_evsys_win32_t *ew = e->ev_sys_data; + ares_evsys_win32_eventdata_t *ed; + WSAPROTOCOL_INFOW protocol_info; + + ed = ares_malloc_zero(sizeof(*ed)); + ed->event = event; + ed->socket = event->fd; + ed->base_socket = ARES_SOCKET_BAD; + ed->peer_socket = ARES_SOCKET_BAD; + + /* Likely a signal event, not something we will directly handle. We create + * the ares_evsys_win32_eventdata_t as the placeholder to use as the + * IOCP Completion Key */ + if (ed->socket == ARES_SOCKET_BAD) { + event->data = ed; + return ARES_TRUE; + } + + ed->base_socket = ares_evsys_win32_basesocket(ed->socket); + if (ed->base_socket == ARES_SOCKET_BAD) { + fprintf(stderr, "%s(): could not determine base socket for fd %d\n", + __FUNCTION__, (int)event->fd); + ares_evsys_win32_eventdata_destroy(ed); + return ARES_FALSE; + } + + /* Create a peer socket that supports OVERLAPPED so we can use IOCP on the + * socket handle */ + if (WSADuplicateSocketW(ed->base_socket, GetCurrentProcessId(), + &protocol_info) != 0) { + fprintf(stderr, + "%s(): could not retrieve protocol info for creating peer socket\n", + __FUNCTION__); + ares_evsys_win32_eventdata_destroy(ed); + return ARES_FALSE; + } + + ed->peer_socket = + WSASocketW(protocol_info.iAddressFamily, protocol_info.iSocketType, + protocol_info.iProtocol, &protocol_info, 0, WSA_FLAG_OVERLAPPED); + if (ed->peer_socket == ARES_SOCKET_BAD) { + fprintf(stderr, "%s(): could not create peer socket\n", __FUNCTION__); + ares_evsys_win32_eventdata_destroy(ed); + return ARES_FALSE; + } + + SetHandleInformation((HANDLE)ed->peer_socket, HANDLE_FLAG_INHERIT, 0); + + if (CreateIoCompletionPort((HANDLE)ed->peer_socket, ew->iocp_handle, + (ULONG_PTR)ed, 0) == NULL) { + fprintf(stderr, "%s(): failed to bind peer socket to IOCP\n", __FUNCTION__); + ares_evsys_win32_eventdata_destroy(ed); + return ARES_FALSE; + } + + event->data = ed; + + if (!ares_evsys_win32_afd_enqueue(event, event->flags)) { + event->data = NULL; + ares_evsys_win32_eventdata_destroy(ed); + return ARES_FALSE; + } + + return ARES_TRUE; +} + +static void ares_evsys_win32_event_del(ares_event_t *event) +{ + ares_evsys_win32_eventdata_t *ed = event->data; + ares_event_thread_t *e = event->e; + + if (event->fd == ARES_SOCKET_BAD || !e->isup || ed == NULL || + !ares_evsys_win32_afd_cancel(ed)) { + /* Didn't need to enqueue a cancellation, for one of these reasons: + * - Not an IOCP socket + * - This is during shutdown of the event thread, no more signals can be + * delivered. + * - It has been determined there is no AFD POLL queued currently for the + * socket. + */ + ares_evsys_win32_eventdata_destroy(ed); + event->data = NULL; + } else { + /* Detach from event, so when the cancel event comes through, + * it will clean up */ + ed->event = NULL; + event->data = NULL; + } +} + +static void ares_evsys_win32_event_mod(ares_event_t *event, + ares_event_flags_t new_flags) +{ + ares_evsys_win32_eventdata_t *ed = event->data; + + /* Not for us */ + if (event->fd == ARES_SOCKET_BAD || ed == NULL) { + return; + } + + /* Try to cancel any current outstanding poll, if one is not running, + * go ahead and queue it up */ + if (!ares_evsys_win32_afd_cancel(ed)) { + ares_evsys_win32_afd_enqueue(event, new_flags); + } +} + +static size_t ares_evsys_win32_wait(ares_event_thread_t *e, + unsigned long timeout_ms) +{ + ares_evsys_win32_t *ew = e->ev_sys_data; + OVERLAPPED_ENTRY entries[16]; + ULONG nentries = sizeof(entries) / sizeof(*entries); + BOOL status; + size_t i; + size_t cnt = 0; + + status = GetQueuedCompletionStatusEx( + ew->iocp_handle, entries, nentries, &nentries, + (timeout_ms == 0) ? INFINITE : (DWORD)timeout_ms, FALSE); + + if (!status) { + return 0; + } + + for (i = 0; i < (size_t)nentries; i++) { + ares_event_flags_t flags = 0; + ares_evsys_win32_eventdata_t *ed = + (ares_evsys_win32_eventdata_t *)entries[i].lpCompletionKey; + ares_event_t *event = ed->event; + + if (ed->socket == ARES_SOCKET_BAD) { + /* Some sort of signal event */ + flags = ARES_EVENT_FLAG_OTHER; + } else { + /* Process events */ + if (ed->afd_poll_info.NumberOfHandles > 0) { + if (ed->afd_poll_info.Handles[0].Events & + (AFD_POLL_RECEIVE | AFD_POLL_DISCONNECT | AFD_POLL_ACCEPT | + AFD_POLL_ABORT)) { + flags |= ARES_EVENT_FLAG_READ; + } + if (ed->afd_poll_info.Handles[0].Events & + (AFD_POLL_SEND | AFD_POLL_CONNECT_FAIL)) { + flags |= ARES_EVENT_FLAG_WRITE; + } + + /* XXX: Handle ed->afd_poll_info.Handles[0].Events & + * AFD_POLL_LOCAL_CLOSE */ + } + + if (event == NULL) { + /* This means we need to cleanup the private event data as we've been + * detached */ + ares_evsys_win32_eventdata_destroy(ed); + } else { + /* Re-enqueue so we can get more events on the socket */ + ares_evsys_win32_afd_enqueue(event, event->flags); + } + } + + if (event != NULL && flags != 0) { + cnt++; + event->cb(e, event->fd, event->data, flags); + } + } + + return cnt; +} + +const ares_event_sys_t ares_evsys_win32 = { "win32", + ares_evsys_win32_init, + ares_evsys_win32_destroy, + ares_evsys_win32_event_add, + ares_evsys_win32_event_del, + ares_evsys_win32_event_mod, + ares_evsys_win32_wait }; +#endif diff --git a/src/lib/ares_event_win32.h b/src/lib/ares_event_win32.h new file mode 100644 index 00000000..99cd5c90 --- /dev/null +++ b/src/lib/ares_event_win32.h @@ -0,0 +1,119 @@ +/* MIT License + * + * Copyright (c) 2024 Brad House + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * SPDX-License-Identifier: MIT + */ +#ifndef __ARES_EVENT_WIN32_H +#define __ARES_EVENT_WIN32_H + +#ifdef _WIN32 +# ifdef HAVE_WINSOCK2_H +# include +# endif +# ifdef HAVE_WS2TCPIP_H +# include +# endif +# ifdef HAVE_MSWSOCK_H +# include +# endif +# ifdef HAVE_WINDOWS_H +# include +# endif + +/* From winternl.h */ + +/* If WDK is not installed and not using MinGW, provide the needed definitions + */ +typedef LONG NTSTATUS; + +typedef struct _IO_STATUS_BLOCK { + union { + NTSTATUS Status; + PVOID Pointer; + }; + + ULONG_PTR Information; +} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK; + +typedef VOID(NTAPI *PIO_APC_ROUTINE)(PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, + ULONG Reserved); + +/* From ntstatus.h */ +# define STATUS_SUCCESS ((NTSTATUS)0x00000000) +# ifndef STATUS_PENDING +# define STATUS_PENDING ((NTSTATUS)0x00000103L) +# endif +# define STATUS_CANCELLED ((NTSTATUS)0xC0000120L) +# define STATUS_NOT_FOUND ((NTSTATUS)0xC0000225L) + +/* Not sure what headers might have these */ +# define IOCTL_AFD_POLL 0x00012024 + +# define AFD_POLL_RECEIVE 0x0001 +# define AFD_POLL_RECEIVE_EXPEDITED 0x0002 +# define AFD_POLL_SEND 0x0004 +# define AFD_POLL_DISCONNECT 0x0008 +# define AFD_POLL_ABORT 0x0010 +# define AFD_POLL_LOCAL_CLOSE 0x0020 +# define AFD_POLL_ACCEPT 0x0080 +# define AFD_POLL_CONNECT_FAIL 0x0100 + +typedef struct _AFD_POLL_HANDLE_INFO { + HANDLE Handle; + ULONG Events; + NTSTATUS Status; +} AFD_POLL_HANDLE_INFO, *PAFD_POLL_HANDLE_INFO; + +typedef struct _AFD_POLL_INFO { + LARGE_INTEGER Timeout; + ULONG NumberOfHandles; + ULONG Exclusive; + AFD_POLL_HANDLE_INFO Handles[1]; +} AFD_POLL_INFO, *PAFD_POLL_INFO; + +/* Prototypes for dynamically loaded functions from ntdll.dll */ +typedef NTSTATUS(NTAPI *NtCancelIoFileEx_t)(HANDLE FileHandle, + PIO_STATUS_BLOCK IoRequestToCancel, + PIO_STATUS_BLOCK IoStatusBlock); +typedef NTSTATUS(NTAPI *NtDeviceIoControlFile_t)( + HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, ULONG IoControlCode, PVOID InputBuffer, + ULONG InputBufferLength, PVOID OutputBuffer, ULONG OutputBufferLength); + +/* On UWP/Windows Store, these definitions aren't there for some reason */ +# ifndef SIO_BSP_HANDLE_POLL +# define SIO_BSP_HANDLE_POLL 0x4800001D +# endif + +# ifndef SIO_BASE_HANDLE +# define SIO_BASE_HANDLE 0x48000022 +# endif + +# ifndef HANDLE_FLAG_INHERIT +# define HANDLE_FLAG_INHERIT 0x00000001 +# endif + +#endif /* _WIN32 */ + +#endif /* __ARES_EVENT_WIN32_H */ diff --git a/src/lib/ares_init.c b/src/lib/ares_init.c index fab8c04d..014226f3 100644 --- a/src/lib/ares_init.c +++ b/src/lib/ares_init.c @@ -389,6 +389,14 @@ int ares_init_options(ares_channel_t **channelptr, ares_strerror(status))); } + /* Initialize the event thread */ + if (channel->optmask & ARES_OPT_EVENT_THREAD) { + status = ares_event_thread_init(channel); + if (status != ARES_SUCCESS) { + goto done; + } + } + done: if (status != ARES_SUCCESS) { ares_destroy(channel); diff --git a/src/lib/ares_options.c b/src/lib/ares_options.c index 649487dd..59e3aa7a 100644 --- a/src/lib/ares_options.c +++ b/src/lib/ares_options.c @@ -225,6 +225,10 @@ int ares_save_options(ares_channel_t *channel, struct ares_options *options, options->qcache_max_ttl = channel->qcache_max_ttl; } + if (channel->optmask & ARES_OPT_EVENT_THREAD) { + options->evsys = channel->evsys; + } + *optmask = (int)channel->optmask; return ARES_SUCCESS; @@ -267,6 +271,17 @@ ares_status_t ares__init_by_options(ares_channel_t *channel, } /* Easy stuff. */ + + /* Event Thread requires threading support and is incompatible with socket + * state callbacks */ + if (optmask & ARES_OPT_EVENT_THREAD) { + if (!ares_threadsafety()) + return ARES_ENOTIMP; + if (optmask & ARES_OPT_SOCK_STATE_CB) + return ARES_EFORMERR; + channel->evsys = options->evsys; + } + if (optmask & ARES_OPT_FLAGS) { channel->flags = (unsigned int)options->flags; } diff --git a/src/lib/ares_private.h b/src/lib/ares_private.h index 4c1df3bd..53c8d532 100644 --- a/src/lib/ares_private.h +++ b/src/lib/ares_private.h @@ -117,6 +117,7 @@ typedef struct ares_rand_state ares_rand_state; #include "ares__buf.h" #include "ares_dns_private.h" #include "ares__iface_ips.h" +#include "ares__threads.h" #ifndef HAVE_GETENV # include "ares_getenv.h" @@ -231,9 +232,6 @@ typedef struct ares__qcache ares__qcache_t; struct ares_hosts_file; typedef struct ares_hosts_file ares_hosts_file_t; -struct ares__thread_mutex; -typedef struct ares__thread_mutex ares__thread_mutex_t; - struct ares_channeldata { /* Configuration data */ unsigned int flags; @@ -253,6 +251,7 @@ struct ares_channeldata { char *lookups; size_t ednspsz; unsigned int qcache_max_ttl; + ares_evsys_t evsys; unsigned int optmask; /* For binding to local devices and/or IP addresses. Leave @@ -581,6 +580,13 @@ void ares__channel_threading_destroy(ares_channel_t *channel); void ares__channel_lock(ares_channel_t *channel); void ares__channel_unlock(ares_channel_t *channel); +struct ares_event_thread; +typedef struct ares_event_thread ares_event_thread_t; + +void ares_event_thread_destroy(ares_channel_t *channel); +ares_status_t ares_event_thread_init(ares_channel_t *channel); + + #ifdef _MSC_VER typedef __int64 ares_int64_t; typedef unsigned __int64 ares_uint64_t; diff --git a/src/lib/ares_process.c b/src/lib/ares_process.c index ff5899c1..d24add05 100644 --- a/src/lib/ares_process.c +++ b/src/lib/ares_process.c @@ -837,7 +837,6 @@ static size_t ares__calc_query_timeout(const struct query *query) /* For each trip through the entire server list, we want to double the * retry from the last retry */ rounds = (query->try_count / num_servers); - if (rounds > 0) { timeplus <<= rounds; } @@ -1002,7 +1001,6 @@ ares_status_t ares__send_query(struct query *query, struct timeval *now) } timeplus = ares__calc_query_timeout(query); - /* Keep track of queries bucketed by timeout, so we can process * timeout events quickly. */ diff --git a/test/Makefile.inc b/test/Makefile.inc index 8ef4f8e3..1d8af8c6 100644 --- a/test/Makefile.inc +++ b/test/Makefile.inc @@ -21,12 +21,13 @@ TESTSOURCES = ares-test-main.cc \ ares-test-live.cc \ ares-test-mock.cc \ ares-test-mock-ai.cc \ + ares-test-mock-et.cc \ ares-test-internal.cc \ dns-proto.cc \ dns-proto-test.cc TESTHEADERS = ares-test.h \ - dns-proto.h \ + dns-proto.h \ ares-test-ai.h FUZZSOURCES = ares-test-fuzz.c \ diff --git a/test/ares-test-main.cc b/test/ares-test-main.cc index 28ab00eb..f0dfeb5e 100644 --- a/test/ares-test-main.cc +++ b/test/ares-test-main.cc @@ -39,9 +39,13 @@ int main(int argc, char* argv[]) { } else if (strcmp(argv[ii], "-4") == 0) { ares::test::families = ares::test::ipv4_family; ares::test::families_modes = ares::test::ipv4_family_both_modes; + ares::test::evsys_families = ares::test::all_evsys_ipv4_family; + ares::test::evsys_families_modes = ares::test::all_evsys_ipv4_family_both_modes; } else if (strcmp(argv[ii], "-6") == 0) { ares::test::families = ares::test::ipv6_family; ares::test::families_modes = ares::test::ipv6_family_both_modes; + ares::test::evsys_families = ares::test::all_evsys_ipv6_family; + ares::test::evsys_families_modes = ares::test::all_evsys_ipv6_family_both_modes; } else { gtest_argv.push_back(argv[ii]); } diff --git a/test/ares-test-mock-ai.cc b/test/ares-test-mock-ai.cc index 1b1ccf41..87716f4b 100644 --- a/test/ares-test-mock-ai.cc +++ b/test/ares-test-mock-ai.cc @@ -748,28 +748,28 @@ TEST_P(MockChannelTestAI, FamilyV4ServiceName) { } INSTANTIATE_TEST_SUITE_P(AddressFamiliesAI, MockChannelTestAI, - ::testing::ValuesIn(ares::test::families_modes)); + ::testing::ValuesIn(ares::test::families_modes), PrintFamilyMode); INSTANTIATE_TEST_SUITE_P(AddressFamiliesAI, MockUDPChannelTestAI, - ::testing::ValuesIn(ares::test::families)); + ::testing::ValuesIn(ares::test::families), PrintFamily); INSTANTIATE_TEST_SUITE_P(AddressFamiliesAI, MockTCPChannelTestAI, - ::testing::ValuesIn(ares::test::families)); + ::testing::ValuesIn(ares::test::families), PrintFamily); INSTANTIATE_TEST_SUITE_P(AddressFamiliesAI, MockExtraOptsTestAI, - ::testing::ValuesIn(ares::test::families_modes)); + ::testing::ValuesIn(ares::test::families_modes), PrintFamilyMode); INSTANTIATE_TEST_SUITE_P(AddressFamiliesAI, MockExtraOptsNDots5TestAI, - ::testing::ValuesIn(ares::test::families_modes)); + ::testing::ValuesIn(ares::test::families_modes), PrintFamilyMode); INSTANTIATE_TEST_SUITE_P(AddressFamiliesAI, MockNoCheckRespChannelTestAI, - ::testing::ValuesIn(ares::test::families_modes)); + ::testing::ValuesIn(ares::test::families_modes), PrintFamilyMode); INSTANTIATE_TEST_SUITE_P(AddressFamiliesAI, MockEDNSChannelTestAI, - ::testing::ValuesIn(ares::test::families_modes)); + ::testing::ValuesIn(ares::test::families_modes), PrintFamilyMode); INSTANTIATE_TEST_SUITE_P(TransportModesAI, NoRotateMultiMockTestAI, - ::testing::ValuesIn(ares::test::families_modes)); + ::testing::ValuesIn(ares::test::families_modes), PrintFamilyMode); } // namespace test diff --git a/test/ares-test-mock-et.cc b/test/ares-test-mock-et.cc new file mode 100644 index 00000000..a143c3a6 --- /dev/null +++ b/test/ares-test-mock-et.cc @@ -0,0 +1,1396 @@ +/* MIT License + * + * Copyright (c) The c-ares project and its contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * SPDX-License-Identifier: MIT + */ +#include "ares-test.h" +#include "dns-proto.h" + +#ifdef CARES_THREADS + +#ifndef WIN32 +#include +#include +#endif + +#include +#include + +using testing::InvokeWithoutArgs; +using testing::DoAll; + +namespace ares { +namespace test { + +TEST_P(MockEventThreadTest, Basic) { + std::vector reply = { + 0x00, 0x00, // qid + 0x84, // response + query + AA + not-TC + not-RD + 0x00, // not-RA + not-Z + not-AD + not-CD + rc=NoError + 0x00, 0x01, // 1 question + 0x00, 0x01, // 1 answer RRs + 0x00, 0x00, // 0 authority RRs + 0x00, 0x00, // 0 additional RRs + // Question + 0x03, 'w', 'w', 'w', + 0x06, 'g', 'o', 'o', 'g', 'l', 'e', + 0x03, 'c', 'o', 'm', + 0x00, + 0x00, 0x01, // type A + 0x00, 0x01, // class IN + // Answer + 0x03, 'w', 'w', 'w', + 0x06, 'g', 'o', 'o', 'g', 'l', 'e', + 0x03, 'c', 'o', 'm', + 0x00, + 0x00, 0x01, // type A + 0x00, 0x01, // class IN + 0x00, 0x00, 0x01, 0x00, // TTL + 0x00, 0x04, // rdata length + 0x01, 0x02, 0x03, 0x04 + }; + + ON_CALL(server_, OnRequest("www.google.com", T_A)) + .WillByDefault(SetReplyData(&server_, reply)); + + HostResult result; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + std::stringstream ss; + ss << result.host_; + EXPECT_EQ("{'www.google.com' aliases=[] addrs=[1.2.3.4]}", ss.str()); +} + +// UDP only so mock server doesn't get confused by concatenated requests +TEST_P(MockUDPEventThreadTest, GetHostByNameParallelLookups) { + DNSPacket rsp1; + rsp1.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 100, {2, 3, 4, 5})); + ON_CALL(server_, OnRequest("www.google.com", T_A)) + .WillByDefault(SetReply(&server_, &rsp1)); + DNSPacket rsp2; + rsp2.set_response().set_aa() + .add_question(new DNSQuestion("www.example.com", T_A)) + .add_answer(new DNSARR("www.example.com", 100, {1, 2, 3, 4})); + ON_CALL(server_, OnRequest("www.example.com", T_A)) + .WillByDefault(SetReply(&server_, &rsp2)); + + HostResult result1; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result1); + HostResult result2; + ares_gethostbyname(channel_, "www.example.com.", AF_INET, HostCallback, &result2); + HostResult result3; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result3); + Process(); + EXPECT_TRUE(result1.done_); + EXPECT_TRUE(result2.done_); + EXPECT_TRUE(result3.done_); + std::stringstream ss1; + ss1 << result1.host_; + EXPECT_EQ("{'www.google.com' aliases=[] addrs=[2.3.4.5]}", ss1.str()); + std::stringstream ss2; + ss2 << result2.host_; + EXPECT_EQ("{'www.example.com' aliases=[] addrs=[1.2.3.4]}", ss2.str()); + std::stringstream ss3; + ss3 << result3.host_; + EXPECT_EQ("{'www.google.com' aliases=[] addrs=[2.3.4.5]}", ss3.str()); +} + +// UDP to TCP specific test +TEST_P(MockUDPEventThreadTest, TruncationRetry) { + DNSPacket rsptruncated; + rsptruncated.set_response().set_aa().set_tc() + .add_question(new DNSQuestion("www.google.com", T_A)); + DNSPacket rspok; + rspok.set_response() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 100, {1, 2, 3, 4})); + EXPECT_CALL(server_, OnRequest("www.google.com", T_A)) + .WillOnce(SetReply(&server_, &rsptruncated)) + .WillOnce(SetReply(&server_, &rspok)); + HostResult result; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + std::stringstream ss; + ss << result.host_; + EXPECT_EQ("{'www.google.com' aliases=[] addrs=[1.2.3.4]}", ss.str()); +} + +static int sock_cb_count = 0; +static int SocketConnectCallback(ares_socket_t fd, int type, void *data) { + int rc = *(int*)data; + if (verbose) std::cerr << "SocketConnectCallback(" << fd << ") invoked" << std::endl; + sock_cb_count++; + return rc; +} + +TEST_P(MockEventThreadTest, SockCallback) { + DNSPacket rsp; + rsp.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 100, {2, 3, 4, 5})); + EXPECT_CALL(server_, OnRequest("www.google.com", T_A)) + .WillOnce(SetReply(&server_, &rsp)); + + // Get notified of new sockets + int rc = ARES_SUCCESS; + ares_set_socket_callback(channel_, SocketConnectCallback, &rc); + + HostResult result; + sock_cb_count = 0; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result); + Process(); + EXPECT_EQ(1, sock_cb_count); + EXPECT_TRUE(result.done_); + std::stringstream ss; + ss << result.host_; + EXPECT_EQ("{'www.google.com' aliases=[] addrs=[2.3.4.5]}", ss.str()); +} + +TEST_P(MockEventThreadTest, SockFailCallback) { + // Notification of new sockets gives an error. + int rc = -1; + ares_set_socket_callback(channel_, SocketConnectCallback, &rc); + + HostResult result; + sock_cb_count = 0; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result); + Process(); + EXPECT_LT(1, sock_cb_count); + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_ECONNREFUSED, result.status_); +} + + +TEST_P(MockEventThreadTest, ReInit) { + DNSPacket rsp; + rsp.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 100, {2, 3, 4, 5})); + EXPECT_CALL(server_, OnRequest("www.google.com", T_A)) + .WillOnce(SetReply(&server_, &rsp)); + + HostResult result; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result); + EXPECT_EQ(ARES_SUCCESS, ares_reinit(channel_)); + Process(); + EXPECT_TRUE(result.done_); + std::stringstream ss; + ss << result.host_; + EXPECT_EQ("{'www.google.com' aliases=[] addrs=[2.3.4.5]}", ss.str()); +} + +#define MAXUDPQUERIES_TOTAL 32 +#define MAXUDPQUERIES_LIMIT 8 + +class MockUDPEventThreadMaxQueriesTest + : public MockEventThreadOptsTest, + public ::testing::WithParamInterface> { + public: + MockUDPEventThreadMaxQueriesTest() + : MockEventThreadOptsTest(1, std::get<0>(GetParam()), std::get<1>(GetParam()), false, + FillOptions(&opts_), + ARES_OPT_UDP_MAX_QUERIES) {} + static struct ares_options* FillOptions(struct ares_options * opts) { + memset(opts, 0, sizeof(struct ares_options)); + opts->udp_max_queries = MAXUDPQUERIES_LIMIT; + return opts; + } + private: + struct ares_options opts_; +}; + +TEST_P(MockUDPEventThreadMaxQueriesTest, GetHostByNameParallelLookups) { + DNSPacket rsp; + rsp.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 100, {2, 3, 4, 5})); + ON_CALL(server_, OnRequest("www.google.com", T_A)) + .WillByDefault(SetReply(&server_, &rsp)); + + // Get notified of new sockets so we can validate how many are created + int rc = ARES_SUCCESS; + ares_set_socket_callback(channel_, SocketConnectCallback, &rc); + sock_cb_count = 0; + + HostResult result[MAXUDPQUERIES_TOTAL]; + for (size_t i=0; i> { + public: + CacheQueriesEventThreadTest() + : MockEventThreadOptsTest(1, std::get<0>(GetParam()), std::get<1>(GetParam()), false, + FillOptions(&opts_), + ARES_OPT_QUERY_CACHE) {} + static struct ares_options* FillOptions(struct ares_options * opts) { + memset(opts, 0, sizeof(struct ares_options)); + opts->qcache_max_ttl = 3600; + return opts; + } + private: + struct ares_options opts_; +}; + +TEST_P(CacheQueriesEventThreadTest, GetHostByNameCache) { + DNSPacket rsp; + rsp.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 100, {2, 3, 4, 5})); + ON_CALL(server_, OnRequest("www.google.com", T_A)) + .WillByDefault(SetReply(&server_, &rsp)); + + // Get notified of new sockets so we can validate how many are created + int rc = ARES_SUCCESS; + ares_set_socket_callback(channel_, SocketConnectCallback, &rc); + sock_cb_count = 0; + + HostResult result1; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result1); + Process(); + + std::stringstream ss1; + EXPECT_TRUE(result1.done_); + ss1 << result1.host_; + EXPECT_EQ("{'www.google.com' aliases=[] addrs=[2.3.4.5]}", ss1.str()); + + /* Run again, should return cached result */ + HostResult result2; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result2); + Process(); + + std::stringstream ss2; + EXPECT_TRUE(result2.done_); + ss2 << result2.host_; + EXPECT_EQ("{'www.google.com' aliases=[] addrs=[2.3.4.5]}", ss2.str()); + + EXPECT_EQ(1, sock_cb_count); +} + +#define TCPPARALLELLOOKUPS 32 +TEST_P(MockTCPEventThreadTest, GetHostByNameParallelLookups) { + DNSPacket rsp; + rsp.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 100, {2, 3, 4, 5})); + ON_CALL(server_, OnRequest("www.google.com", T_A)) + .WillByDefault(SetReply(&server_, &rsp)); + + // Get notified of new sockets so we can validate how many are created + int rc = ARES_SUCCESS; + ares_set_socket_callback(channel_, SocketConnectCallback, &rc); + sock_cb_count = 0; + + HostResult result[TCPPARALLELLOOKUPS]; + for (size_t i=0; i one = {0x00}; + ON_CALL(server_, OnRequest("www.google.com", T_A)) + .WillByDefault(SetReplyData(&server_, one)); + + HostResult result; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_ETIMEOUT, result.status_); +} + +TEST_P(MockTCPEventThreadTest, FormErrResponse) { + DNSPacket rsp; + rsp.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)); + rsp.set_rcode(FORMERR); + EXPECT_CALL(server_, OnRequest("www.google.com", T_A)) + .WillOnce(SetReply(&server_, &rsp)); + HostResult result; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_EFORMERR, result.status_); +} + +TEST_P(MockTCPEventThreadTest, ServFailResponse) { + DNSPacket rsp; + rsp.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)); + rsp.set_rcode(SERVFAIL); + ON_CALL(server_, OnRequest("www.google.com", T_A)) + .WillByDefault(SetReply(&server_, &rsp)); + HostResult result; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_ESERVFAIL, result.status_); +} + +TEST_P(MockTCPEventThreadTest, NotImplResponse) { + DNSPacket rsp; + rsp.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)); + rsp.set_rcode(NOTIMP); + ON_CALL(server_, OnRequest("www.google.com", T_A)) + .WillByDefault(SetReply(&server_, &rsp)); + HostResult result; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_ENOTIMP, result.status_); +} + +TEST_P(MockTCPEventThreadTest, RefusedResponse) { + DNSPacket rsp; + rsp.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)); + rsp.set_rcode(REFUSED); + ON_CALL(server_, OnRequest("www.google.com", T_A)) + .WillByDefault(SetReply(&server_, &rsp)); + HostResult result; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_EREFUSED, result.status_); +} + +TEST_P(MockTCPEventThreadTest, YXDomainResponse) { + DNSPacket rsp; + rsp.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)); + rsp.set_rcode(YXDOMAIN); + EXPECT_CALL(server_, OnRequest("www.google.com", T_A)) + .WillOnce(SetReply(&server_, &rsp)); + HostResult result; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_ENODATA, result.status_); +} + +class MockExtraOptsEventThreadTest + : public MockEventThreadOptsTest, + public ::testing::WithParamInterface > { + public: + MockExtraOptsEventThreadTest() + : MockEventThreadOptsTest(1, std::get<0>(GetParam()), std::get<1>(GetParam()), std::get<2>(GetParam()), + FillOptions(&opts_), + ARES_OPT_SOCK_SNDBUF|ARES_OPT_SOCK_RCVBUF) {} + static struct ares_options* FillOptions(struct ares_options * opts) { + memset(opts, 0, sizeof(struct ares_options)); + // Set a few options that affect socket communications + opts->socket_send_buffer_size = 514; + opts->socket_receive_buffer_size = 514; + return opts; + } + private: + struct ares_options opts_; +}; + +TEST_P(MockExtraOptsEventThreadTest, SimpleQuery) { + ares_set_local_ip4(channel_, 0x7F000001); + byte addr6[16] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}; + ares_set_local_ip6(channel_, addr6); + ares_set_local_dev(channel_, "dummy"); + + DNSPacket rsp; + rsp.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 100, {2, 3, 4, 5})); + ON_CALL(server_, OnRequest("www.google.com", T_A)) + .WillByDefault(SetReply(&server_, &rsp)); + + HostResult result; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + std::stringstream ss; + ss << result.host_; + EXPECT_EQ("{'www.google.com' aliases=[] addrs=[2.3.4.5]}", ss.str()); +} + +class MockFlagsEventThreadOptsTest + : public MockEventThreadOptsTest, + public ::testing::WithParamInterface< std::tuple > { + public: + MockFlagsEventThreadOptsTest(int flags) + : MockEventThreadOptsTest(1, std::get<0>(GetParam()), std::get<1>(GetParam()), std::get<2>(GetParam()), + FillOptions(&opts_, flags), ARES_OPT_FLAGS) {} + static struct ares_options* FillOptions(struct ares_options * opts, int flags) { + memset(opts, 0, sizeof(struct ares_options)); + opts->flags = flags; + return opts; + } + private: + struct ares_options opts_; +}; + +class MockNoCheckRespEventThreadTest : public MockFlagsEventThreadOptsTest { + public: + MockNoCheckRespEventThreadTest() : MockFlagsEventThreadOptsTest(ARES_FLAG_NOCHECKRESP) {} +}; + +TEST_P(MockNoCheckRespEventThreadTest, ServFailResponse) { + DNSPacket rsp; + rsp.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)); + rsp.set_rcode(SERVFAIL); + ON_CALL(server_, OnRequest("www.google.com", T_A)) + .WillByDefault(SetReply(&server_, &rsp)); + HostResult result; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_ESERVFAIL, result.status_); +} + +TEST_P(MockNoCheckRespEventThreadTest, NotImplResponse) { + DNSPacket rsp; + rsp.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)); + rsp.set_rcode(NOTIMP); + ON_CALL(server_, OnRequest("www.google.com", T_A)) + .WillByDefault(SetReply(&server_, &rsp)); + HostResult result; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_ENOTIMP, result.status_); +} + +TEST_P(MockNoCheckRespEventThreadTest, RefusedResponse) { + DNSPacket rsp; + rsp.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)); + rsp.set_rcode(REFUSED); + ON_CALL(server_, OnRequest("www.google.com", T_A)) + .WillByDefault(SetReply(&server_, &rsp)); + HostResult result; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_EREFUSED, result.status_); +} + +class MockEDNSEventThreadTest : public MockFlagsEventThreadOptsTest { + public: + MockEDNSEventThreadTest() : MockFlagsEventThreadOptsTest(ARES_FLAG_EDNS) {} +}; + +TEST_P(MockEDNSEventThreadTest, RetryWithoutEDNS) { + DNSPacket rspfail; + rspfail.set_response().set_aa().set_rcode(FORMERR) + .add_question(new DNSQuestion("www.google.com", T_A)); + DNSPacket rspok; + rspok.set_response() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 100, {1, 2, 3, 4})); + EXPECT_CALL(server_, OnRequest("www.google.com", T_A)) + .WillOnce(SetReply(&server_, &rspfail)) + .WillOnce(SetReply(&server_, &rspok)); + HostResult result; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + std::stringstream ss; + ss << result.host_; + EXPECT_EQ("{'www.google.com' aliases=[] addrs=[1.2.3.4]}", ss.str()); +} + +TEST_P(MockEventThreadTest, SearchDomains) { + DNSPacket nofirst; + nofirst.set_response().set_aa().set_rcode(NXDOMAIN) + .add_question(new DNSQuestion("www.first.com", T_A)); + ON_CALL(server_, OnRequest("www.first.com", T_A)) + .WillByDefault(SetReply(&server_, &nofirst)); + DNSPacket nosecond; + nosecond.set_response().set_aa().set_rcode(NXDOMAIN) + .add_question(new DNSQuestion("www.second.org", T_A)); + ON_CALL(server_, OnRequest("www.second.org", T_A)) + .WillByDefault(SetReply(&server_, &nosecond)); + DNSPacket yesthird; + yesthird.set_response().set_aa() + .add_question(new DNSQuestion("www.third.gov", T_A)) + .add_answer(new DNSARR("www.third.gov", 0x0200, {2, 3, 4, 5})); + ON_CALL(server_, OnRequest("www.third.gov", T_A)) + .WillByDefault(SetReply(&server_, &yesthird)); + + HostResult result; + ares_gethostbyname(channel_, "www", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + std::stringstream ss; + ss << result.host_; + EXPECT_EQ("{'www.third.gov' aliases=[] addrs=[2.3.4.5]}", ss.str()); +} + +// Relies on retries so is UDP-only +TEST_P(MockUDPEventThreadTest, SearchDomainsWithResentReply) { + DNSPacket nofirst; + nofirst.set_response().set_aa().set_rcode(NXDOMAIN) + .add_question(new DNSQuestion("www.first.com", T_A)); + EXPECT_CALL(server_, OnRequest("www.first.com", T_A)) + .WillOnce(SetReply(&server_, &nofirst)); + DNSPacket nosecond; + nosecond.set_response().set_aa().set_rcode(NXDOMAIN) + .add_question(new DNSQuestion("www.second.org", T_A)); + EXPECT_CALL(server_, OnRequest("www.second.org", T_A)) + .WillOnce(SetReply(&server_, &nosecond)); + DNSPacket yesthird; + yesthird.set_response().set_aa() + .add_question(new DNSQuestion("www.third.gov", T_A)) + .add_answer(new DNSARR("www.third.gov", 0x0200, {2, 3, 4, 5})); + // Before sending the real answer, resend an earlier reply + EXPECT_CALL(server_, OnRequest("www.third.gov", T_A)) + .WillOnce(DoAll(SetReply(&server_, &nofirst), + SetReplyQID(&server_, 123))) + .WillOnce(DoAll(SetReply(&server_, &yesthird), + SetReplyQID(&server_, -1))); + + HostResult result; + ares_gethostbyname(channel_, "www", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + std::stringstream ss; + ss << result.host_; + EXPECT_EQ("{'www.third.gov' aliases=[] addrs=[2.3.4.5]}", ss.str()); +} + +TEST_P(MockEventThreadTest, SearchDomainsBare) { + DNSPacket nofirst; + nofirst.set_response().set_aa().set_rcode(NXDOMAIN) + .add_question(new DNSQuestion("www.first.com", T_A)); + ON_CALL(server_, OnRequest("www.first.com", T_A)) + .WillByDefault(SetReply(&server_, &nofirst)); + DNSPacket nosecond; + nosecond.set_response().set_aa().set_rcode(NXDOMAIN) + .add_question(new DNSQuestion("www.second.org", T_A)); + ON_CALL(server_, OnRequest("www.second.org", T_A)) + .WillByDefault(SetReply(&server_, &nosecond)); + DNSPacket nothird; + nothird.set_response().set_aa().set_rcode(NXDOMAIN) + .add_question(new DNSQuestion("www.third.gov", T_A)); + ON_CALL(server_, OnRequest("www.third.gov", T_A)) + .WillByDefault(SetReply(&server_, ¬hird)); + DNSPacket yesbare; + yesbare.set_response().set_aa() + .add_question(new DNSQuestion("www", T_A)) + .add_answer(new DNSARR("www", 0x0200, {2, 3, 4, 5})); + ON_CALL(server_, OnRequest("www", T_A)) + .WillByDefault(SetReply(&server_, &yesbare)); + + HostResult result; + ares_gethostbyname(channel_, "www", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + std::stringstream ss; + ss << result.host_; + EXPECT_EQ("{'www' aliases=[] addrs=[2.3.4.5]}", ss.str()); +} + +TEST_P(MockEventThreadTest, SearchNoDataThenSuccess) { + // First two search domains recognize the name but have no A records. + DNSPacket nofirst; + nofirst.set_response().set_aa() + .add_question(new DNSQuestion("www.first.com", T_A)); + ON_CALL(server_, OnRequest("www.first.com", T_A)) + .WillByDefault(SetReply(&server_, &nofirst)); + DNSPacket nosecond; + nosecond.set_response().set_aa() + .add_question(new DNSQuestion("www.second.org", T_A)); + ON_CALL(server_, OnRequest("www.second.org", T_A)) + .WillByDefault(SetReply(&server_, &nosecond)); + DNSPacket yesthird; + yesthird.set_response().set_aa() + .add_question(new DNSQuestion("www.third.gov", T_A)) + .add_answer(new DNSARR("www.third.gov", 0x0200, {2, 3, 4, 5})); + ON_CALL(server_, OnRequest("www.third.gov", T_A)) + .WillByDefault(SetReply(&server_, &yesthird)); + + HostResult result; + ares_gethostbyname(channel_, "www", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + std::stringstream ss; + ss << result.host_; + EXPECT_EQ("{'www.third.gov' aliases=[] addrs=[2.3.4.5]}", ss.str()); +} + +TEST_P(MockEventThreadTest, SearchNoDataThenNoDataBare) { + // First two search domains recognize the name but have no A records. + DNSPacket nofirst; + nofirst.set_response().set_aa() + .add_question(new DNSQuestion("www.first.com", T_A)); + ON_CALL(server_, OnRequest("www.first.com", T_A)) + .WillByDefault(SetReply(&server_, &nofirst)); + DNSPacket nosecond; + nosecond.set_response().set_aa() + .add_question(new DNSQuestion("www.second.org", T_A)); + ON_CALL(server_, OnRequest("www.second.org", T_A)) + .WillByDefault(SetReply(&server_, &nosecond)); + DNSPacket nothird; + nothird.set_response().set_aa() + .add_question(new DNSQuestion("www.third.gov", T_A)); + ON_CALL(server_, OnRequest("www.third.gov", T_A)) + .WillByDefault(SetReply(&server_, ¬hird)); + DNSPacket nobare; + nobare.set_response().set_aa() + .add_question(new DNSQuestion("www", T_A)); + ON_CALL(server_, OnRequest("www", T_A)) + .WillByDefault(SetReply(&server_, &nobare)); + + HostResult result; + ares_gethostbyname(channel_, "www", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_ENODATA, result.status_); +} + +TEST_P(MockEventThreadTest, SearchNoDataThenFail) { + // First two search domains recognize the name but have no A records. + DNSPacket nofirst; + nofirst.set_response().set_aa() + .add_question(new DNSQuestion("www.first.com", T_A)); + ON_CALL(server_, OnRequest("www.first.com", T_A)) + .WillByDefault(SetReply(&server_, &nofirst)); + DNSPacket nosecond; + nosecond.set_response().set_aa() + .add_question(new DNSQuestion("www.second.org", T_A)); + ON_CALL(server_, OnRequest("www.second.org", T_A)) + .WillByDefault(SetReply(&server_, &nosecond)); + DNSPacket nothird; + nothird.set_response().set_aa() + .add_question(new DNSQuestion("www.third.gov", T_A)); + ON_CALL(server_, OnRequest("www.third.gov", T_A)) + .WillByDefault(SetReply(&server_, ¬hird)); + DNSPacket nobare; + nobare.set_response().set_aa().set_rcode(NXDOMAIN) + .add_question(new DNSQuestion("www", T_A)); + ON_CALL(server_, OnRequest("www", T_A)) + .WillByDefault(SetReply(&server_, &nobare)); + + HostResult result; + ares_gethostbyname(channel_, "www", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_ENODATA, result.status_); +} + +TEST_P(MockEventThreadTest, SearchHighNdots) { + DNSPacket nobare; + nobare.set_response().set_aa().set_rcode(NXDOMAIN) + .add_question(new DNSQuestion("a.b.c.w.w.w", T_A)); + ON_CALL(server_, OnRequest("a.b.c.w.w.w", T_A)) + .WillByDefault(SetReply(&server_, &nobare)); + DNSPacket yesfirst; + yesfirst.set_response().set_aa() + .add_question(new DNSQuestion("a.b.c.w.w.w.first.com", T_A)) + .add_answer(new DNSARR("a.b.c.w.w.w.first.com", 0x0200, {2, 3, 4, 5})); + ON_CALL(server_, OnRequest("a.b.c.w.w.w.first.com", T_A)) + .WillByDefault(SetReply(&server_, &yesfirst)); + + SearchResult result; + ares_search(channel_, "a.b.c.w.w.w", C_IN, T_A, SearchCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_SUCCESS, result.status_); + std::stringstream ss; + ss << PacketToString(result.data_); + EXPECT_EQ("RSP QRY AA NOERROR Q:{'a.b.c.w.w.w.first.com' IN A} " + "A:{'a.b.c.w.w.w.first.com' IN A TTL=512 2.3.4.5}", + ss.str()); +} + +TEST_P(MockEventThreadTest, V4WorksV6Timeout) { + std::vector nothing; + DNSPacket reply; + reply.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 0x0100, {0x01, 0x02, 0x03, 0x04})); + + ON_CALL(server_, OnRequest("www.google.com", T_A)) + .WillByDefault(SetReply(&server_, &reply)); + + ON_CALL(server_, OnRequest("www.google.com", T_AAAA)) + .WillByDefault(SetReplyData(&server_, nothing)); + + HostResult result; + ares_gethostbyname(channel_, "www.google.com.", AF_UNSPEC, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + EXPECT_EQ(1, result.timeouts_); + std::stringstream ss; + ss << result.host_; + EXPECT_EQ("{'www.google.com' aliases=[] addrs=[1.2.3.4]}", ss.str()); +} + +TEST_P(MockEventThreadTest, DestroyQuick) { + /* We are not looking for any particular result as its possible (but unlikely) + * it finished before the destroy completed. We really just want to make sure + * cleanup works in this case properly. */ + HostResult result; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result); + ares_destroy(channel_); + channel_ = nullptr; + EXPECT_TRUE(result.done_); +} + +#ifndef CARES_SYMBOL_HIDING +// Test case for Issue #662 +TEST_P(MockEventThreadTest, PartialQueryCancel) { + std::vector nothing; + DNSPacket reply; + reply.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 0x0100, {0x01, 0x02, 0x03, 0x04})); + + ON_CALL(server_, OnRequest("www.google.com", T_A)) + .WillByDefault(SetReply(&server_, &reply)); + + ON_CALL(server_, OnRequest("www.google.com", T_AAAA)) + .WillByDefault(SetReplyData(&server_, nothing)); + + HostResult result; + ares_gethostbyname(channel_, "www.google.com.", AF_UNSPEC, HostCallback, &result); + // After 100ms, issues ares_cancel(), this should be enough time for the A + // record reply, but before the timeout on the AAAA record. + Process(100); + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_ECANCELLED, result.status_); +} +#endif + +TEST_P(MockEventThreadTest, UnspecifiedFamilyV6) { + DNSPacket rsp6; + rsp6.set_response().set_aa() + .add_question(new DNSQuestion("example.com", T_AAAA)) + .add_answer(new DNSAaaaRR("example.com", 100, + {0x21, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03})); + ON_CALL(server_, OnRequest("example.com", T_AAAA)) + .WillByDefault(SetReply(&server_, &rsp6)); + + DNSPacket rsp4; + rsp4.set_response().set_aa() + .add_question(new DNSQuestion("example.com", T_A)); + ON_CALL(server_, OnRequest("example.com", T_A)) + .WillByDefault(SetReply(&server_, &rsp4)); + + HostResult result; + ares_gethostbyname(channel_, "example.com.", AF_UNSPEC, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + std::stringstream ss; + ss << result.host_; + // Default to IPv6 when both are available. + EXPECT_EQ("{'example.com' aliases=[] addrs=[2121:0000:0000:0000:0000:0000:0000:0303]}", ss.str()); +} + +TEST_P(MockEventThreadTest, UnspecifiedFamilyV4) { + DNSPacket rsp6; + rsp6.set_response().set_aa() + .add_question(new DNSQuestion("example.com", T_AAAA)); + ON_CALL(server_, OnRequest("example.com", T_AAAA)) + .WillByDefault(SetReply(&server_, &rsp6)); + DNSPacket rsp4; + rsp4.set_response().set_aa() + .add_question(new DNSQuestion("example.com", T_A)) + .add_answer(new DNSARR("example.com", 100, {2, 3, 4, 5})); + ON_CALL(server_, OnRequest("example.com", T_A)) + .WillByDefault(SetReply(&server_, &rsp4)); + + HostResult result; + ares_gethostbyname(channel_, "example.com.", AF_UNSPEC, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + std::stringstream ss; + ss << result.host_; + EXPECT_EQ("{'example.com' aliases=[] addrs=[2.3.4.5]}", ss.str()); +} + +TEST_P(MockEventThreadTest, UnspecifiedFamilyNoData) { + DNSPacket rsp6; + rsp6.set_response().set_aa() + .add_question(new DNSQuestion("example.com", T_AAAA)) + .add_answer(new DNSCnameRR("example.com", 100, "elsewhere.com")); + ON_CALL(server_, OnRequest("example.com", T_AAAA)) + .WillByDefault(SetReply(&server_, &rsp6)); + DNSPacket rsp4; + rsp4.set_response().set_aa() + .add_question(new DNSQuestion("example.com", T_A)); + ON_CALL(server_, OnRequest("example.com", T_A)) + .WillByDefault(SetReply(&server_, &rsp4)); + + HostResult result; + ares_gethostbyname(channel_, "example.com.", AF_UNSPEC, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + std::stringstream ss; + ss << result.host_; + EXPECT_EQ("{'' aliases=[] addrs=[]}", ss.str()); +} + +TEST_P(MockEventThreadTest, UnspecifiedFamilyCname6A4) { + DNSPacket rsp6; + rsp6.set_response().set_aa() + .add_question(new DNSQuestion("example.com", T_AAAA)) + .add_answer(new DNSCnameRR("example.com", 100, "elsewhere.com")); + ON_CALL(server_, OnRequest("example.com", T_AAAA)) + .WillByDefault(SetReply(&server_, &rsp6)); + DNSPacket rsp4; + rsp4.set_response().set_aa() + .add_question(new DNSQuestion("example.com", T_A)) + .add_answer(new DNSARR("example.com", 100, {1, 2, 3, 4})); + ON_CALL(server_, OnRequest("example.com", T_A)) + .WillByDefault(SetReply(&server_, &rsp4)); + + HostResult result; + ares_gethostbyname(channel_, "example.com.", AF_UNSPEC, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + std::stringstream ss; + ss << result.host_; + EXPECT_EQ("{'example.com' aliases=[] addrs=[1.2.3.4]}", ss.str()); +} + +TEST_P(MockEventThreadTest, ExplicitIP) { + HostResult result; + ares_gethostbyname(channel_, "1.2.3.4", AF_INET, HostCallback, &result); + EXPECT_TRUE(result.done_); // Immediate return + EXPECT_EQ(ARES_SUCCESS, result.status_); + std::stringstream ss; + ss << result.host_; + EXPECT_EQ("{'1.2.3.4' aliases=[] addrs=[1.2.3.4]}", ss.str()); +} + +TEST_P(MockEventThreadTest, SortListV4) { + DNSPacket rsp; + rsp.set_response().set_aa() + .add_question(new DNSQuestion("example.com", T_A)) + .add_answer(new DNSARR("example.com", 100, {22, 23, 24, 25})) + .add_answer(new DNSARR("example.com", 100, {12, 13, 14, 15})) + .add_answer(new DNSARR("example.com", 100, {2, 3, 4, 5})); + ON_CALL(server_, OnRequest("example.com", T_A)) + .WillByDefault(SetReply(&server_, &rsp)); + + { + EXPECT_EQ(ARES_SUCCESS, ares_set_sortlist(channel_, "12.13.0.0/255.255.0.0 1234::5678")); + HostResult result; + ares_gethostbyname(channel_, "example.com.", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + std::stringstream ss; + ss << result.host_; + EXPECT_EQ("{'example.com' aliases=[] addrs=[12.13.14.15, 22.23.24.25, 2.3.4.5]}", ss.str()); + } + { + EXPECT_EQ(ARES_SUCCESS, ares_set_sortlist(channel_, "2.3.0.0/16 130.140.150.160/26")); + HostResult result; + ares_gethostbyname(channel_, "example.com.", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + std::stringstream ss; + ss << result.host_; + EXPECT_EQ("{'example.com' aliases=[] addrs=[2.3.4.5, 22.23.24.25, 12.13.14.15]}", ss.str()); + } + struct ares_options options; + memset(&options, 0, sizeof(options)); + int optmask = 0; + EXPECT_EQ(ARES_SUCCESS, ares_save_options(channel_, &options, &optmask)); + EXPECT_TRUE((optmask & ARES_OPT_SORTLIST) == ARES_OPT_SORTLIST); + ares_destroy_options(&options); +} + +TEST_P(MockEventThreadTest, SortListV6) { + DNSPacket rsp; + rsp.set_response().set_aa() + .add_question(new DNSQuestion("example.com", T_AAAA)) + .add_answer(new DNSAaaaRR("example.com", 100, + {0x11, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x02})) + .add_answer(new DNSAaaaRR("example.com", 100, + {0x21, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03})); + ON_CALL(server_, OnRequest("example.com", T_AAAA)) + .WillByDefault(SetReply(&server_, &rsp)); + + { + ares_set_sortlist(channel_, "1111::/16 2.3.0.0/255.255.0.0"); + HostResult result; + ares_gethostbyname(channel_, "example.com.", AF_INET6, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + std::stringstream ss; + ss << result.host_; + EXPECT_EQ("{'example.com' aliases=[] addrs=[1111:0000:0000:0000:0000:0000:0000:0202, " + "2121:0000:0000:0000:0000:0000:0000:0303]}", ss.str()); + } + { + ares_set_sortlist(channel_, "2121::/8"); + HostResult result; + ares_gethostbyname(channel_, "example.com.", AF_INET6, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + std::stringstream ss; + ss << result.host_; + EXPECT_EQ("{'example.com' aliases=[] addrs=[2121:0000:0000:0000:0000:0000:0000:0303, " + "1111:0000:0000:0000:0000:0000:0000:0202]}", ss.str()); + } +} + +// Relies on retries so is UDP-only +TEST_P(MockUDPEventThreadTest, Resend) { + std::vector nothing; + DNSPacket reply; + reply.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 0x0100, {0x01, 0x02, 0x03, 0x04})); + + EXPECT_CALL(server_, OnRequest("www.google.com", T_A)) + .WillOnce(SetReplyData(&server_, nothing)) + .WillOnce(SetReplyData(&server_, nothing)) + .WillOnce(SetReply(&server_, &reply)); + + HostResult result; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + EXPECT_EQ(2, result.timeouts_); + std::stringstream ss; + ss << result.host_; + EXPECT_EQ("{'www.google.com' aliases=[] addrs=[1.2.3.4]}", ss.str()); +} + +TEST_P(MockEventThreadTest, CancelImmediate) { + HostResult result; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result); + ares_cancel(channel_); + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_ECANCELLED, result.status_); + EXPECT_EQ(0, result.timeouts_); +} + +TEST_P(MockEventThreadTest, CancelImmediateGetHostByAddr) { + HostResult result; + struct in_addr addr; + addr.s_addr = htonl(0x08080808); + + ares_gethostbyaddr(channel_, &addr, sizeof(addr), AF_INET, HostCallback, &result); + ares_cancel(channel_); + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_ECANCELLED, result.status_); + EXPECT_EQ(0, result.timeouts_); +} + +// Relies on retries so is UDP-only +TEST_P(MockUDPEventThreadTest, CancelLater) { + std::vector nothing; + + // On second request, cancel the channel. + EXPECT_CALL(server_, OnRequest("www.google.com", T_A)) + .WillOnce(SetReplyData(&server_, nothing)) + .WillOnce(CancelChannel(&server_, channel_)); + + HostResult result; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_ECANCELLED, result.status_); + EXPECT_EQ(0, result.timeouts_); +} + +TEST_P(MockEventThreadTest, DisconnectFirstAttempt) { + DNSPacket reply; + reply.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 0x0100, {0x01, 0x02, 0x03, 0x04})); + + // On second request, cancel the channel. + EXPECT_CALL(server_, OnRequest("www.google.com", T_A)) + .WillOnce(Disconnect(&server_)) + .WillOnce(SetReply(&server_, &reply)); + + HostResult result; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + if (result.done_) { + std::stringstream ss; + ss << result.host_; + EXPECT_EQ("{'www.google.com' aliases=[] addrs=[1.2.3.4]}", ss.str()); + } +} + +TEST_P(MockEventThreadTest, GetHostByNameCNAMENoData) { + DNSPacket response; + response.set_response().set_aa() + .add_question(new DNSQuestion("cname.first.com", T_A)) + .add_answer(new DNSCnameRR("cname.first.com", 100, "a.first.com")); + ON_CALL(server_, OnRequest("cname.first.com", T_A)) + .WillByDefault(SetReply(&server_, &response)); + + HostResult result; + ares_gethostbyname(channel_, "cname.first.com.", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_ENODATA, result.status_); +} + +#ifndef WIN32 +TEST_P(MockEventThreadTest, HostAlias) { + DNSPacket reply; + reply.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 0x0100, {0x01, 0x02, 0x03, 0x04})); + ON_CALL(server_, OnRequest("www.google.com", T_A)) + .WillByDefault(SetReply(&server_, &reply)); + + TempFile aliases("\n\n# www commentedout\nwww www.google.com\n"); + EnvValue with_env("HOSTALIASES", aliases.filename()); + + HostResult result; + ares_gethostbyname(channel_, "www", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + std::stringstream ss; + ss << result.host_; + EXPECT_EQ("{'www.google.com' aliases=[] addrs=[1.2.3.4]}", ss.str()); +} + +TEST_P(MockEventThreadTest, HostAliasMissing) { + DNSPacket yesfirst; + yesfirst.set_response().set_aa() + .add_question(new DNSQuestion("www.first.com", T_A)) + .add_answer(new DNSARR("www.first.com", 0x0200, {2, 3, 4, 5})); + ON_CALL(server_, OnRequest("www.first.com", T_A)) + .WillByDefault(SetReply(&server_, &yesfirst)); + + TempFile aliases("\n\n# www commentedout\nww www.google.com\n"); + EnvValue with_env("HOSTALIASES", aliases.filename()); + HostResult result; + ares_gethostbyname(channel_, "www", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + std::stringstream ss; + ss << result.host_; + EXPECT_EQ("{'www.first.com' aliases=[] addrs=[2.3.4.5]}", ss.str()); +} + +TEST_P(MockEventThreadTest, HostAliasMissingFile) { + DNSPacket yesfirst; + yesfirst.set_response().set_aa() + .add_question(new DNSQuestion("www.first.com", T_A)) + .add_answer(new DNSARR("www.first.com", 0x0200, {2, 3, 4, 5})); + ON_CALL(server_, OnRequest("www.first.com", T_A)) + .WillByDefault(SetReply(&server_, &yesfirst)); + + EnvValue with_env("HOSTALIASES", "bogus.mcfile"); + HostResult result; + ares_gethostbyname(channel_, "www", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + std::stringstream ss; + ss << result.host_; + EXPECT_EQ("{'www.first.com' aliases=[] addrs=[2.3.4.5]}", ss.str()); +} + +TEST_P(MockEventThreadTest, HostAliasUnreadable) { + TempFile aliases("www www.google.com\n"); + EXPECT_EQ(chmod(aliases.filename(), 0), 0); + + /* Perform OS sanity checks. We are observing on Debian after the chmod(fn, 0) + * that we are still able to fopen() the file which is unexpected. Skip the + * test if we observe this behavior */ + struct stat st; + EXPECT_EQ(stat(aliases.filename(), &st), 0); + EXPECT_EQ(st.st_mode & (S_IRWXU|S_IRWXG|S_IRWXO), 0); + FILE *fp = fopen(aliases.filename(), "r"); + if (fp != NULL) { + if (verbose) std::cerr << "Skipping Test due to OS incompatibility (open file caching)" << std::endl; + fclose(fp); + return; + } + + EnvValue with_env("HOSTALIASES", aliases.filename()); + + HostResult result; + ares_gethostbyname(channel_, "www", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_EFILE, result.status_); + chmod(aliases.filename(), 0777); +} +#endif + +class MockMultiServerEventThreadTest + : public MockEventThreadOptsTest, + public ::testing::WithParamInterface< std::tuple > { + public: + MockMultiServerEventThreadTest(bool rotate) + : MockEventThreadOptsTest(3, std::get<0>(GetParam()), std::get<1>(GetParam()), std::get<2>(GetParam()), nullptr, rotate ? ARES_OPT_ROTATE : ARES_OPT_NOROTATE) {} + void CheckExample() { + HostResult result; + ares_gethostbyname(channel_, "www.example.com.", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + std::stringstream ss; + ss << result.host_; + EXPECT_EQ("{'www.example.com' aliases=[] addrs=[2.3.4.5]}", ss.str()); + } +}; + +class NoRotateMultiMockEventThreadTest : public MockMultiServerEventThreadTest { + public: + NoRotateMultiMockEventThreadTest() : MockMultiServerEventThreadTest(false) {} +}; + +TEST_P(NoRotateMultiMockEventThreadTest, ThirdServer) { + struct ares_options opts; + int optmask = 0; + memset(&opts, 0, sizeof(opts)); + EXPECT_EQ(ARES_SUCCESS, ares_save_options(channel_, &opts, &optmask)); + EXPECT_EQ(ARES_OPT_NOROTATE, (optmask & ARES_OPT_NOROTATE)); + ares_destroy_options(&opts); + + DNSPacket servfailrsp; + servfailrsp.set_response().set_aa().set_rcode(SERVFAIL) + .add_question(new DNSQuestion("www.example.com", T_A)); + DNSPacket notimplrsp; + notimplrsp.set_response().set_aa().set_rcode(NOTIMP) + .add_question(new DNSQuestion("www.example.com", T_A)); + DNSPacket okrsp; + okrsp.set_response().set_aa() + .add_question(new DNSQuestion("www.example.com", T_A)) + .add_answer(new DNSARR("www.example.com", 100, {2,3,4,5})); + + EXPECT_CALL(*servers_[0], OnRequest("www.example.com", T_A)) + .WillOnce(SetReply(servers_[0].get(), &servfailrsp)); + EXPECT_CALL(*servers_[1], OnRequest("www.example.com", T_A)) + .WillOnce(SetReply(servers_[1].get(), ¬implrsp)); + EXPECT_CALL(*servers_[2], OnRequest("www.example.com", T_A)) + .WillOnce(SetReply(servers_[2].get(), &okrsp)); + CheckExample(); + + // Second time around, still starts from server [2], as [0] and [1] both + // recorded failures + EXPECT_CALL(*servers_[2], OnRequest("www.example.com", T_A)) + .WillOnce(SetReply(servers_[2].get(), &servfailrsp)); + EXPECT_CALL(*servers_[0], OnRequest("www.example.com", T_A)) + .WillOnce(SetReply(servers_[0].get(), ¬implrsp)); + EXPECT_CALL(*servers_[1], OnRequest("www.example.com", T_A)) + .WillOnce(SetReply(servers_[1].get(), &okrsp)); + CheckExample(); + + // Third time around, server order is [1] (f0), [2] (f1), [0] (f2), which + // means [1] will get called twice in a row as after the first call + // order will be [1] (f1), [2] (f1), [0] (f2) since sort order is + // (failure count, index) + EXPECT_CALL(*servers_[1], OnRequest("www.example.com", T_A)) + .WillOnce(SetReply(servers_[1].get(), &servfailrsp)) + .WillOnce(SetReply(servers_[1].get(), ¬implrsp)); + EXPECT_CALL(*servers_[2], OnRequest("www.example.com", T_A)) + .WillOnce(SetReply(servers_[2].get(), ¬implrsp)); + EXPECT_CALL(*servers_[0], OnRequest("www.example.com", T_A)) + .WillOnce(SetReply(servers_[0].get(), &okrsp)); + CheckExample(); +} + +TEST_P(NoRotateMultiMockEventThreadTest, ServerNoResponseFailover) { + std::vector nothing; + DNSPacket okrsp; + okrsp.set_response().set_aa() + .add_question(new DNSQuestion("www.example.com", T_A)) + .add_answer(new DNSARR("www.example.com", 100, {2,3,4,5})); + + /* Server #1 works fine on first attempt, then acts like its offline on + * second, then backonline on the third. */ + EXPECT_CALL(*servers_[0], OnRequest("www.example.com", T_A)) + .WillOnce(SetReply(servers_[0].get(), &okrsp)) + .WillOnce(SetReplyData(servers_[0].get(), nothing)) + .WillOnce(SetReply(servers_[0].get(), &okrsp)); + + /* Server #2 always acts like its offline */ + ON_CALL(*servers_[1], OnRequest("www.example.com", T_A)) + .WillByDefault(SetReplyData(servers_[1].get(), nothing)); + + /* Server #3 works fine on first and second request, then no reply on 3rd */ + EXPECT_CALL(*servers_[2], OnRequest("www.example.com", T_A)) + .WillOnce(SetReply(servers_[2].get(), &okrsp)) + .WillOnce(SetReply(servers_[2].get(), &okrsp)) + .WillOnce(SetReplyData(servers_[2].get(), nothing)); + + HostResult result; + + /* 1. First server returns a response on the first request immediately, normal + * operation on channel. */ + ares_gethostbyname(channel_, "www.example.com.", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + EXPECT_EQ(0, result.timeouts_); + std::stringstream ss1; + ss1 << result.host_; + EXPECT_EQ("{'www.example.com' aliases=[] addrs=[2.3.4.5]}", ss1.str()); + + /* 2. On the second request, simulate the first and second servers not + * returning a response at all, but the 3rd server works, so should have + * 2 timeouts. */ + ares_gethostbyname(channel_, "www.example.com.", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + EXPECT_EQ(2, result.timeouts_); + std::stringstream ss2; + ss2 << result.host_; + EXPECT_EQ("{'www.example.com' aliases=[] addrs=[2.3.4.5]}", ss2.str()); + + /* 3. On the third request, the active server should be #3, so should respond + * immediately with no timeouts */ + ares_gethostbyname(channel_, "www.example.com.", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + EXPECT_EQ(0, result.timeouts_); + std::stringstream ss3; + ss3 << result.host_; + EXPECT_EQ("{'www.example.com' aliases=[] addrs=[2.3.4.5]}", ss3.str()); + + /* 4. On the fourth request, the active server should be #3, but will timeout, + * and the first server should then respond */ + ares_gethostbyname(channel_, "www.example.com.", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + EXPECT_EQ(1, result.timeouts_); + std::stringstream ss4; + ss4 << result.host_; + EXPECT_EQ("{'www.example.com' aliases=[] addrs=[2.3.4.5]}", ss4.str()); +} + + +static const char *evsys_tostr(ares_evsys_t evsys) +{ + switch (evsys) { + case ARES_EVSYS_WIN32: + return "WIN32"; + case ARES_EVSYS_EPOLL: + return "EPOLL"; + case ARES_EVSYS_KQUEUE: + return "KQUEUE"; + case ARES_EVSYS_POLL: + return "POLL"; + case ARES_EVSYS_SELECT: + return "SELECT"; + case ARES_EVSYS_DEFAULT: + return "DEFAULT"; + } + return "UNKNOWN"; +} + +const char *af_tostr(int af) +{ + switch (af) { + case AF_INET: + return "ipv4"; + case AF_INET6: + return "ipv6"; + } + return "ipunknown"; +} + +const char *mode_tostr(bool mode) +{ + return mode?"ForceTCP":"DefaultUDP"; +} + +static std::string PrintEvsysFamilyMode(const testing::TestParamInfo> &info) +{ + std::string name; + + name += evsys_tostr(std::get<0>(info.param)); + name += "_"; + name += af_tostr(std::get<1>(info.param)); + name += "_"; + name += mode_tostr(std::get<2>(info.param)); + return name; +} + +static std::string PrintEvsysFamily(const testing::TestParamInfo> &info) +{ + std::string name; + + name += evsys_tostr(std::get<0>(info.param)); + name += "_"; + name += af_tostr(std::get<1>(info.param)); + return name; +} + +INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockEventThreadTest, ::testing::ValuesIn(ares::test::evsys_families_modes), ares::test::PrintEvsysFamilyMode); + +INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockUDPEventThreadTest, ::testing::ValuesIn(ares::test::evsys_families), ares::test::PrintEvsysFamily); + +INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockUDPEventThreadMaxQueriesTest, ::testing::ValuesIn(ares::test::evsys_families), ares::test::PrintEvsysFamily); + +INSTANTIATE_TEST_SUITE_P(AddressFamilies, CacheQueriesEventThreadTest, ::testing::ValuesIn(ares::test::evsys_families), ares::test::PrintEvsysFamily); + +INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockTCPEventThreadTest, ::testing::ValuesIn(ares::test::evsys_families), ares::test::PrintEvsysFamily); + +INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockExtraOptsEventThreadTest, ::testing::ValuesIn(ares::test::evsys_families_modes), ares::test::PrintEvsysFamilyMode); + +INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockNoCheckRespEventThreadTest, ::testing::ValuesIn(ares::test::evsys_families_modes), ares::test::PrintEvsysFamilyMode); + +INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockEDNSEventThreadTest, ::testing::ValuesIn(ares::test::evsys_families_modes), ares::test::PrintEvsysFamilyMode); + +INSTANTIATE_TEST_SUITE_P(TransportModes, NoRotateMultiMockEventThreadTest, ::testing::ValuesIn(ares::test::evsys_families_modes), ares::test::PrintEvsysFamilyMode); + +} // namespace test +} // namespace ares + +#endif /* CARES_THREADS */ diff --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc index 849eb698..6725725a 100644 --- a/test/ares-test-mock.cc +++ b/test/ares-test-mock.cc @@ -1442,23 +1442,41 @@ TEST_P(NoRotateMultiMockTest, ServerNoResponseFailover) { EXPECT_EQ("{'www.example.com' aliases=[] addrs=[2.3.4.5]}", ss4.str()); } -INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockChannelTest, ::testing::ValuesIn(ares::test::families_modes)); +std::string PrintFamilyMode(const testing::TestParamInfo> &info) +{ + std::string name; + + name += af_tostr(std::get<0>(info.param)); + name += "_"; + name += mode_tostr(std::get<1>(info.param)); + return name; +} + +std::string PrintFamily(const testing::TestParamInfo &info) +{ + std::string name; + + name += af_tostr(info.param); + return name; +} + +INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockChannelTest, ::testing::ValuesIn(ares::test::families_modes), PrintFamilyMode); -INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockUDPChannelTest, ::testing::ValuesIn(ares::test::families)); +INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockUDPChannelTest, ::testing::ValuesIn(ares::test::families), PrintFamily); -INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockUDPMaxQueriesTest, ::testing::ValuesIn(ares::test::families)); +INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockUDPMaxQueriesTest, ::testing::ValuesIn(ares::test::families), PrintFamily); -INSTANTIATE_TEST_SUITE_P(AddressFamilies, CacheQueriesTest, ::testing::ValuesIn(ares::test::families)); +INSTANTIATE_TEST_SUITE_P(AddressFamilies, CacheQueriesTest, ::testing::ValuesIn(ares::test::families), PrintFamily); -INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockTCPChannelTest, ::testing::ValuesIn(ares::test::families)); +INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockTCPChannelTest, ::testing::ValuesIn(ares::test::families), PrintFamily); -INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockExtraOptsTest, ::testing::ValuesIn(ares::test::families_modes)); +INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockExtraOptsTest, ::testing::ValuesIn(ares::test::families_modes), PrintFamilyMode); -INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockNoCheckRespChannelTest, ::testing::ValuesIn(ares::test::families_modes)); +INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockNoCheckRespChannelTest, ::testing::ValuesIn(ares::test::families_modes), PrintFamilyMode); -INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockEDNSChannelTest, ::testing::ValuesIn(ares::test::families_modes)); +INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockEDNSChannelTest, ::testing::ValuesIn(ares::test::families_modes), PrintFamilyMode); -INSTANTIATE_TEST_SUITE_P(TransportModes, NoRotateMultiMockTest, ::testing::ValuesIn(ares::test::families_modes)); +INSTANTIATE_TEST_SUITE_P(TransportModes, NoRotateMultiMockTest, ::testing::ValuesIn(ares::test::families_modes), PrintFamilyMode); } // namespace test } // namespace ares diff --git a/test/ares-test.cc b/test/ares-test.cc index f9c046f1..29621c61 100644 --- a/test/ares-test.cc +++ b/test/ares-test.cc @@ -92,12 +92,161 @@ const std::vector> ipv6_family_both_modes = { std::make_pair(AF_INET6, true) }; + +const std::vector> all_evsys_ipv4_family_both_modes = { +#ifdef _WIN32 + std::make_tuple(ARES_EVSYS_WIN32, AF_INET, false), + std::make_tuple(ARES_EVSYS_WIN32, AF_INET, true), +#endif +#ifdef HAVE_KQUEUE + std::make_tuple(ARES_EVSYS_KQUEUE, AF_INET, false), + std::make_tuple(ARES_EVSYS_KQUEUE, AF_INET, true), +#endif +#ifdef HAVE_EPOLL + std::make_tuple(ARES_EVSYS_EPOLL, AF_INET, false), + std::make_tuple(ARES_EVSYS_EPOLL, AF_INET, true), +#endif +#ifdef HAVE_POLL + std::make_tuple(ARES_EVSYS_POLL, AF_INET, false), + std::make_tuple(ARES_EVSYS_POLL, AF_INET, true), +#endif +#ifdef HAVE_PIPE + std::make_tuple(ARES_EVSYS_SELECT, AF_INET, false), + std::make_tuple(ARES_EVSYS_SELECT, AF_INET, true), +#endif +}; + +const std::vector> all_evsys_ipv6_family_both_modes = { +#ifdef _WIN32 + std::make_tuple(ARES_EVSYS_WIN32, AF_INET6, false), + std::make_tuple(ARES_EVSYS_WIN32, AF_INET6, true), +#endif +#ifdef HAVE_KQUEUE + std::make_tuple(ARES_EVSYS_KQUEUE, AF_INET6, false), + std::make_tuple(ARES_EVSYS_KQUEUE, AF_INET6, true), +#endif +#ifdef HAVE_EPOLL + std::make_tuple(ARES_EVSYS_EPOLL, AF_INET6, false), + std::make_tuple(ARES_EVSYS_EPOLL, AF_INET6, true), +#endif +#ifdef HAVE_POLL + std::make_tuple(ARES_EVSYS_POLL, AF_INET6, false), + std::make_tuple(ARES_EVSYS_POLL, AF_INET6, true), +#endif +#ifdef HAVE_PIPE + std::make_tuple(ARES_EVSYS_SELECT, AF_INET6, false), + std::make_tuple(ARES_EVSYS_SELECT, AF_INET6, true), +#endif +}; + +const std::vector> all_evsys_both_families_both_modes = { +#ifdef _WIN32 + std::make_tuple(ARES_EVSYS_WIN32, AF_INET, false), + std::make_tuple(ARES_EVSYS_WIN32, AF_INET, true), + std::make_tuple(ARES_EVSYS_WIN32, AF_INET6, false), + std::make_tuple(ARES_EVSYS_WIN32, AF_INET6, true), +#endif +#ifdef HAVE_KQUEUE + std::make_tuple(ARES_EVSYS_KQUEUE, AF_INET, false), + std::make_tuple(ARES_EVSYS_KQUEUE, AF_INET, true), + std::make_tuple(ARES_EVSYS_KQUEUE, AF_INET6, false), + std::make_tuple(ARES_EVSYS_KQUEUE, AF_INET6, true), +#endif +#ifdef HAVE_EPOLL + std::make_tuple(ARES_EVSYS_EPOLL, AF_INET, false), + std::make_tuple(ARES_EVSYS_EPOLL, AF_INET, true), + std::make_tuple(ARES_EVSYS_EPOLL, AF_INET6, false), + std::make_tuple(ARES_EVSYS_EPOLL, AF_INET6, true), +#endif +#ifdef HAVE_POLL + std::make_tuple(ARES_EVSYS_POLL, AF_INET, false), + std::make_tuple(ARES_EVSYS_POLL, AF_INET, true), + std::make_tuple(ARES_EVSYS_POLL, AF_INET6, false), + std::make_tuple(ARES_EVSYS_POLL, AF_INET6, true), +#endif +#ifdef HAVE_PIPE + std::make_tuple(ARES_EVSYS_SELECT, AF_INET, false), + std::make_tuple(ARES_EVSYS_SELECT, AF_INET, true), + std::make_tuple(ARES_EVSYS_SELECT, AF_INET6, false), + std::make_tuple(ARES_EVSYS_SELECT, AF_INET6, true), +#endif +}; + + +std::vector> evsys_families_modes = all_evsys_both_families_both_modes; + + +const std::vector> all_evsys_ipv4_family = { +#ifdef _WIN32 + std::make_tuple(ARES_EVSYS_WIN32, AF_INET), +#endif +#ifdef HAVE_KQUEUE + std::make_tuple(ARES_EVSYS_KQUEUE, AF_INET), +#endif +#ifdef HAVE_EPOLL + std::make_tuple(ARES_EVSYS_EPOLL, AF_INET), +#endif +#ifdef HAVE_POLL + std::make_tuple(ARES_EVSYS_POLL, AF_INET), +#endif +#ifdef HAVE_PIPE + std::make_tuple(ARES_EVSYS_SELECT, AF_INET), +#endif +}; + +const std::vector> all_evsys_ipv6_family = { +#ifdef _WIN32 + std::make_tuple(ARES_EVSYS_WIN32, AF_INET6), +#endif +#ifdef HAVE_KQUEUE + std::make_tuple(ARES_EVSYS_KQUEUE, AF_INET6), +#endif +#ifdef HAVE_EPOLL + std::make_tuple(ARES_EVSYS_EPOLL, AF_INET6), +#endif +#ifdef HAVE_POLL + std::make_tuple(ARES_EVSYS_POLL, AF_INET6), +#endif +#ifdef HAVE_PIPE + std::make_tuple(ARES_EVSYS_SELECT, AF_INET6), +#endif +}; + +const std::vector> all_evsys_both_families = { +#ifdef _WIN32 + std::make_tuple(ARES_EVSYS_WIN32, AF_INET), + std::make_tuple(ARES_EVSYS_WIN32, AF_INET6), +#endif +#ifdef HAVE_KQUEUE + std::make_tuple(ARES_EVSYS_KQUEUE, AF_INET), + std::make_tuple(ARES_EVSYS_KQUEUE, AF_INET6), +#endif +#ifdef HAVE_EPOLL + std::make_tuple(ARES_EVSYS_EPOLL, AF_INET), + std::make_tuple(ARES_EVSYS_EPOLL, AF_INET6), +#endif +#ifdef HAVE_POLL + std::make_tuple(ARES_EVSYS_POLL, AF_INET), + std::make_tuple(ARES_EVSYS_POLL, AF_INET6), +#endif +#ifdef HAVE_PIPE + std::make_tuple(ARES_EVSYS_SELECT, AF_INET), + std::make_tuple(ARES_EVSYS_SELECT, AF_INET6), +#endif +}; + + + +std::vector> evsys_families = all_evsys_both_families; + + // Which parameters to use in tests std::vector families = both_families; std::vector> families_modes = both_families_both_modes; unsigned long long LibraryTest::fails_ = 0; std::map LibraryTest::size_fails_; +std::mutex LibraryTest::lock_; void ProcessWork(ares_channel_t *channel, std::function()> get_extrafds, @@ -188,33 +337,132 @@ void ProcessWork(ares_channel_t *channel, } } +void ProcessWorkEventThread(ares_channel_t *channel, + std::function()> get_extrafds, + std::function process_extra, + unsigned int cancel_ms) { + int nfds=0, count; + fd_set readers; + size_t retry_cnt = 1; + +#ifndef CARES_SYMBOL_HIDING + struct timeval tv_begin = ares__tvnow(); + struct timeval tv_cancel = tv_begin; + + if (cancel_ms) { + if (verbose) std::cerr << "ares_cancel will be called after " << cancel_ms << "ms" << std::endl; + tv_cancel.tv_sec += (cancel_ms / 1000); + tv_cancel.tv_usec += ((cancel_ms % 1000) * 1000); + } +#else + if (cancel_ms) { + std::cerr << "library built with symbol hiding, can't test with cancel support" << std::endl; + return; + } +#endif + + while (true) { +#ifndef CARES_SYMBOL_HIDING + struct timeval tv_now = ares__tvnow(); + struct timeval tv_remaining; +#endif + struct timeval tv; + + /* c-ares is using its own event thread, so we only need to monitor the + * extrafds passed in */ + FD_ZERO(&readers); + std::set extrafds = get_extrafds(); + for (ares_socket_t extrafd : extrafds) { + FD_SET(extrafd, &readers); + if (extrafd >= (ares_socket_t)nfds) { + nfds = (int)extrafd + 1; + } + } + + /* If ares_timeout returns NULL, it means there are no requests in queue, + * so we can break out, but lets loop one additional time just incase we + * have some weird multithreading issue where a result hasn't yet been + * delivered. This is really just an odd case, its not "normal" to try + * to determine if an event has been delivered by solely monitoring the + * channel, really we should know how many callbacks we expect and how + * many we get, but that's not easy to do in a test framework. */ + if (ares_timeout(channel, NULL, &tv) == NULL) { + if (retry_cnt == 0) + return; + retry_cnt--; + } else { + retry_cnt = 1; + } + +#ifndef CARES_SYMBOL_HIDING + if (cancel_ms) { + unsigned int remaining_ms; + ares__timeval_remaining(&tv_remaining, + &tv_now, + &tv_cancel); + remaining_ms = (unsigned int)((tv_remaining.tv_sec * 1000) + (tv_remaining.tv_usec / 1000)); + if (remaining_ms == 0) { + if (verbose) std::cerr << "Issuing ares_cancel()" << std::endl; + ares_cancel(channel); + cancel_ms = 0; /* Disable issuing cancel again */ + } + } +#endif + + /* We just always wait 50ms then recheck. Not doing any complex signalling. */ + tv.tv_sec = 0; + tv.tv_usec = 50000; + + count = select(nfds, &readers, nullptr, nullptr, &tv); + if (count < 0) { + fprintf(stderr, "select() failed, errno %d\n", errno); + return; + } + + // Let the provided callback process any activity on the extra FD. + for (ares_socket_t extrafd : extrafds) { + if (FD_ISSET(extrafd, &readers)) { + process_extra(extrafd); + } + } + } +} + // static void LibraryTest::SetAllocFail(int nth) { + lock_.lock(); assert(nth > 0); assert(nth <= (int)(8 * sizeof(fails_))); fails_ |= (1LL << (nth - 1)); + lock_.unlock(); } // static void LibraryTest::SetAllocSizeFail(size_t size) { + lock_.lock(); size_fails_[size]++; + lock_.unlock(); } // static void LibraryTest::ClearFails() { + lock_.lock(); fails_ = 0; size_fails_.clear(); + lock_.unlock(); } // static bool LibraryTest::ShouldAllocFail(size_t size) { + lock_.lock(); bool fail = (fails_ & 0x01); fails_ >>= 1; if (size_fails_[size] > 0) { size_fails_[size]--; fail = true; } + lock_.unlock(); return fail; } @@ -647,6 +895,14 @@ void MockChannelOptsTest::Process(unsigned int cancel_ms) { cancel_ms); } +void MockEventThreadOptsTest::Process(unsigned int cancel_ms) { + using namespace std::placeholders; + ProcessWorkEventThread(channel_, + std::bind(&MockEventThreadOptsTest::fds, this), + std::bind(&MockEventThreadOptsTest::ProcessFD, this, _1), + cancel_ms); +} + std::ostream& operator<<(std::ostream& os, const HostResult& result) { os << '{'; if (result.done_) { diff --git a/test/ares-test.h b/test/ares-test.h index 7fbf85cc..ebeec50b 100644 --- a/test/ares-test.h +++ b/test/ares-test.h @@ -49,6 +49,7 @@ #include #include #include +#include #include #include @@ -68,9 +69,20 @@ extern const std::vector> both_families_both_modes; extern const std::vector> ipv4_family_both_modes; extern const std::vector> ipv6_family_both_modes; +extern const std::vector> all_evsys_ipv4_family_both_modes; +extern const std::vector> all_evsys_ipv6_family_both_modes; +extern const std::vector> all_evsys_both_families_both_modes; + +extern const std::vector> all_evsys_ipv4_family; +extern const std::vector> all_evsys_ipv6_family; +extern const std::vector> all_evsys_both_families; + // Which parameters to use in tests -extern std::vector families; -extern std::vector> families_modes; +extern std::vector families; +extern std::vector> evsys_families; +extern std::vector> families_modes; +extern std::vector> evsys_families_modes; + // Process all pending work on ares-owned file descriptors, plus // optionally the given set-of-FDs + work function. @@ -78,8 +90,19 @@ void ProcessWork(ares_channel_t *cha std::function()> get_extrafds, std::function process_extra, unsigned int cancel_ms = 0); +void ProcessWorkEventThread(ares_channel_t *channel, + std::function()> get_extrafds, + std::function process_extra, + unsigned int cancel_ms); std::set NoExtraFDs(); +const char *af_tostr(int af); +const char *mode_tostr(bool mode); +std::string PrintFamilyMode(const testing::TestParamInfo> &info); +std::string PrintFamily(const testing::TestParamInfo &info); + + + // Test fixture that ensures library initialization, and allows // memory allocations to be failed. class LibraryTest : public ::testing::Test { @@ -113,6 +136,7 @@ private: static bool ShouldAllocFail(size_t size); static unsigned long long fails_; static std::map size_fails_; + static std::mutex lock_; }; // Test fixture that uses a default channel. @@ -324,6 +348,59 @@ public: } }; + +class MockEventThreadOptsTest : public MockChannelOptsTest { +public: + MockEventThreadOptsTest(int count, ares_evsys_t evsys, int family, bool force_tcp, + struct ares_options *givenopts, int optmask) + : MockChannelOptsTest(count, family, force_tcp, FillOptionsET(&evopts_, givenopts, evsys), optmask | ARES_OPT_EVENT_THREAD) + { + } + + void Process(unsigned int cancel_ms = 0); + + static struct ares_options *FillOptionsET(struct ares_options *opts, struct ares_options *givenopts, ares_evsys_t evsys) { + if (givenopts) { + memcpy(opts, givenopts, sizeof(*opts)); + } else { + memset(opts, 0, sizeof(*opts)); + } + opts->evsys = evsys; + return opts; + } + +private: + struct ares_options evopts_; +}; + +class MockEventThreadTest + : public MockEventThreadOptsTest, + public ::testing::WithParamInterface> { +public: + MockEventThreadTest() + : MockEventThreadOptsTest(1, std::get<0>(GetParam()), std::get<1>(GetParam()), std::get<2>(GetParam()), nullptr, 0) + { + } +}; + +class MockUDPEventThreadTest : public MockEventThreadOptsTest, + public ::testing::WithParamInterface> { +public: + MockUDPEventThreadTest() : MockEventThreadOptsTest(1, std::get<0>(GetParam()), std::get<1>(GetParam()), false, nullptr, 0) + { + } +}; + +class MockTCPEventThreadTest : public MockEventThreadOptsTest, + public ::testing::WithParamInterface> { +public: + MockTCPEventThreadTest() : MockEventThreadOptsTest(1, std::get<0>(GetParam()), std::get<1>(GetParam()), true, nullptr, 0) + { + } +}; + + + // gMock action to set the reply for a mock server. ACTION_P2(SetReplyData, mockserver, data) {