Merge branch '1250-socket-listener-leak' into 'main'

gsocketlistener: Fix IPv4 listen() error handling

Closes #1250

See merge request GNOME/glib!643
This commit is contained in:
Philip Withnall 2025-04-03 15:15:59 +00:00
commit 1bf83890a0
2 changed files with 596 additions and 82 deletions

View File

@ -420,6 +420,14 @@ g_socket_listener_add_address (GSocketListener *listener,
* creates a TCP/IP socket listening on IPv4 and IPv6 (if
* supported) on the specified port on all interfaces.
*
* If possible, the [class@Gio.SocketListener] will listen on both IPv4 and
* IPv6 (listening on the same port on both). If listening on one of the socket
* families fails, the [class@Gio.SocketListener] will only listen on the other.
* If listening on both fails, an error will be returned.
*
* If you need to distinguish whether listening on IPv4 or IPv6 or both was
* successful, connect to [signal@Gio.SocketListener::event].
*
* @source_object will be passed out in the various calls
* to accept to identify this particular source, which is
* useful if you're listening on multiple addresses and do
@ -442,6 +450,8 @@ g_socket_listener_add_inet_port (GSocketListener *listener,
gboolean need_ipv4_socket = TRUE;
GSocket *socket4 = NULL;
GSocket *socket6;
GError *socket4_create_error = NULL;
GError *socket4_listen_error = NULL, *socket6_listen_error = NULL;
g_return_val_if_fail (listener != NULL, FALSE);
g_return_val_if_fail (port != 0, FALSE);
@ -487,23 +497,20 @@ g_socket_listener_add_inet_port (GSocketListener *listener,
g_signal_emit (listener, signals[EVENT], 0,
G_SOCKET_LISTENER_LISTENING, socket6);
if (!g_socket_listen (socket6, error))
if (g_socket_listen (socket6, &socket6_listen_error))
{
g_object_unref (socket6);
return FALSE;
g_signal_emit (listener, signals[EVENT], 0,
G_SOCKET_LISTENER_LISTENED, socket6);
if (source_object)
g_object_set_qdata_full (G_OBJECT (socket6), source_quark,
g_object_ref (source_object),
g_object_unref);
/* If this socket already speaks IPv4 then we are done. */
if (g_socket_speaks_ipv4 (socket6))
need_ipv4_socket = FALSE;
}
g_signal_emit (listener, signals[EVENT], 0,
G_SOCKET_LISTENER_LISTENED, socket6);
if (source_object)
g_object_set_qdata_full (G_OBJECT (socket6), source_quark,
g_object_ref (source_object),
g_object_unref);
/* If this socket already speaks IPv4 then we are done. */
if (g_socket_speaks_ipv4 (socket6))
need_ipv4_socket = FALSE;
}
if (need_ipv4_socket)
@ -519,7 +526,7 @@ g_socket_listener_add_inet_port (GSocketListener *listener,
socket4 = g_socket_new (G_SOCKET_FAMILY_IPV4,
G_SOCKET_TYPE_STREAM,
G_SOCKET_PROTOCOL_DEFAULT,
error);
&socket4_create_error);
if (socket4 != NULL)
/* IPv4 is supported on this platform, so if we fail now it is
@ -542,10 +549,10 @@ g_socket_listener_add_inet_port (GSocketListener *listener,
if (!g_socket_bind (socket4, address, TRUE, error))
{
g_object_unref (address);
g_object_unref (socket4);
if (socket6 != NULL)
g_object_unref (socket6);
g_clear_object (&address);
g_clear_object (&socket4);
g_clear_object (&socket6);
g_clear_error (&socket6_listen_error);
return FALSE;
}
@ -557,22 +564,16 @@ g_socket_listener_add_inet_port (GSocketListener *listener,
g_signal_emit (listener, signals[EVENT], 0,
G_SOCKET_LISTENER_LISTENING, socket4);
if (!g_socket_listen (socket4, error))
if (g_socket_listen (socket4, &socket4_listen_error))
{
g_object_unref (socket4);
if (socket6 != NULL)
g_object_unref (socket6);
g_signal_emit (listener, signals[EVENT], 0,
G_SOCKET_LISTENER_LISTENED, socket4);
return FALSE;
if (source_object)
g_object_set_qdata_full (G_OBJECT (socket4), source_quark,
g_object_ref (source_object),
g_object_unref);
}
g_signal_emit (listener, signals[EVENT], 0,
G_SOCKET_LISTENER_LISTENED, socket4);
if (source_object)
g_object_set_qdata_full (G_OBJECT (socket4), source_quark,
g_object_ref (source_object),
g_object_unref);
}
else
/* Ok. So IPv4 is not supported on this platform. If we
@ -580,13 +581,34 @@ g_socket_listener_add_inet_port (GSocketListener *listener,
* otherwise we need to tell the user we failed.
*/
{
if (socket6 != NULL)
g_clear_error (error);
else
return FALSE;
if (socket6 == NULL || socket6_listen_error != NULL)
{
g_clear_object (&socket6);
g_clear_error (&socket6_listen_error);
g_propagate_error (error, g_steal_pointer (&socket4_create_error));
return FALSE;
}
}
}
/* Only error out if both listen() calls failed. */
if ((socket6 == NULL || socket6_listen_error != NULL) &&
(socket4 == NULL || socket4_listen_error != NULL))
{
GError **set_error = ((socket6_listen_error != NULL) ?
&socket6_listen_error :
&socket4_listen_error);
g_propagate_error (error, g_steal_pointer (set_error));
g_clear_error (&socket6_listen_error);
g_clear_error (&socket4_listen_error);
g_clear_object (&socket6);
g_clear_object (&socket4);
return FALSE;
}
g_assert (socket6 != NULL || socket4 != NULL);
if (socket6 != NULL)
@ -598,6 +620,9 @@ g_socket_listener_add_inet_port (GSocketListener *listener,
if (G_SOCKET_LISTENER_GET_CLASS (listener)->changed)
G_SOCKET_LISTENER_GET_CLASS (listener)->changed (listener);
g_clear_error (&socket6_listen_error);
g_clear_error (&socket4_listen_error);
return TRUE;
}
@ -1042,6 +1067,14 @@ g_socket_listener_close (GSocketListener *listener)
* This is useful if you need to have a socket for incoming connections
* but don't care about the specific port number.
*
* If possible, the [class@Gio.SocketListener] will listen on both IPv4 and
* IPv6 (listening on the same port on both). If listening on one of the socket
* families fails, the [class@Gio.SocketListener] will only listen on the other.
* If listening on both fails, an error will be returned.
*
* If you need to distinguish whether listening on IPv4 or IPv6 or both was
* successful, connect to [signal@Gio.SocketListener::event].
*
* @source_object will be passed out in the various calls
* to accept to identify this particular source, which is
* useful if you're listening on multiple addresses and do
@ -1061,6 +1094,7 @@ g_socket_listener_add_any_inet_port (GSocketListener *listener,
GSocket *socket6 = NULL;
GSocket *socket4 = NULL;
gint attempts = 37;
GError *socket4_listen_error = NULL, *socket6_listen_error = NULL;
/*
* multi-step process:
@ -1102,8 +1136,7 @@ g_socket_listener_add_any_inet_port (GSocketListener *listener,
if (!result ||
!(address = g_socket_get_local_address (socket6, error)))
{
g_object_unref (socket6);
socket6 = NULL;
g_clear_object (&socket6);
break;
}
@ -1175,14 +1208,12 @@ g_socket_listener_add_any_inet_port (GSocketListener *listener,
else
/* we failed to bind to the specified port. try again. */
{
g_object_unref (socket4);
socket4 = NULL;
g_clear_object (&socket4);
/* keep this open so we get a different port number */
sockets_to_close = g_slist_prepend (sockets_to_close,
socket6);
g_steal_pointer (&socket6));
candidate_port = 0;
socket6 = NULL;
}
}
else
@ -1196,8 +1227,7 @@ g_socket_listener_add_any_inet_port (GSocketListener *listener,
if (!result ||
!(address = g_socket_get_local_address (socket4, error)))
{
g_object_unref (socket4);
socket4 = NULL;
g_clear_object (&socket4);
break;
}
@ -1216,14 +1246,11 @@ g_socket_listener_add_any_inet_port (GSocketListener *listener,
/* should only be non-zero if we have a socket */
g_assert ((candidate_port != 0) == (socket4 || socket6));
while (sockets_to_close)
{
g_object_unref (sockets_to_close->data);
sockets_to_close = g_slist_delete_link (sockets_to_close,
sockets_to_close);
}
g_slist_free_full (g_steal_pointer (&sockets_to_close), g_object_unref);
/* now we actually listen() the sockets and add them to the listener */
/* now we actually listen() the sockets and add them to the listener. If
* either of the listen()s fails, only return the other socket. Fail if both
* failed. */
if (socket6 != NULL)
{
g_socket_set_listen_backlog (socket6, listener->priv->listen_backlog);
@ -1231,24 +1258,22 @@ g_socket_listener_add_any_inet_port (GSocketListener *listener,
g_signal_emit (listener, signals[EVENT], 0,
G_SOCKET_LISTENER_LISTENING, socket6);
if (!g_socket_listen (socket6, error))
if (g_socket_listen (socket6, &socket6_listen_error))
{
g_object_unref (socket6);
if (socket4)
g_object_unref (socket4);
g_signal_emit (listener, signals[EVENT], 0,
G_SOCKET_LISTENER_LISTENED, socket6);
return 0;
if (source_object)
g_object_set_qdata_full (G_OBJECT (socket6), source_quark,
g_object_ref (source_object),
g_object_unref);
g_ptr_array_add (listener->priv->sockets, socket6);
}
else
{
g_clear_object (&socket6);
}
g_signal_emit (listener, signals[EVENT], 0,
G_SOCKET_LISTENER_LISTENED, socket6);
if (source_object)
g_object_set_qdata_full (G_OBJECT (socket6), source_quark,
g_object_ref (source_object),
g_object_unref);
g_ptr_array_add (listener->priv->sockets, socket6);
}
if (socket4 != NULL)
@ -1258,26 +1283,46 @@ g_socket_listener_add_any_inet_port (GSocketListener *listener,
g_signal_emit (listener, signals[EVENT], 0,
G_SOCKET_LISTENER_LISTENING, socket4);
if (!g_socket_listen (socket4, error))
if (g_socket_listen (socket4, &socket4_listen_error))
{
g_object_unref (socket4);
if (socket6)
g_object_unref (socket6);
g_signal_emit (listener, signals[EVENT], 0,
G_SOCKET_LISTENER_LISTENED, socket4);
return 0;
if (source_object)
g_object_set_qdata_full (G_OBJECT (socket4), source_quark,
g_object_ref (source_object),
g_object_unref);
g_ptr_array_add (listener->priv->sockets, socket4);
}
else
{
g_clear_object (&socket4);
}
g_signal_emit (listener, signals[EVENT], 0,
G_SOCKET_LISTENER_LISTENED, socket4);
if (source_object)
g_object_set_qdata_full (G_OBJECT (socket4), source_quark,
g_object_ref (source_object),
g_object_unref);
g_ptr_array_add (listener->priv->sockets, socket4);
}
/* Error out if both listen() calls failed (or if theres no separate IPv4
* socket and the IPv6 listen() call failed). */
if (socket6_listen_error != NULL &&
(socket4 == NULL || socket4_listen_error != NULL))
{
GError **set_error = ((socket6_listen_error != NULL) ?
&socket6_listen_error :
&socket4_listen_error);
g_propagate_error (error, g_steal_pointer (set_error));
g_clear_error (&socket6_listen_error);
g_clear_error (&socket4_listen_error);
g_clear_object (&socket6);
g_clear_object (&socket4);
return 0;
}
g_clear_error (&socket6_listen_error);
g_clear_error (&socket4_listen_error);
if ((socket4 != NULL || socket6 != NULL) &&
G_SOCKET_LISTENER_GET_CLASS (listener)->changed)
G_SOCKET_LISTENER_GET_CLASS (listener)->changed (listener);

View File

@ -1,6 +1,7 @@
/* GLib testing framework examples and tests
*
* Copyright 2014 Red Hat, Inc.
* Copyright 2025 GNOME Foundation, Inc.
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*
@ -19,8 +20,106 @@
* <http://www.gnu.org/licenses/>.
*/
#include "config.h"
#include <gio/gio.h>
#ifdef HAVE_RTLD_NEXT
#include <dlfcn.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdint.h>
#include <sys/socket.h>
#endif
/* Override the socket(), bind(), listen() and getsockopt() functions from libc
* so that we can mock results from them in the tests. The libc implementations
* are used by default (via `dlsym()`) unless a test sets a callback
* deliberately.
*
* These override functions are used simply because the linker will resolve them
* before it finds the symbols in libc. This is effectively like `LD_PRELOAD`,
* except without using an external library for them.
*
* This mechanism is intended to be generic and not to force tests in this file
* to be written in a certain way. Tests are free to override these functions
* with their own implementations, or leave them as default. Different tests may
* need to mock these socket functions differently.
*
* If a test overrides these functions, it *must* do so at the start of the test
* (before starting any threads), and *must* clear them to `NULL` at the end of
* the test. The overrides are not thread-safe and will not be automatically
* cleared at the end of a test.
*/
/* FIXME: Not currently supported on macOS as its symbol lookup works
* differently to Linux. It will likely need something like DYLD_INTERPOSE()
* from getpwuid-preload.c here to work. At that point, this common code for
* mocking arbitrary syscalls using dlsym(RTLD_NEXT) should probably be factored
* out into a set of internal helpers, because various tests do it for various
* syscalls. */
#if defined(HAVE_RTLD_NEXT) && !defined(__APPLE__)
#define MOCK_SOCKET_SUPPORTED
#endif
#ifdef MOCK_SOCKET_SUPPORTED
static int (*real_socket) (int, int, int);
static int (*real_bind) (int, const struct sockaddr *, socklen_t);
static int (*real_listen) (int, int);
static int (*real_getsockopt) (int, int, int, void *, socklen_t *);
static int (*mock_socket) (int, int, int);
static int (*mock_bind) (int, const struct sockaddr *, socklen_t);
static int (*mock_listen) (int, int);
static int (*mock_getsockopt) (int, int, int, void *, socklen_t *);
int
socket (int domain,
int type,
int protocol)
{
if (real_socket == NULL)
real_socket = dlsym (RTLD_NEXT, "socket");
return ((mock_socket != NULL) ? mock_socket : real_socket) (domain, type, protocol);
}
int
bind (int sockfd,
const struct sockaddr *addr,
socklen_t addrlen)
{
if (real_bind == NULL)
real_bind = dlsym (RTLD_NEXT, "bind");
return ((mock_bind != NULL) ? mock_bind : real_bind) (sockfd, addr, addrlen);
}
int
listen (int sockfd,
int backlog)
{
if (real_listen == NULL)
real_listen = dlsym (RTLD_NEXT, "listen");
return ((mock_listen != NULL) ? mock_listen : real_listen) (sockfd, backlog);
}
int
getsockopt (int sockfd,
int level,
int optname,
void *optval,
socklen_t *optlen)
{
if (real_getsockopt == NULL)
real_getsockopt = dlsym (RTLD_NEXT, "getsockopt");
return ((mock_getsockopt != NULL) ? mock_getsockopt : real_getsockopt) (sockfd, level, optname, optval, optlen);
}
#endif /* MOCK_SOCKET_SUPPORTED */
/* Test event signals. */
static void
event_cb (GSocketListener *listener,
GSocketListenerEvent event,
@ -82,6 +181,374 @@ test_event_signal (void)
g_object_unref (listener);
}
/* Provide a mock implementation of socket(), listen(), bind() and getsockopt()
* which use a simple fixed configuration to either force a call to fail with a
* given errno, or allow it to pass through to the system implementation (which
* is assumed to succeed). Results are differentiated by protocol (IPv4 or IPv6)
* but nothing more complex than that.
*
* This allows the `listen()` fallback code in
* `g_socket_listener_add_any_inet_port()` and
* `g_socket_listener_add_inet_port()` to be tested for different situations
* where IPv4 and/or IPv6 sockets dont work. It doesnt allow the port
* allocation retry logic to be tested (as it forces all IPv6 `bind()` calls to
* have the same result), but this means it doesnt have to do more complex
* state tracking of fully mocked-up sockets.
*
* It also means that the test wont work on systems which dont support IPv6,
* or which have a configuration which causes the first test case (which passes
* all syscalls through to the system) to fail. On those systems, the test
* should be skipped rather than the mock made more complex.
*/
#ifdef MOCK_SOCKET_SUPPORTED
typedef struct {
gboolean ipv6_socket_supports_ipv4;
int ipv4_socket_errno; /* 0 for socket() to succeed on the IPv4 socket (i.e. IPv4 sockets are supported) */
int ipv6_socket_errno; /* similarly */
int ipv4_bind_errno; /* 0 for bind() to succeed on the IPv4 socket */
int ipv6_bind_errno; /* similarly */
int ipv4_listen_errno; /* 0 for listen() to succeed on the IPv4 socket */
int ipv6_listen_errno; /* similarly */
} ListenFailuresConfig;
static struct {
/* Input: */
ListenFailuresConfig config;
/* State (we only support tracking one socket of each type): */
int ipv4_socket_fd;
int ipv6_socket_fd;
} listen_failures_mock_state;
static int
listen_failures_socket (int domain,
int type,
int protocol)
{
int fd;
/* Error out if told to */
if (domain == AF_INET && listen_failures_mock_state.config.ipv4_socket_errno != 0)
{
errno = listen_failures_mock_state.config.ipv4_socket_errno;
return -1;
}
else if (domain == AF_INET6 && listen_failures_mock_state.config.ipv6_socket_errno != 0)
{
errno = listen_failures_mock_state.config.ipv6_socket_errno;
return -1;
}
else if (domain != AF_INET && domain != AF_INET6)
{
/* we dont expect to support other socket types */
g_assert_not_reached ();
}
/* Pass through to the system, which we require to succeed because were only
* mocking errors and not the full socket stack state */
fd = real_socket (domain, type, protocol);
g_assert_no_errno (fd);
/* Track the returned FD for each socket type */
if (domain == AF_INET)
{
g_assert (listen_failures_mock_state.ipv4_socket_fd == 0);
listen_failures_mock_state.ipv4_socket_fd = fd;
}
else if (domain == AF_INET6)
{
g_assert (listen_failures_mock_state.ipv6_socket_fd == 0);
listen_failures_mock_state.ipv6_socket_fd = fd;
}
return fd;
}
static int
listen_failures_bind (int sockfd,
const struct sockaddr *addr,
socklen_t addrlen)
{
int retval;
/* Error out if told to */
if (listen_failures_mock_state.ipv4_socket_fd == sockfd &&
listen_failures_mock_state.config.ipv4_bind_errno != 0)
{
errno = listen_failures_mock_state.config.ipv4_bind_errno;
return -1;
}
else if (listen_failures_mock_state.ipv6_socket_fd == sockfd &&
listen_failures_mock_state.config.ipv6_bind_errno != 0)
{
errno = listen_failures_mock_state.config.ipv6_bind_errno;
return -1;
}
else if (listen_failures_mock_state.ipv4_socket_fd != sockfd &&
listen_failures_mock_state.ipv6_socket_fd != sockfd)
{
g_assert_not_reached ();
}
/* Pass through to the system, which we require to succeed because were only
* mocking errors and not the full socket stack state */
retval = real_bind (sockfd, addr, addrlen);
g_assert_no_errno (retval);
return retval;
}
static int
listen_failures_listen (int sockfd,
int backlog)
{
int retval;
/* Error out if told to */
if (listen_failures_mock_state.ipv4_socket_fd == sockfd &&
listen_failures_mock_state.config.ipv4_listen_errno != 0)
{
errno = listen_failures_mock_state.config.ipv4_listen_errno;
return -1;
}
else if (listen_failures_mock_state.ipv6_socket_fd == sockfd &&
listen_failures_mock_state.config.ipv6_listen_errno != 0)
{
errno = listen_failures_mock_state.config.ipv6_listen_errno;
return -1;
}
else if (listen_failures_mock_state.ipv4_socket_fd != sockfd &&
listen_failures_mock_state.ipv6_socket_fd != sockfd)
{
g_assert_not_reached ();
}
/* Pass through to the system, which we require to succeed because were only
* mocking errors and not the full socket stack state */
retval = real_listen (sockfd, backlog);
g_assert_no_errno (retval);
return retval;
}
static int
listen_failures_getsockopt (int sockfd,
int level,
int optname,
void *optval,
socklen_t *optlen)
{
/* Mock whether IPv6 sockets claim to support IPv4 */
#if defined (IPPROTO_IPV6) && defined (IPV6_V6ONLY)
if (listen_failures_mock_state.ipv6_socket_fd == sockfd &&
level == IPPROTO_IPV6 && optname == IPV6_V6ONLY)
{
int *v6_only = optval;
*v6_only = !listen_failures_mock_state.config.ipv6_socket_supports_ipv4;
return 0;
}
#endif
/* Dont assert that the system getsockopt() succeeded, as it could be used
* in complex ways, and its incidental to what were actually trying to test. */
return real_getsockopt (sockfd, level, optname, optval, optlen);
}
#endif /* MOCK_SOCKET_SUPPORTED */
static void
test_add_any_inet_port_listen_failures (void)
{
#ifdef MOCK_SOCKET_SUPPORTED
const struct
{
ListenFailuresConfig config;
GQuark expected_error_domain; /* 0 if success is expected */
int expected_error_code; /* 0 if success is expected */
}
test_matrix[] =
{
/* If everything works, it should all work: */
{ { TRUE, 0, 0, 0, 0, 0, 0 }, 0, 0 },
/* If IPv4 sockets are not supported, IPv6 should be used: */
{ { TRUE, EAFNOSUPPORT, 0, 0, 0, 0, 0 }, 0, 0 },
/* If IPv6 sockets are not supported, IPv4 should be used: */
{ { TRUE, 0, EAFNOSUPPORT, 0, 0, 0, 0, }, 0, 0 },
/* If no sockets are supported, everything should fail: */
{ { TRUE, EAFNOSUPPORT, EAFNOSUPPORT, 0, 0, 0, 0 },
G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED },
/* If binding IPv4 fails, IPv6 should be used: */
{ { TRUE, 0, 0, EADDRINUSE, 0, 0, 0 }, 0, 0 },
/* If binding IPv6 fails, fail overall (the algorithm is not symmetric): */
{ { TRUE, 0, 0, 0, EADDRINUSE, 0, 0 },
G_IO_ERROR, G_IO_ERROR_ADDRESS_IN_USE },
/* If binding them both fails, fail overall: */
{ { TRUE, 0, 0, EADDRINUSE, EADDRINUSE, 0, 0 },
G_IO_ERROR, G_IO_ERROR_ADDRESS_IN_USE },
/* If listening on IPv4 fails, IPv6 should be used: */
{ { TRUE, 0, 0, 0, 0, EADDRINUSE, 0 }, 0, 0 },
/* If listening on IPv6 fails, IPv4 should be used:
* FIXME: If the IPv6 socket claims to support IPv4, this currently wont
* retry with an IPv4-only socket; see https://gitlab.gnome.org/GNOME/glib/-/issues/3604 */
{ { TRUE, 0, 0, 0, 0, 0, EADDRINUSE },
G_IO_ERROR, G_IO_ERROR_ADDRESS_IN_USE },
/* If listening on IPv6 fails (and the IPv6 socket doesnt claim to
* support IPv4), IPv4 should be used: */
{ { FALSE, 0, 0, 0, 0, 0, EADDRINUSE }, 0, 0 },
/* If listening on both fails, fail overall: */
{ { TRUE, 0, 0, 0, 0, EADDRINUSE, EADDRINUSE },
G_IO_ERROR, G_IO_ERROR_ADDRESS_IN_USE },
};
/* Override the socket(), bind(), listen() and getsockopt() functions */
mock_socket = listen_failures_socket;
mock_bind = listen_failures_bind;
mock_listen = listen_failures_listen;
mock_getsockopt = listen_failures_getsockopt;
g_test_summary ("Test that adding a listening port succeeds if either "
"listening on IPv4 or IPv6 succeeds");
for (size_t i = 0; i < G_N_ELEMENTS (test_matrix); i++)
{
GSocketService *service = NULL;
GError *local_error = NULL;
uint16_t port;
g_test_message ("Test %" G_GSIZE_FORMAT, i);
/* Configure the mock socket behaviour. */
memset (&listen_failures_mock_state, 0, sizeof (listen_failures_mock_state));
listen_failures_mock_state.config = test_matrix[i].config;
/* Create a GSocketService to test. */
service = g_socket_service_new ();
port = g_socket_listener_add_any_inet_port (G_SOCKET_LISTENER (service), NULL, &local_error);
if (test_matrix[i].expected_error_domain == 0)
{
g_assert_no_error (local_error);
g_assert_cmpuint (port, !=, 0);
}
else
{
g_assert_error (local_error, test_matrix[i].expected_error_domain,
test_matrix[i].expected_error_code);
g_assert_cmpuint (port, ==, 0);
}
g_clear_error (&local_error);
g_socket_listener_close (G_SOCKET_LISTENER (service));
g_clear_object (&service);
}
/* Tidy up. */
mock_socket = NULL;
mock_bind = NULL;
mock_listen = NULL;
mock_getsockopt = NULL;
memset (&listen_failures_mock_state, 0, sizeof (listen_failures_mock_state));
#else /* if !MOCK_SOCKET_SUPPORTED */
g_test_skip ("Mock socket not supported");
#endif
}
static void
test_add_inet_port_listen_failures (void)
{
#ifdef MOCK_SOCKET_SUPPORTED
const struct
{
ListenFailuresConfig config;
GQuark expected_error_domain; /* 0 if success is expected */
int expected_error_code; /* 0 if success is expected */
}
test_matrix[] =
{
/* If everything works, it should all work: */
{ { TRUE, 0, 0, 0, 0, 0, 0 }, 0, 0 },
/* If IPv4 sockets are not supported, IPv6 should be used: */
{ { TRUE, EAFNOSUPPORT, 0, 0, 0, 0, 0 }, 0, 0 },
/* If IPv6 sockets are not supported, IPv4 should be used: */
{ { TRUE, 0, EAFNOSUPPORT, 0, 0, 0, 0, }, 0, 0 },
/* If no sockets are supported, everything should fail: */
{ { TRUE, EAFNOSUPPORT, EAFNOSUPPORT, 0, 0, 0, 0 },
G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED },
/* If binding IPv4 fails, IPv6 should be used: */
{ { TRUE, 0, 0, EADDRINUSE, 0, 0, 0 }, 0, 0 },
/* If binding IPv6 fails, fail overall (the algorithm is not symmetric): */
{ { TRUE, 0, 0, 0, EADDRINUSE, 0, 0 },
G_IO_ERROR, G_IO_ERROR_ADDRESS_IN_USE },
/* If binding them both fails, fail overall: */
{ { TRUE, 0, 0, EADDRINUSE, EADDRINUSE, 0, 0 },
G_IO_ERROR, G_IO_ERROR_ADDRESS_IN_USE },
/* If listening on IPv4 fails, IPv6 should be used: */
{ { TRUE, 0, 0, 0, 0, EADDRINUSE, 0 }, 0, 0 },
/* If listening on IPv6 fails, IPv4 should be used: */
{ { TRUE, 0, 0, 0, 0, 0, EADDRINUSE }, 0, 0 },
/* If listening on IPv6 fails (and the IPv6 socket doesnt claim to
* support IPv4), IPv4 should be used: */
{ { FALSE, 0, 0, 0, 0, 0, EADDRINUSE }, 0, 0 },
/* If listening on both fails, fail overall: */
{ { TRUE, 0, 0, 0, 0, EADDRINUSE, EADDRINUSE },
G_IO_ERROR, G_IO_ERROR_ADDRESS_IN_USE },
};
/* Override the socket(), bind(), listen() and getsockopt() functions */
mock_socket = listen_failures_socket;
mock_bind = listen_failures_bind;
mock_listen = listen_failures_listen;
mock_getsockopt = listen_failures_getsockopt;
g_test_summary ("Test that adding a listening port succeeds if either "
"listening on IPv4 or IPv6 succeeds");
for (size_t i = 0; i < G_N_ELEMENTS (test_matrix); i++)
{
GSocketService *service = NULL;
GError *local_error = NULL;
gboolean retval;
g_test_message ("Test %" G_GSIZE_FORMAT, i);
/* Configure the mock socket behaviour. */
memset (&listen_failures_mock_state, 0, sizeof (listen_failures_mock_state));
listen_failures_mock_state.config = test_matrix[i].config;
/* Create a GSocketService to test. */
service = g_socket_service_new ();
retval = g_socket_listener_add_inet_port (G_SOCKET_LISTENER (service), 4321, NULL, &local_error);
if (test_matrix[i].expected_error_domain == 0)
{
g_assert_no_error (local_error);
g_assert_true (retval);
}
else
{
g_assert_error (local_error, test_matrix[i].expected_error_domain,
test_matrix[i].expected_error_code);
g_assert_false (retval);
}
g_clear_error (&local_error);
g_socket_listener_close (G_SOCKET_LISTENER (service));
g_clear_object (&service);
}
/* Tidy up. */
mock_socket = NULL;
mock_bind = NULL;
mock_listen = NULL;
mock_getsockopt = NULL;
memset (&listen_failures_mock_state, 0, sizeof (listen_failures_mock_state));
#else /* if !MOCK_SOCKET_SUPPORTED */
g_test_skip ("Mock socket not supported");
#endif
}
int
main (int argc,
char *argv[])
@ -89,6 +556,8 @@ main (int argc,
g_test_init (&argc, &argv, NULL);
g_test_add_func ("/socket-listener/event-signal", test_event_signal);
g_test_add_func ("/socket-listener/add-any-inet-port/listen-failures", test_add_any_inet_port_listen_failures);
g_test_add_func ("/socket-listener/add-inet-port/listen-failures", test_add_inet_port_listen_failures);
return g_test_run();
}