diff options
| author | Michael Biebl <biebl@debian.org> | 2021-02-11 18:11:46 +0100 |
|---|---|---|
| committer | Michael Biebl <biebl@debian.org> | 2021-02-11 18:11:46 +0100 |
| commit | 80ec1decc49c72efec2a8b87c06245c92c0ab807 (patch) | |
| tree | e3b229aa94e8dcf0590f2317664176e7b8f7607b /src/core/nm-connectivity.c | |
| parent | 65f86e8f56267192d42f2b629fc6b0c99fb9cd0c (diff) | |
New upstream version 1.29.90 upstream/1.29.90
Diffstat (limited to 'src/core/nm-connectivity.c')
| -rw-r--r-- | src/core/nm-connectivity.c | 1229 |
1 files changed, 1229 insertions, 0 deletions
diff --git a/src/core/nm-connectivity.c b/src/core/nm-connectivity.c new file mode 100644 index 00000000..f847c8f4 --- /dev/null +++ b/src/core/nm-connectivity.c @@ -0,0 +1,1229 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2011 Thomas Bechtold <thomasbechtold@jpberlin.de> + * Copyright (C) 2011 Dan Williams <dcbw@redhat.com> + * Copyright (C) 2016 - 2018 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.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; +} |