// SPDX-License-Identifier: GPL-2.0+
/*
* Copyright (C) 2011 Thomas Bechtold <thomasbechtold@jpberlin.de>
* Copyright (C) 2011 Dan Williams <dcbw@redhat.com>
* Copyright (C) 2016 - 2018 Red Hat, Inc.
*/
#include "nm-default.h"
#include "nm-connectivity.h"
#if WITH_CONCHECK
#include <curl/curl.h>
#endif
#include <linux/rtnetlink.h>
#include <glib-unix.h>
#include "c-list/src/c-list.h"
#include "nm-core-internal.h"
#include "nm-config.h"
#include "NetworkManagerUtils.h"
#include "nm-dbus-manager.h"
#include "dns/nm-dns-manager.h"
#define HEADER_STATUS_ONLINE "X-NetworkManager-Status: online\r\n"
/*****************************************************************************/
static
NM_UTILS_LOOKUP_STR_DEFINE (_state_to_string, int /*NMConnectivityState*/,
NM_UTILS_LOOKUP_DEFAULT_WARN ("???"),
NM_UTILS_LOOKUP_STR_ITEM (NM_CONNECTIVITY_UNKNOWN, "UNKNOWN"),
NM_UTILS_LOOKUP_STR_ITEM (NM_CONNECTIVITY_NONE, "NONE"),
NM_UTILS_LOOKUP_STR_ITEM (NM_CONNECTIVITY_LIMITED, "LIMITED"),
NM_UTILS_LOOKUP_STR_ITEM (NM_CONNECTIVITY_PORTAL, "PORTAL"),
NM_UTILS_LOOKUP_STR_ITEM (NM_CONNECTIVITY_FULL, "FULL"),
NM_UTILS_LOOKUP_STR_ITEM (NM_CONNECTIVITY_ERROR, "ERROR"),
NM_UTILS_LOOKUP_STR_ITEM (NM_CONNECTIVITY_FAKE, "FAKE"),
NM_UTILS_LOOKUP_STR_ITEM (NM_CONNECTIVITY_CANCELLED, "CANCELLED"),
NM_UTILS_LOOKUP_STR_ITEM (NM_CONNECTIVITY_DISPOSING, "DISPOSING"),
);
const char *
nm_connectivity_state_to_string (NMConnectivityState state)
{
return _state_to_string (state);
}
/*****************************************************************************/
typedef struct {
guint ref_count;
char *uri;
char *host;
char *port;
char *response;
} ConConfig;
struct _NMConnectivityCheckHandle {
CList handles_lst;
NMConnectivity *self;
NMConnectivityCheckCallback callback;
gpointer user_data;
char *ifspec;
const char *completed_log_message;
char *completed_log_message_free;
#if WITH_CONCHECK
struct {
ConConfig *con_config;
GCancellable *resolve_cancellable;
CURLM *curl_mhandle;
CURL *curl_ehandle;
struct curl_slist *request_headers;
struct curl_slist *hosts;
gsize response_good_cnt;
guint curl_timer;
int ch_ifindex;
} concheck;
#endif
guint64 request_counter;
int addr_family;
guint timeout_id;
NMConnectivityState completed_state;
const char *completed_reason;
};
enum {
CONFIG_CHANGED,
LAST_SIGNAL
};
static guint signals[LAST_SIGNAL] = { 0 };
typedef struct {
CList handles_lst_head;
CList completed_handles_lst_head;
NMConfig *config;
ConConfig *con_config;
guint interval;
bool enabled:1;
bool uri_valid:1;
} NMConnectivityPrivate;
struct _NMConnectivity {
GObject parent;
NMConnectivityPrivate _priv;
};
struct _NMConnectivityClass {
GObjectClass parent;
};
G_DEFINE_TYPE (NMConnectivity, nm_connectivity, G_TYPE_OBJECT)
#define NM_CONNECTIVITY_GET_PRIVATE(self) _NM_GET_PRIVATE (self, NMConnectivity, NM_IS_CONNECTIVITY)
NM_DEFINE_SINGLETON_GETTER (NMConnectivity, nm_connectivity_get, NM_TYPE_CONNECTIVITY);
/*****************************************************************************/
#define _NMLOG_DOMAIN LOGD_CONCHECK
#define _NMLOG(level, ...) __NMLOG_DEFAULT (level, _NMLOG_DOMAIN, "connectivity", __VA_ARGS__)
#define _NMLOG2_DOMAIN LOGD_CONCHECK
#define _NMLOG2(level, ...) \
G_STMT_START { \
const NMLogLevel __level = (level); \
\
if (nm_logging_enabled (__level, _NMLOG2_DOMAIN)) { \
_nm_log (__level, _NMLOG2_DOMAIN, 0, \
(cb_data->ifspec ? &cb_data->ifspec[3] : NULL), \
NULL, \
"connectivity: (%s,IPv%c,%"G_GUINT64_FORMAT") " \
_NM_UTILS_MACRO_FIRST (__VA_ARGS__), \
(cb_data->ifspec ? &cb_data->ifspec[3] : ""), \
nm_utils_addr_family_to_char (cb_data->addr_family), \
cb_data->request_counter \
_NM_UTILS_MACRO_REST (__VA_ARGS__)); \
} \
} G_STMT_END
/*****************************************************************************/
#if WITH_CONCHECK
static ConConfig *
_con_config_ref (ConConfig *con_config)
{
if (con_config) {
nm_assert (con_config->ref_count > 0);
++con_config->ref_count;
}
return con_config;
}
#endif
static void
_con_config_unref (ConConfig *con_config)
{
if (!con_config)
return;
nm_assert (con_config->ref_count > 0);
if (--con_config->ref_count != 0)
return;
g_free (con_config->uri);
g_free (con_config->host);
g_free (con_config->port);
g_free (con_config->response);
g_slice_free (ConConfig, con_config);
}
#if WITH_CONCHECK
static const char *
_con_config_get_response (const ConConfig *con_config)
{
return con_config->response ?: NM_CONFIG_DEFAULT_CONNECTIVITY_RESPONSE;
}
#endif
/*****************************************************************************/
static void
cb_data_complete (NMConnectivityCheckHandle *cb_data,
NMConnectivityState state,
const char *log_message)
{
NMConnectivity *self;
nm_assert (cb_data);
nm_assert (NM_IS_CONNECTIVITY (cb_data->self));
nm_assert (cb_data->callback);
nm_assert (state != NM_CONNECTIVITY_UNKNOWN);
nm_assert (log_message);
self = cb_data->self;
/* mark the handle as completing. After this point, nm_connectivity_check_cancel()
* is no longer possible. */
cb_data->self = NULL;
c_list_unlink_stale (&cb_data->handles_lst);
#if WITH_CONCHECK
if (cb_data->concheck.curl_ehandle) {
/* Contrary to what cURL manual claim it is *not* safe to remove
* the easy handle "at any moment"; specifically it's not safe to
* remove *any* handle from within a libcurl callback. That is
* why we queue completed handles in this case.
*
* cb_data_complete() is however only called *not* from within a
* libcurl callback. So, this is fine. */
curl_easy_setopt (cb_data->concheck.curl_ehandle, CURLOPT_WRITEFUNCTION, NULL);
curl_easy_setopt (cb_data->concheck.curl_ehandle, CURLOPT_WRITEDATA, NULL);
curl_easy_setopt (cb_data->concheck.curl_ehandle, CURLOPT_HEADERFUNCTION, NULL);
curl_easy_setopt (cb_data->concheck.curl_ehandle, CURLOPT_HEADERDATA, NULL);
curl_easy_setopt (cb_data->concheck.curl_ehandle, CURLOPT_PRIVATE, NULL);
curl_easy_setopt (cb_data->concheck.curl_ehandle, CURLOPT_HTTPHEADER, NULL);
curl_multi_remove_handle (cb_data->concheck.curl_mhandle,
cb_data->concheck.curl_ehandle);
curl_easy_cleanup (cb_data->concheck.curl_ehandle);
curl_multi_cleanup (cb_data->concheck.curl_mhandle);
curl_slist_free_all (cb_data->concheck.request_headers);
curl_slist_free_all (cb_data->concheck.hosts);
}
nm_clear_g_source (&cb_data->concheck.curl_timer);
nm_clear_g_cancellable (&cb_data->concheck.resolve_cancellable);
#endif
nm_clear_g_source (&cb_data->timeout_id);
_LOG2D ("check completed: %s; %s",
nm_connectivity_state_to_string (state),
log_message);
cb_data->callback (self,
cb_data,
state,
cb_data->user_data);
/* Note: self might be a danling pointer at this point. It must not be used
* after this point, and all callers must either take a reference first, or
* not use the self pointer too. */
#if WITH_CONCHECK
_con_config_unref (cb_data->concheck.con_config);
#endif
g_free (cb_data->ifspec);
if (cb_data->completed_log_message_free)
g_free (cb_data->completed_log_message_free);
g_slice_free (NMConnectivityCheckHandle, cb_data);
}
/*****************************************************************************/
#if WITH_CONCHECK
static void
cb_data_queue_completed (NMConnectivityCheckHandle *cb_data,
NMConnectivityState state,
const char *log_message_static,
char *log_message_take /* take */)
{
nm_assert (cb_data);
nm_assert (NM_IS_CONNECTIVITY (cb_data->self));
nm_assert (state != NM_CONNECTIVITY_UNKNOWN);
nm_assert (log_message_static || log_message_take);
nm_assert (cb_data->completed_state == NM_CONNECTIVITY_UNKNOWN);
nm_assert (!cb_data->completed_log_message);
nm_assert (c_list_contains (&NM_CONNECTIVITY_GET_PRIVATE (cb_data->self)->handles_lst_head, &cb_data->handles_lst));
cb_data->completed_state = state;
cb_data->completed_log_message = log_message_static ?: log_message_take;
cb_data->completed_log_message_free = log_message_take;
c_list_unlink_stale (&cb_data->handles_lst);
c_list_link_tail (&NM_CONNECTIVITY_GET_PRIVATE (cb_data->self)->completed_handles_lst_head, &cb_data->handles_lst);
}
static void
_complete_queued (NMConnectivity *self)
{
NMConnectivity *self_keep_alive = NULL;
NMConnectivityPrivate *priv = NM_CONNECTIVITY_GET_PRIVATE (self);
NMConnectivityCheckHandle *cb_data;
while ((cb_data = c_list_first_entry (&priv->completed_handles_lst_head, NMConnectivityCheckHandle, handles_lst))) {
if (!self_keep_alive)
self_keep_alive = g_object_ref (self);
cb_data_complete (cb_data,
cb_data->completed_state,
cb_data->completed_log_message);
}
nm_g_object_unref (self_keep_alive);
}
static gboolean
_con_curl_check_connectivity (CURLM *mhandle, int sockfd, int ev_bitmask)
{
NMConnectivityCheckHandle *cb_data;
CURLMsg *msg;
int m_left;
long response_code;
CURLMcode ret;
int running_handles;
gboolean success = TRUE;
ret = curl_multi_socket_action (mhandle, sockfd, ev_bitmask, &running_handles);
if (ret != CURLM_OK) {
_LOGD ("connectivity check failed: (%d) %s", ret, curl_multi_strerror (ret));
success = FALSE;
}
while ((msg = curl_multi_info_read (mhandle, &m_left))) {
const char *response;
CURLcode eret;
if (msg->msg != CURLMSG_DONE)
continue;
/* Here we have completed a session. Check easy session result. */
eret = curl_easy_getinfo (msg->easy_handle, CURLINFO_PRIVATE, (char **) &cb_data);
if (eret != CURLE_OK) {
_LOGD ("curl cannot extract cb_data for easy handle, skipping msg: (%d) %s",
eret, curl_easy_strerror (eret));
success = FALSE;
continue;
}
nm_assert (cb_data);
nm_assert (NM_IS_CONNECTIVITY (cb_data->self));
if (cb_data->completed_state != NM_CONNECTIVITY_UNKNOWN) {
/* callback was already invoked earlier. Nothing to do. */
continue;
}
if (msg->data.result != CURLE_OK) {
cb_data_queue_completed (cb_data,
NM_CONNECTIVITY_LIMITED,
NULL,
g_strdup_printf ("check failed: (%d) %s",
msg->data.result,
curl_easy_strerror (msg->data.result)));
continue;
}
response = _con_config_get_response (cb_data->concheck.con_config);
if ( response[0] == '\0'
&& (curl_easy_getinfo (msg->easy_handle, CURLINFO_RESPONSE_CODE, &response_code) == CURLE_OK)) {
if (response_code == 204) {
/* We expected an empty response, and we got a 204 response code (no content).
* We may or may not have received any content (we would ignore it).
* Anyway, the response_code 204 means we are good. */
cb_data_queue_completed (cb_data,
NM_CONNECTIVITY_FULL,
"no content, as expected",
NULL);
continue;
}
if ( response_code == 200
&& cb_data->concheck.response_good_cnt == 0) {
/* we expected no response, and indeed we got an empty reply (with status code 200) */
cb_data_queue_completed (cb_data,
NM_CONNECTIVITY_FULL,
"empty response, as expected",
NULL);
continue;
}
}
/* If we get here, it means that easy_write_cb() didn't read enough
* bytes to be able to do a match, or that we were asking for no content
* (204 response code) and we actually got some. Either way, that is
* an indication of a captive portal */
cb_data_queue_completed (cb_data,
NM_CONNECTIVITY_PORTAL,
"unexpected short response",
NULL);
}
/* if we return a failure, we don't know what went wrong. It's likely serious, because
* a failure here is not expected. Return FALSE, so that we stop polling the file descriptor.
* Worst case, this leaves the pending connectivity check unhandled, until our regular
* time-out kicks in. */
return success;
}
static gboolean
_con_curl_timeout_cb (gpointer user_data)
{
NMConnectivityCheckHandle *cb_data = user_data;
_con_curl_check_connectivity (cb_data->concheck.curl_mhandle, CURL_SOCKET_TIMEOUT, 0);
_complete_queued (cb_data->self);
return G_SOURCE_CONTINUE;
}
static int
multi_timer_cb (CURLM *multi, long timeout_msec, void *userdata)
{
NMConnectivityCheckHandle *cb_data = userdata;
nm_clear_g_source (&cb_data->concheck.curl_timer);
if (timeout_msec != -1)
cb_data->concheck.curl_timer = g_timeout_add (timeout_msec, _con_curl_timeout_cb, cb_data);
return 0;
}
typedef struct {
NMConnectivityCheckHandle *cb_data;
GSource *source;
/* this is a very simplistic weak-pointer. If ConCurlSockData gets
* destroyed, it will set *destroy_notify to TRUE.
*
* _con_curl_socketevent_cb() uses this to detect whether it can
* safely access @fdp after _con_curl_check_connectivity(). */
gboolean *destroy_notify;
} ConCurlSockData;
static gboolean
_con_curl_socketevent_cb (int fd,
GIOCondition condition,
gpointer user_data)
{
ConCurlSockData *fdp = user_data;
NMConnectivityCheckHandle *cb_data = fdp->cb_data;
int action = 0;
gboolean fdp_destroyed = FALSE;
gboolean success;
if (condition & G_IO_IN)
action |= CURL_CSELECT_IN;
if (condition & G_IO_OUT)
action |= CURL_CSELECT_OUT;
if (condition & G_IO_ERR)
action |= CURL_CSELECT_ERR;
nm_assert (!fdp->destroy_notify);
fdp->destroy_notify = &fdp_destroyed;
success = _con_curl_check_connectivity (cb_data->concheck.curl_mhandle, fd, action);
if (fdp_destroyed) {
/* hups. fdp got invalidated during _con_curl_check_connectivity(). That's fine,
* just don't touch it. */
} else {
nm_assert (fdp->destroy_notify == &fdp_destroyed);
fdp->destroy_notify = NULL;
if (!success)
nm_clear_g_source_inst (&fdp->source);
}
_complete_queued (cb_data->self);
return G_SOURCE_CONTINUE;
}
static int
multi_socket_cb (CURL *e_handle, curl_socket_t fd, int what, void *userdata, void *socketp)
{
NMConnectivityCheckHandle *cb_data = userdata;
ConCurlSockData *fdp = socketp;
(void) _NM_ENSURE_TYPE (int, fd);
if (what == CURL_POLL_REMOVE) {
if (fdp) {
if (fdp->destroy_notify)
*fdp->destroy_notify = TRUE;
nm_clear_g_source_inst (&fdp->source);
curl_multi_assign (cb_data->concheck.curl_mhandle, fd, NULL);
g_slice_free (ConCurlSockData, fdp);
}
} else {
GIOCondition condition;
if (!fdp) {
fdp = g_slice_new (ConCurlSockData);
*fdp = (ConCurlSockData) {
.cb_data = cb_data,
};
curl_multi_assign (cb_data->concheck.curl_mhandle, fd, fdp);
} else
nm_clear_g_source_inst (&fdp->source);
if (what == CURL_POLL_IN)
condition = G_IO_IN;
else if (what == CURL_POLL_OUT)
condition = G_IO_OUT;
else if (what == CURL_POLL_INOUT)
condition = G_IO_IN | G_IO_OUT;
else
condition = 0;
if (condition) {
fdp->source = nm_g_unix_fd_source_new (fd,
condition,
G_PRIORITY_DEFAULT,
_con_curl_socketevent_cb,
fdp,
NULL);
g_source_attach (fdp->source, NULL);
}
}
return CURLM_OK;
}
static size_t
easy_header_cb (char *buffer, size_t size, size_t nitems, void *userdata)
{
NMConnectivityCheckHandle *cb_data = userdata;
size_t len = size * nitems;
if (cb_data->completed_state != NM_CONNECTIVITY_UNKNOWN) {
/* already completed. */
return 0;
}
if ( len >= sizeof (HEADER_STATUS_ONLINE) - 1
&& !g_ascii_strncasecmp (buffer, HEADER_STATUS_ONLINE, sizeof (HEADER_STATUS_ONLINE) - 1)) {
cb_data_queue_completed (cb_data,
NM_CONNECTIVITY_FULL,
"status header found",
NULL);
return 0;
}
return len;
}
static size_t
easy_write_cb (void *buffer, size_t size, size_t nmemb, void *userdata)
{
NMConnectivityCheckHandle *cb_data = userdata;
size_t len = size * nmemb;
size_t response_len;
size_t check_len;
const char *response;
if (cb_data->completed_state != NM_CONNECTIVITY_UNKNOWN) {
/* already completed. */
return 0;
}
if (len == 0) {
/* no data. That can happen, it's fine. */
return len;
}
response = _con_config_get_response (cb_data->concheck.con_config);;
if (response[0] == '\0') {
/* no response expected. We are however graceful and accept any
* extra response that we might receive. We determine the empty
* response based on the status code 204.
*
* Continue receiving... */
cb_data->concheck.response_good_cnt += len;
if (cb_data->concheck.response_good_cnt > (gsize) (100 * 1024)) {
/* we expect an empty response. We accept either
* 1) status code 204 and any response
* 2) status code 200 and an empty response.
*
* Here, we want to continue receiving data, to see whether we have
* case 1). Arguably, the server shouldn't send us 204 with a non-empty
* response, but we accept that also with a non-empty response, so
* keep receiving.
*
* However, if we get an excessive amount of data, we put a stop on it
* and fail. */
cb_data_queue_completed (cb_data,
NM_CONNECTIVITY_PORTAL,
"unexpected non-empty response",
NULL);
return 0;
}
return len;
}
nm_assert (cb_data->concheck.response_good_cnt < strlen (response));
response_len = strlen (response);
check_len = NM_MIN (len,
response_len - cb_data->concheck.response_good_cnt);
if (strncmp (&response[cb_data->concheck.response_good_cnt],
buffer,
check_len) != 0) {
cb_data_queue_completed (cb_data,
NM_CONNECTIVITY_PORTAL,
"unexpected response",
NULL);
return 0;
}
cb_data->concheck.response_good_cnt += len;
if (cb_data->concheck.response_good_cnt >= response_len) {
/* We already have enough data, and it matched. */
cb_data_queue_completed (cb_data,
NM_CONNECTIVITY_FULL,
"expected response",
NULL);
return 0;
}
return len;
}
static gboolean
_timeout_cb (gpointer user_data)
{
NMConnectivityCheckHandle *cb_data = user_data;
nm_assert (NM_IS_CONNECTIVITY (cb_data->self));
nm_assert (c_list_contains (&NM_CONNECTIVITY_GET_PRIVATE (cb_data->self)->handles_lst_head, &cb_data->handles_lst));
cb_data_complete (cb_data, NM_CONNECTIVITY_LIMITED, "timeout");
return G_SOURCE_REMOVE;
}
#endif
static gboolean
_idle_cb (gpointer user_data)
{
NMConnectivityCheckHandle *cb_data = user_data;
nm_assert (NM_IS_CONNECTIVITY (cb_data->self));
nm_assert (c_list_contains (&NM_CONNECTIVITY_GET_PRIVATE (cb_data->self)->handles_lst_head, &cb_data->handles_lst));
nm_assert (cb_data->completed_reason);
cb_data->timeout_id = 0;
cb_data_complete (cb_data, cb_data->completed_state, cb_data->completed_reason);
return G_SOURCE_REMOVE;
}
#if WITH_CONCHECK
static void
do_curl_request (NMConnectivityCheckHandle *cb_data)
{
CURLM *mhandle;
CURL *ehandle;
long resolve;
mhandle = curl_multi_init ();
if (!mhandle) {
cb_data_complete (cb_data, NM_CONNECTIVITY_ERROR, "curl error");
return;
}
ehandle = curl_easy_init ();
if (!ehandle) {
curl_multi_cleanup (mhandle);
cb_data_complete (cb_data, NM_CONNECTIVITY_ERROR, "curl error");
return;
}
cb_data->concheck.curl_mhandle = mhandle;
cb_data->concheck.curl_ehandle = ehandle;
cb_data->concheck.request_headers = curl_slist_append (NULL, "Connection: close");
cb_data->timeout_id = g_timeout_add_seconds (20, _timeout_cb, cb_data);
curl_multi_setopt (mhandle, CURLMOPT_SOCKETFUNCTION, multi_socket_cb);
curl_multi_setopt (mhandle, CURLMOPT_SOCKETDATA, cb_data);
curl_multi_setopt (mhandle, CURLMOPT_TIMERFUNCTION, multi_timer_cb);
curl_multi_setopt (mhandle, CURLMOPT_TIMERDATA, cb_data);
switch (cb_data->addr_family) {
case AF_INET:
resolve = CURL_IPRESOLVE_V4;
break;
case AF_INET6:
resolve = CURL_IPRESOLVE_V6;
break;
case AF_UNSPEC:
resolve = CURL_IPRESOLVE_WHATEVER;
break;
default:
resolve = CURL_IPRESOLVE_WHATEVER;
g_warn_if_reached ();
}
curl_easy_setopt (ehandle, CURLOPT_URL, cb_data->concheck.con_config->uri);
curl_easy_setopt (ehandle, CURLOPT_WRITEFUNCTION, easy_write_cb);
curl_easy_setopt (ehandle, CURLOPT_WRITEDATA, cb_data);
curl_easy_setopt (ehandle, CURLOPT_HEADERFUNCTION, easy_header_cb);
curl_easy_setopt (ehandle, CURLOPT_HEADERDATA, cb_data);
curl_easy_setopt (ehandle, CURLOPT_PRIVATE, cb_data);
curl_easy_setopt (ehandle, CURLOPT_HTTPHEADER, cb_data->concheck.request_headers);
curl_easy_setopt (ehandle, CURLOPT_INTERFACE, cb_data->ifspec);
curl_easy_setopt (ehandle, CURLOPT_RESOLVE, cb_data->concheck.hosts);
curl_easy_setopt (ehandle, CURLOPT_IPRESOLVE, resolve);
curl_multi_add_handle (mhandle, ehandle);
}
static void
resolve_cb (GObject *object, GAsyncResult *res, gpointer user_data)
{
NMConnectivityCheckHandle *cb_data;
gs_unref_variant GVariant *result = NULL;
gs_unref_variant GVariant *addresses = NULL;
gsize no_addresses;
int ifindex;
int addr_family;
gsize len = 0;
gsize i;
gs_free_error GError *error = NULL;
result = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), res, &error);
if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
return;
cb_data = user_data;
g_clear_object (&cb_data->concheck.resolve_cancellable);
if (!result) {
/* Never mind. Just let do curl do its own resolving. */
_LOG2D ("can't resolve a name via systemd-resolved: %s", error->message);
do_curl_request (cb_data);
return;
}
addresses = g_variant_get_child_value (result, 0);
no_addresses = g_variant_n_children (addresses);
for (i = 0; i < no_addresses; i++) {
gs_unref_variant GVariant *address = NULL;
char str_addr[NM_UTILS_INET_ADDRSTRLEN];
gs_free char *host_entry = NULL;
const guchar *address_buf;
g_variant_get_child (addresses, i, "(ii@ay)", &ifindex, &addr_family, &address);
if ( cb_data->addr_family != AF_UNSPEC
&& cb_data->addr_family != addr_family)
continue;
address_buf = g_variant_get_fixed_array (address, &len, 1);
if ( (addr_family == AF_INET && len != sizeof (struct in_addr))
|| (addr_family == AF_INET6 && len != sizeof (struct in6_addr)))
continue;
host_entry = g_strdup_printf ("%s:%s:%s",
cb_data->concheck.con_config->host,
cb_data->concheck.con_config->port ?: "80",
nm_utils_inet_ntop (addr_family, address_buf, str_addr));
cb_data->concheck.hosts = curl_slist_append (cb_data->concheck.hosts, host_entry);
_LOG2T ("adding '%s' to curl resolve list", host_entry);
}
do_curl_request (cb_data);
}
#endif
#define SD_RESOLVED_DNS ((guint64) (1LL << 0))
static NMConnectivityState
check_platform_config (NMConnectivity *self,
NMPlatform *platform,
int ifindex,
int addr_family,
const char **reason)
{
const NMDedupMultiHeadEntry *addresses;
const NMDedupMultiHeadEntry *routes;
if (!nm_platform_link_is_connected (platform, ifindex)) {
NM_SET_OUT (reason, "no carrier");
return NM_CONNECTIVITY_NONE;
}
addresses = nm_platform_lookup_object (platform,
addr_family == AF_INET
? NMP_OBJECT_TYPE_IP4_ADDRESS
: NMP_OBJECT_TYPE_IP6_ADDRESS,
ifindex);
if (!addresses || addresses->len == 0) {
NM_SET_OUT (reason, "no IP address configured");
return NM_CONNECTIVITY_NONE;
}
routes = nm_platform_lookup_object (platform,
addr_family == AF_INET
? NMP_OBJECT_TYPE_IP4_ROUTE
: NMP_OBJECT_TYPE_IP6_ROUTE,
ifindex);
if (!routes || routes->len == 0) {
NM_SET_OUT (reason, "no IP route configured");
return NM_CONNECTIVITY_NONE;
}
switch (addr_family) {
case AF_INET: {
const NMPlatformIP4Route *route;
gboolean found_global = FALSE;
NMDedupMultiIter iter;
const NMPObject *plobj;
/* For IPv4 also require a route with global scope. */
nmp_cache_iter_for_each (&iter, routes, &plobj) {
route = NMP_OBJECT_CAST_IP4_ROUTE (plobj);
if (nm_platform_route_scope_inv (route->scope_inv) == RT_SCOPE_UNIVERSE) {
found_global = TRUE;
break;
}
}
if (!found_global) {
NM_SET_OUT (reason, "no global route configured");
return NM_CONNECTIVITY_LIMITED;
}
break;
}
case AF_INET6:
/* Route scopes aren't meaningful for IPv6 so any route is fine. */
break;
default:
g_return_val_if_reached (FALSE);
}
NM_SET_OUT (reason, NULL);
return NM_CONNECTIVITY_UNKNOWN;
}
NMConnectivityCheckHandle *
nm_connectivity_check_start (NMConnectivity *self,
int addr_family,
NMPlatform *platform,
int ifindex,
const char *iface,
NMConnectivityCheckCallback callback,
gpointer user_data)
{
NMConnectivityPrivate *priv;
NMConnectivityCheckHandle *cb_data;
static guint64 request_counter = 0;
g_return_val_if_fail (NM_IS_CONNECTIVITY (self), NULL);
g_return_val_if_fail (callback, NULL);
nm_assert (!platform || NM_IS_PLATFORM (platform));
priv = NM_CONNECTIVITY_GET_PRIVATE (self);
cb_data = g_slice_new0 (NMConnectivityCheckHandle);
cb_data->self = self;
cb_data->request_counter = ++request_counter;
c_list_link_tail (&priv->handles_lst_head, &cb_data->handles_lst);
cb_data->callback = callback;
cb_data->user_data = user_data;
cb_data->completed_state = NM_CONNECTIVITY_UNKNOWN;
cb_data->addr_family = addr_family;
if (iface)
cb_data->ifspec = g_strdup_printf ("if!%s", iface);
#if WITH_CONCHECK
cb_data->concheck.con_config = _con_config_ref (priv->con_config);
if ( iface
&& ifindex > 0
&& priv->enabled
&& priv->uri_valid) {
gboolean has_systemd_resolved;
NMConnectivityState state;
const char *reason;
cb_data->concheck.ch_ifindex = ifindex;
if (platform) {
state = check_platform_config (self,
platform,
ifindex,
addr_family,
&reason);
nm_assert ((state == NM_CONNECTIVITY_UNKNOWN) == !reason);
if (state != NM_CONNECTIVITY_UNKNOWN) {
_LOG2D ("skip connectivity check due to %s", reason);
cb_data->completed_state = state;
cb_data->completed_reason = reason;
cb_data->timeout_id = g_idle_add (_idle_cb, cb_data);
return cb_data;
}
}
/* note that we pick up support for systemd-resolved right away when we need it.
* We don't need to remember the setting, because we can (cheaply) check anew
* on each request.
*
* Yes, this makes NMConnectivity singleton dependent on NMDnsManager singleton.
* Well, not really: it makes connectivity-check-start dependent on NMDnsManager
* which merely means, not to start a connectivity check, late during shutdown.
*
* NMDnsSystemdResolved tries to D-Bus activate systemd-resolved only once,
* to not spam syslog with failures messages from dbus-daemon.
* Note that unless NMDnsSystemdResolved tried and failed to start systemd-resolved,
* it guesses that systemd-resolved is activatable and returns %TRUE here. That
* means, while NMDnsSystemdResolved would not try to D-Bus activate systemd-resolved
* more than once, NMConnectivity might -- until NMDnsSystemdResolved tried itself
* and noticed that systemd-resolved is not available.
* This is relatively cumbersome to avoid, because we would have to go through
* NMDnsSystemdResolved trying to asynchronously start the service, to ensure there
* is only one attempt to start the service. */
has_systemd_resolved = nm_dns_manager_has_systemd_resolved (nm_dns_manager_get ());
if (has_systemd_resolved) {
GDBusConnection *dbus_connection;
dbus_connection = NM_MAIN_DBUS_CONNECTION_GET;
if (!dbus_connection) {
/* we have no D-Bus connection? That might happen in configure and quit mode.
*
* Anyway, something is very odd, just fail connectivity check. */
_LOG2D ("start fake request (fail due to no D-Bus connection)");
cb_data->completed_state = NM_CONNECTIVITY_ERROR;
cb_data->completed_reason = "no D-Bus connection";
cb_data->timeout_id = g_idle_add (_idle_cb, cb_data);
return cb_data;
}
cb_data->concheck.resolve_cancellable = g_cancellable_new ();
g_dbus_connection_call (dbus_connection,
"org.freedesktop.resolve1",
"/org/freedesktop/resolve1",
"org.freedesktop.resolve1.Manager",
"ResolveHostname",
g_variant_new ("(isit)",
(gint32) cb_data->concheck.ch_ifindex,
cb_data->concheck.con_config->host,
(gint32) cb_data->addr_family,
SD_RESOLVED_DNS),
G_VARIANT_TYPE ("(a(iiay)st)"),
G_DBUS_CALL_FLAGS_NONE,
-1,
cb_data->concheck.resolve_cancellable,
resolve_cb,
cb_data);
_LOG2D ("start request to '%s' (try resolving '%s' using systemd-resolved)",
cb_data->concheck.con_config->uri,
cb_data->concheck.con_config->host);
} else {
_LOG2D ("start request to '%s' (systemd-resolved not available)",
cb_data->concheck.con_config->uri);
do_curl_request (cb_data);
}
return cb_data;
}
#endif
if (!cb_data->ifspec) {
cb_data->completed_state = NM_CONNECTIVITY_ERROR;
cb_data->completed_reason = "missing interface";
} else {
cb_data->completed_state = NM_CONNECTIVITY_FAKE;
cb_data->completed_reason = "fake result";
}
_LOG2D ("start fake request (%s)", cb_data->completed_reason);
cb_data->timeout_id = g_idle_add (_idle_cb, cb_data);
return cb_data;
}
void
nm_connectivity_check_cancel (NMConnectivityCheckHandle *cb_data)
{
g_return_if_fail (cb_data);
g_return_if_fail (NM_IS_CONNECTIVITY (cb_data->self));
nm_assert ( c_list_contains (&NM_CONNECTIVITY_GET_PRIVATE (cb_data->self)->handles_lst_head, &cb_data->handles_lst)
|| c_list_contains (&NM_CONNECTIVITY_GET_PRIVATE (cb_data->self)->completed_handles_lst_head, &cb_data->handles_lst));
cb_data_complete (cb_data, NM_CONNECTIVITY_CANCELLED, "cancelled");
}
/*****************************************************************************/
gboolean
nm_connectivity_check_enabled (NMConnectivity *self)
{
g_return_val_if_fail (NM_IS_CONNECTIVITY (self), FALSE);
return NM_CONNECTIVITY_GET_PRIVATE (self)->enabled;
}
/*****************************************************************************/
guint
nm_connectivity_get_interval (NMConnectivity *self)
{
return nm_connectivity_check_enabled (self)
? NM_CONNECTIVITY_GET_PRIVATE (self)->interval
: 0;
}
static gboolean
host_and_port_from_uri (const char *uri, char **host, char **port)
{
const char *p = uri;
const char *host_begin = NULL;
size_t host_len = 0;
const char *port_begin = NULL;
size_t port_len = 0;
/* scheme */
while (*p != ':' && *p != '/') {
if (!*p++)
return FALSE;
}
/* :// */
if (*p++ != ':')
return FALSE;
if (*p++ != '/')
return FALSE;
if (*p++ != '/')
return FALSE;
/* host */
if (*p == '[')
return FALSE;
host_begin = p;
while (*p && *p != ':' && *p != '/') {
host_len++;
p++;
}
if (host_len == 0)
return FALSE;
*host = g_strndup (host_begin, host_len);
/* port */
if (*p++ == ':') {
port_begin = p;
while (*p && *p != '/') {
port_len++;
p++;
}
if (port_len)
*port = g_strndup (port_begin, port_len);
}
return TRUE;
}
static void
update_config (NMConnectivity *self, NMConfigData *config_data)
{
NMConnectivityPrivate *priv = NM_CONNECTIVITY_GET_PRIVATE (self);
guint interval;
gboolean enabled;
gboolean changed = FALSE;
const char *cur_uri = priv->con_config ? priv->con_config->uri : NULL;
const char *cur_response = priv->con_config ? priv->con_config->response : NULL;
const char *new_response;
const char *new_uri;
gboolean new_uri_valid = priv->uri_valid;
gboolean new_host_port = FALSE;
gs_free char *new_host = NULL;
gs_free char *new_port = NULL;
new_uri = nm_config_data_get_connectivity_uri (config_data);
if (!nm_streq0 (new_uri, cur_uri)) {
new_uri_valid = (new_uri && *new_uri);
if (new_uri_valid) {
gs_free char *scheme = g_uri_parse_scheme (new_uri);
gboolean is_https = FALSE;
if (!scheme) {
_LOGE ("invalid URI '%s' for connectivity check.", new_uri);
new_uri_valid = FALSE;
} else if (g_ascii_strcasecmp (scheme, "https") == 0) {
_LOGW ("use of HTTPS for connectivity checking is not reliable and is discouraged (URI: %s)", new_uri);
is_https = TRUE;
} else if (g_ascii_strcasecmp (scheme, "http") != 0) {
_LOGE ("scheme of '%s' uri doesn't use a scheme that is allowed for connectivity check.", new_uri);
new_uri_valid = FALSE;
}
if (new_uri_valid) {
new_host_port = TRUE;
if (!host_and_port_from_uri (new_uri, &new_host, &new_port)) {
_LOGE ("cannot parse host and port from '%s'", new_uri);
new_uri_valid = FALSE;
} else if (!new_port && is_https)
new_port = g_strdup ("443");
}
}
if ( new_uri_valid
|| priv->uri_valid != new_uri_valid)
changed = TRUE;
}
new_response = nm_config_data_get_connectivity_response (config_data);
if (!nm_streq0 (new_response, cur_response))
changed = TRUE;
if ( !priv->con_config
|| !nm_streq0 (new_uri, priv->con_config->uri)
|| !nm_streq0 (new_response, priv->con_config->response)) {
if (!new_host_port) {
new_host = priv->con_config ? g_strdup (priv->con_config->host) : NULL;
new_port = priv->con_config ? g_strdup (priv->con_config->port) : NULL;
}
_con_config_unref (priv->con_config);
priv->con_config = g_slice_new (ConConfig);
*priv->con_config = (ConConfig) {
.ref_count = 1,
.uri = g_strdup (new_uri),
.response = g_strdup (new_response),
.host = g_steal_pointer (&new_host),
.port = g_steal_pointer (&new_port),
};
}
priv->uri_valid = new_uri_valid;
interval = nm_config_data_get_connectivity_interval (config_data);
interval = MIN (interval, (7 * 24 * 3600));
if (priv->interval != interval) {
priv->interval = interval;
changed = TRUE;
}
enabled = FALSE;
#if WITH_CONCHECK
if ( priv->uri_valid
&& priv->interval)
enabled = nm_config_data_get_connectivity_enabled (config_data);
#endif
if (priv->enabled != enabled) {
priv->enabled = enabled;
changed = TRUE;
}
if (changed)
g_signal_emit (self, signals[CONFIG_CHANGED], 0);
}
static void
config_changed_cb (NMConfig *config,
NMConfigData *config_data,
NMConfigChangeFlags changes,
NMConfigData *old_data,
NMConnectivity *self)
{
update_config (self, config_data);
}
/*****************************************************************************/
static void
nm_connectivity_init (NMConnectivity *self)
{
NMConnectivityPrivate *priv = NM_CONNECTIVITY_GET_PRIVATE (self);
#if WITH_CONCHECK
CURLcode ret;
#endif
c_list_init (&priv->handles_lst_head);
c_list_init (&priv->completed_handles_lst_head);
priv->config = g_object_ref (nm_config_get ());
g_signal_connect (G_OBJECT (priv->config),
NM_CONFIG_SIGNAL_CONFIG_CHANGED,
G_CALLBACK (config_changed_cb),
self);
#if WITH_CONCHECK
ret = curl_global_init (CURL_GLOBAL_ALL);
if (ret != CURLE_OK) {
_LOGE ("unable to init cURL, connectivity check will not work: (%d) %s",
ret, curl_easy_strerror (ret));
}
#endif
update_config (self, nm_config_get_data (priv->config));
}
static void
dispose (GObject *object)
{
NMConnectivity *self = NM_CONNECTIVITY (object);
NMConnectivityPrivate *priv = NM_CONNECTIVITY_GET_PRIVATE (self);
NMConnectivityCheckHandle *cb_data;
nm_assert (c_list_is_empty (&priv->completed_handles_lst_head));
while ((cb_data = c_list_first_entry (&priv->handles_lst_head,
NMConnectivityCheckHandle,
handles_lst)))
cb_data_complete (cb_data, NM_CONNECTIVITY_DISPOSING, "shutting down");
nm_clear_pointer (&priv->con_config, _con_config_unref);
#if WITH_CONCHECK
curl_global_cleanup ();
#endif
if (priv->config) {
g_signal_handlers_disconnect_by_func (priv->config, config_changed_cb, self);
g_clear_object (&priv->config);
}
G_OBJECT_CLASS (nm_connectivity_parent_class)->dispose (object);
}
static void
nm_connectivity_class_init (NMConnectivityClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
signals[CONFIG_CHANGED] =
g_signal_new (NM_CONNECTIVITY_CONFIG_CHANGED,
G_OBJECT_CLASS_TYPE (object_class),
G_SIGNAL_RUN_FIRST,
0, NULL, NULL, NULL,
G_TYPE_NONE, 0);
object_class->dispose = dispose;
}