nm-connectivity.c 35.5 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
/* NetworkManager -- Network link manager
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Copyright (C) 2011 Thomas Bechtold <thomasbechtold@jpberlin.de>
19
 * Copyright (C) 2011 Dan Williams <dcbw@redhat.com>
20
 * Copyright (C) 2016 - 2018 Red Hat, Inc.
21 22
 */

23
#include "nm-default.h"
24

25 26
#include "nm-connectivity.h"

27
#if WITH_CONCHECK
28
#include <curl/curl.h>
29
#endif
30

31
#include "c-list/src/c-list.h"
32
#include "nm-core-internal.h"
33
#include "nm-config.h"
34
#include "NetworkManagerUtils.h"
35 36
#include "nm-dbus-manager.h"
#include "dns/nm-dns-manager.h"
37

38 39
#define HEADER_STATUS_ONLINE "X-NetworkManager-Status: online\r\n"

40
/*****************************************************************************/
41

42
NM_UTILS_LOOKUP_STR_DEFINE_STATIC (_state_to_string, int /*NMConnectivityState*/,
43 44 45 46 47 48
	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"),
49

50 51 52 53
	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"),
54 55
);

56 57 58 59 60 61
const char *
nm_connectivity_state_to_string (NMConnectivityState state)
{
	return _state_to_string (state);
}

62 63
/*****************************************************************************/

64 65 66 67 68 69 70 71
typedef struct {
	guint ref_count;
	char *uri;
	char *host;
	char *port;
	char *response;
} ConConfig;

72 73 74 75 76 77 78 79
struct _NMConnectivityCheckHandle {
	CList handles_lst;
	NMConnectivity *self;
	NMConnectivityCheckCallback callback;
	gpointer user_data;

	char *ifspec;

80 81 82
	const char *completed_log_message;
	char *completed_log_message_free;

83
#if WITH_CONCHECK
84
	struct {
85
		ConConfig *con_config;
86

87
		GCancellable *resolve_cancellable;
88
		CURLM *curl_mhandle;
89 90
		CURL *curl_ehandle;
		struct curl_slist *request_headers;
91
		struct curl_slist *hosts;
92

93
		gsize response_good_cnt;
94 95 96

		guint curl_timer;
		int ch_ifindex;
97 98
	} concheck;
#endif
99

100
	guint64 request_counter;
101 102

	int addr_family;
103

Thomas Haller's avatar
Thomas Haller committed
104
	guint timeout_id;
105 106 107 108

	NMConnectivityState completed_state;

	bool fail_reason_no_dbus_connection:1;
109
};
Thomas Haller's avatar
Thomas Haller committed
110 111

enum {
112
	CONFIG_CHANGED,
Thomas Haller's avatar
Thomas Haller committed
113 114 115 116 117 118

	LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = { 0 };

119
typedef struct {
120
	CList handles_lst_head;
121
	CList completed_handles_lst_head;
122
	NMConfig *config;
123
	ConConfig *con_config;
124 125 126
	guint interval;

	bool enabled:1;
127
	bool uri_valid:1;
128 129
} NMConnectivityPrivate;

130 131 132
struct _NMConnectivity {
	GObject parent;
	NMConnectivityPrivate _priv;
133 134
};

135 136 137 138 139 140 141 142
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)

143 144
NM_DEFINE_SINGLETON_GETTER (NMConnectivity, nm_connectivity_get, NM_TYPE_CONNECTIVITY);

145 146
/*****************************************************************************/

147 148
#define _NMLOG_DOMAIN      LOGD_CONCHECK
#define _NMLOG(level, ...) __NMLOG_DEFAULT (level, _NMLOG_DOMAIN, "connectivity", __VA_ARGS__)
149

150 151 152 153 154 155 156
#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, \
157 158
                     (cb_data->ifspec ? &cb_data->ifspec[3] : NULL), \
                     NULL, \
159
                     "connectivity: (%s,IPv%c,%"G_GUINT64_FORMAT") " \
160
                     _NM_UTILS_MACRO_FIRST (__VA_ARGS__), \
161
                     (cb_data->ifspec ? &cb_data->ifspec[3] : ""), \
162 163
                     nm_utils_addr_family_to_char (cb_data->addr_family), \
                     cb_data->request_counter \
164
                     _NM_UTILS_MACRO_REST (__VA_ARGS__)); \
165 166
        } \
    } G_STMT_END
167

168
/*****************************************************************************/
169

170
#if WITH_CONCHECK
171 172 173 174 175 176 177 178 179
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;
}
180
#endif
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199

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);
}

200
#if WITH_CONCHECK
201 202 203 204 205
static const char *
_con_config_get_response (const ConConfig *con_config)
{
	return con_config->response ?: NM_CONFIG_DEFAULT_CONNECTIVITY_RESPONSE;
}
206
#endif
207 208 209

/*****************************************************************************/

210
static void
211 212 213
cb_data_complete (NMConnectivityCheckHandle *cb_data,
                  NMConnectivityState state,
                  const char *log_message)
214
{
215
	NMConnectivity *self;
216 217 218

	nm_assert (cb_data);
	nm_assert (NM_IS_CONNECTIVITY (cb_data->self));
219 220
	nm_assert (cb_data->callback);
	nm_assert (state != NM_CONNECTIVITY_UNKNOWN);
221 222 223 224
	nm_assert (log_message);

	self = cb_data->self;

225 226 227
	/* mark the handle as completing. After this point, nm_connectivity_check_cancel()
	 * is no longer possible. */
	cb_data->self = NULL;
228

229
	c_list_unlink_stale (&cb_data->handles_lst);
230 231 232 233

#if WITH_CONCHECK
	if (cb_data->concheck.curl_ehandle) {
		/* Contrary to what cURL manual claim it is *not* safe to remove
234 235 236 237 238 239
		 * 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. */
240 241 242 243 244 245 246
		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);

247 248
		curl_multi_remove_handle (cb_data->concheck.curl_mhandle,
		                          cb_data->concheck.curl_ehandle);
249
		curl_easy_cleanup (cb_data->concheck.curl_ehandle);
250
		curl_multi_cleanup (cb_data->concheck.curl_mhandle);
251 252

		curl_slist_free_all (cb_data->concheck.request_headers);
253
		curl_slist_free_all (cb_data->concheck.hosts);
254
	}
255
	nm_clear_g_source (&cb_data->concheck.curl_timer);
256
	nm_clear_g_cancellable (&cb_data->concheck.resolve_cancellable);
257 258 259 260
#endif

	nm_clear_g_source (&cb_data->timeout_id);

261 262 263 264 265 266 267 268 269 270 271 272
	_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. */
273 274

#if WITH_CONCHECK
275
	_con_config_unref (cb_data->concheck.con_config);
276
#endif
277
	g_free (cb_data->ifspec);
278 279
	if (cb_data->completed_log_message_free)
		g_free (cb_data->completed_log_message_free);
280
	g_slice_free (NMConnectivityCheckHandle, cb_data);
281 282
}

283 284 285
/*****************************************************************************/

#if WITH_CONCHECK
286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325

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);
}

326
static gboolean
327
_con_curl_check_connectivity (CURLM *mhandle, int sockfd, int ev_bitmask)
328
{
329
	NMConnectivityCheckHandle *cb_data;
330 331
	CURLMsg *msg;
	CURLcode eret;
332
	int m_left;
333
	long response_code;
334 335
	CURLMcode ret;
	int running_handles;
336
	gboolean success = TRUE;
337

338
	ret = curl_multi_socket_action (mhandle, sockfd, ev_bitmask, &running_handles);
339
	if (ret != CURLM_OK) {
340
		_LOGD ("connectivity check failed: (%d) %s", ret, curl_easy_strerror (ret));
341 342
		success = FALSE;
	}
343 344

	while ((msg = curl_multi_info_read (mhandle, &m_left))) {
345
		const char *response;
346

347 348 349 350
		if (msg->msg != CURLMSG_DONE)
			continue;

		/* Here we have completed a session. Check easy session result. */
351
		eret = curl_easy_getinfo (msg->easy_handle, CURLINFO_PRIVATE, (char **) &cb_data);
352
		if (eret != CURLE_OK) {
353 354
			_LOGD ("curl cannot extract cb_data for easy handle, skipping msg: (%d) %s",
			       eret, curl_easy_strerror (eret));
355
			success = FALSE;
356 357 358
			continue;
		}

359 360
		nm_assert (cb_data);
		nm_assert (NM_IS_CONNECTIVITY (cb_data->self));
361

362 363 364 365 366 367 368 369 370
		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,
371 372 373
			                         g_strdup_printf ("check failed: (%d) %s",
			                                          msg->data.result,
			                                          curl_easy_strerror (msg->data.result)));
374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393
			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
394
			    && cb_data->concheck.response_good_cnt == 0) {
395 396 397 398 399 400 401
				/* 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;
			}
402
		}
403 404 405 406 407 408 409 410 411

		/* 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);
412
	}
413 414 415 416 417 418

	/* 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;
419 420 421
}

static gboolean
422
_con_curl_timeout_cb (gpointer user_data)
423
{
424
	NMConnectivityCheckHandle *cb_data = user_data;
425

426 427 428
	cb_data->concheck.curl_timer = 0;
	_con_curl_check_connectivity (cb_data->concheck.curl_mhandle, CURL_SOCKET_TIMEOUT, 0);
	_complete_queued (cb_data->self);
429
	return G_SOURCE_REMOVE;
430 431 432 433 434
}

static int
multi_timer_cb (CURLM *multi, long timeout_ms, void *userdata)
{
435
	NMConnectivityCheckHandle *cb_data = userdata;
436

437
	nm_clear_g_source (&cb_data->concheck.curl_timer);
438
	if (timeout_ms != -1)
439
		cb_data->concheck.curl_timer = g_timeout_add (timeout_ms, _con_curl_timeout_cb, cb_data);
440 441 442
	return 0;
}

443
typedef struct {
444
	NMConnectivityCheckHandle *cb_data;
445 446 447 448 449 450 451 452 453 454 455 456
	GIOChannel *ch;

	/* 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;

	guint ev;
} ConCurlSockData;

457
static gboolean
458
_con_curl_socketevent_cb (GIOChannel *ch, GIOCondition condition, gpointer user_data)
459
{
460
	ConCurlSockData *fdp = user_data;
461
	NMConnectivityCheckHandle *cb_data = fdp->cb_data;
462 463
	int fd = g_io_channel_unix_get_fd (ch);
	int action = 0;
464 465
	gboolean fdp_destroyed = FALSE;
	gboolean success;
466 467 468 469 470

	if (condition & G_IO_IN)
		action |= CURL_CSELECT_IN;
	if (condition & G_IO_OUT)
		action |= CURL_CSELECT_OUT;
471 472
	if (condition & G_IO_ERR)
		action |= CURL_CSELECT_ERR;
473

474 475
	nm_assert (!fdp->destroy_notify);
	fdp->destroy_notify = &fdp_destroyed;
476

477
	success = _con_curl_check_connectivity (cb_data->concheck.curl_mhandle, fd, action);
478 479 480 481 482 483 484 485 486 487 488

	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)
			fdp->ev = 0;
	}

489
	_complete_queued (cb_data->self);
490

491 492
	return success ? G_SOURCE_CONTINUE : G_SOURCE_REMOVE;
}
493 494

static int
495
multi_socket_cb (CURL *e_handle, curl_socket_t fd, int what, void *userdata, void *socketp)
496
{
497
	NMConnectivityCheckHandle *cb_data = userdata;
498
	ConCurlSockData *fdp = socketp;
499 500
	GIOCondition condition = 0;

501 502
	(void) _NM_ENSURE_TYPE (int, fd);

503 504
	if (what == CURL_POLL_REMOVE) {
		if (fdp) {
505 506
			if (fdp->destroy_notify)
				*fdp->destroy_notify = TRUE;
507
			curl_multi_assign (cb_data->concheck.curl_mhandle, fd, NULL);
508 509
			nm_clear_g_source (&fdp->ev);
			g_io_channel_unref (fdp->ch);
510
			g_slice_free (ConCurlSockData, fdp);
511 512 513
		}
	} else {
		if (!fdp) {
514
			fdp = g_slice_new0 (ConCurlSockData);
515
			fdp->cb_data = cb_data;
516
			fdp->ch = g_io_channel_unix_new (fd);
517
			curl_multi_assign (cb_data->concheck.curl_mhandle, fd, fdp);
518 519 520
		} else
			nm_clear_g_source (&fdp->ev);

521 522 523 524
		if (what == CURL_POLL_IN)
			condition = G_IO_IN;
		else if (what == CURL_POLL_OUT)
			condition = G_IO_OUT;
525
		else if (what == CURL_POLL_INOUT)
526 527 528
			condition = G_IO_IN | G_IO_OUT;

		if (condition)
529
			fdp->ev = g_io_add_watch (fdp->ch, condition, _con_curl_socketevent_cb, fdp);
530 531
	}

532
	return CURLM_OK;
533 534 535 536 537
}

static size_t
easy_header_cb (char *buffer, size_t size, size_t nitems, void *userdata)
{
538
	NMConnectivityCheckHandle *cb_data = userdata;
539 540
	size_t len = size * nitems;

541 542 543 544 545
	if (cb_data->completed_state != NM_CONNECTIVITY_UNKNOWN) {
		/* already completed. */
		return 0;
	}

546 547
	if (   len >= sizeof (HEADER_STATUS_ONLINE) - 1
	    && !g_ascii_strncasecmp (buffer, HEADER_STATUS_ONLINE, sizeof (HEADER_STATUS_ONLINE) - 1)) {
548 549 550 551
		cb_data_queue_completed (cb_data,
		                         NM_CONNECTIVITY_FULL,
		                         "status header found",
		                         NULL);
552
		return 0;
553 554 555 556 557 558 559 560
	}

	return len;
}

static size_t
easy_write_cb (void *buffer, size_t size, size_t nmemb, void *userdata)
{
561
	NMConnectivityCheckHandle *cb_data = userdata;
562
	size_t len = size * nmemb;
563 564
	size_t response_len;
	size_t check_len;
565 566 567 568 569 570
	const char *response;

	if (cb_data->completed_state != NM_CONNECTIVITY_UNKNOWN) {
		/* already completed. */
		return 0;
	}
571

572 573 574 575 576
	if (len == 0) {
		/* no data. That can happen, it's fine. */
		return len;
	}

577
	response = _con_config_get_response (cb_data->concheck.con_config);;
578 579 580 581 582 583 584

	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... */
585 586 587 588 589
		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
Lubomir Rintel's avatar
Lubomir Rintel committed
590
			 * 2) status code 200 and an empty response.
591 592 593 594 595 596
			 *
			 * 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.
			 *
Lubomir Rintel's avatar
Lubomir Rintel committed
597
			 * However, if we get an excessive amount of data, we put a stop on it
598
			 * and fail. */
599 600
			cb_data_queue_completed (cb_data,
			                         NM_CONNECTIVITY_PORTAL,
601
			                         "unexpected non-empty response",
602
			                         NULL);
603
			return 0;
604
		}
605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633

		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);
634 635 636
		return 0;
	}

637 638
	return len;
}
639 640

static gboolean
641
_timeout_cb (gpointer user_data)
642
{
643
	NMConnectivityCheckHandle *cb_data = user_data;
644

645
	nm_assert (NM_IS_CONNECTIVITY (cb_data->self));
646
	nm_assert (c_list_contains (&NM_CONNECTIVITY_GET_PRIVATE (cb_data->self)->handles_lst_head, &cb_data->handles_lst));
647

648
	cb_data_complete (cb_data, NM_CONNECTIVITY_LIMITED, "timeout");
649 650
	return G_SOURCE_REMOVE;
}
651
#endif
652

653 654
static gboolean
_idle_cb (gpointer user_data)
655
{
656
	NMConnectivityCheckHandle *cb_data = user_data;
657

658 659
	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));
660

661 662 663
	cb_data->timeout_id = 0;
	if (!cb_data->ifspec) {
		gs_free_error GError *error = NULL;
664

665 666 667
		/* the invocation was with an invalid ifname. It is a fail. */
		g_set_error (&error, NM_UTILS_ERROR, NM_UTILS_ERROR_INVALID_ARGUMENT,
		             "no interface specified for connectivity check");
668
		cb_data_complete (cb_data, NM_CONNECTIVITY_ERROR, "missing interface");
669 670 671 672 673 674
	} else if (cb_data->fail_reason_no_dbus_connection) {
		gs_free_error GError *error = NULL;

		g_set_error (&error, NM_UTILS_ERROR, NM_UTILS_ERROR_INVALID_ARGUMENT,
		             "no D-Bus connection");
		cb_data_complete (cb_data, NM_CONNECTIVITY_ERROR, "no D-Bus connection");
675
	} else
676
		cb_data_complete (cb_data, NM_CONNECTIVITY_FAKE, "fake result");
677 678 679
	return G_SOURCE_REMOVE;
}

680
#if WITH_CONCHECK
681 682 683
static void
do_curl_request (NMConnectivityCheckHandle *cb_data)
{
684
	CURLM *mhandle;
685
	CURL *ehandle;
686
	long resolve;
687

688 689 690 691 692 693
	mhandle = curl_multi_init ();
	if (!mhandle) {
		cb_data_complete (cb_data, NM_CONNECTIVITY_ERROR, "curl error");
		return;
	}

694 695
	ehandle = curl_easy_init ();
	if (!ehandle) {
696 697
		curl_multi_cleanup (mhandle);
		cb_data_complete (cb_data, NM_CONNECTIVITY_ERROR, "curl error");
698 699 700
		return;
	}

701 702 703 704 705 706 707 708 709 710 711
	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);
	curl_multi_setopt (mhandle, CURLOPT_VERBOSE, 1);

712 713 714 715 716 717 718 719 720 721 722 723 724 725 726
	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 ();
	}

727
	curl_easy_setopt (ehandle, CURLOPT_URL, cb_data->concheck.con_config->uri);
728 729 730 731 732 733 734 735
	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);
736
	curl_easy_setopt (ehandle, CURLOPT_IPRESOLVE, resolve);
737

738
	curl_multi_add_handle (mhandle, ehandle);
739 740 741 742 743
}

static void
resolve_cb (GObject *object, GAsyncResult *res, gpointer user_data)
{
744
	NMConnectivityCheckHandle *cb_data;
745 746
	gs_unref_variant GVariant *result = NULL;
	gs_unref_variant GVariant *addresses = NULL;
747 748
	gsize no_addresses;
	int ifindex;
749
	int addr_family;
750 751 752 753
	gsize len = 0;
	gsize i;
	gs_free_error GError *error = NULL;

754
	result = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), res, &error);
755 756 757
	if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
		return;

758
	cb_data = user_data;
759

760 761
	g_clear_object (&cb_data->concheck.resolve_cancellable);

762 763 764 765 766 767 768 769 770 771 772
	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++) {
773 774 775 776 777
		gs_unref_variant GVariant *address = NULL;
		char str_addr[NM_UTILS_INET_ADDRSTRLEN];
		gs_free char *host_entry = NULL;
		const guchar *address_buf;

778
		g_variant_get_child (addresses, i, "(ii@ay)", &ifindex, &addr_family, &address);
779

780 781 782 783 784 785 786 787
		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;
788

789
		host_entry = g_strdup_printf ("%s:%s:%s",
790 791
		                              cb_data->concheck.con_config->host,
		                              cb_data->concheck.con_config->port ?: "80",
792 793 794
		                              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);
795 796 797 798
	}

	do_curl_request (cb_data);
}
799
#endif
800

801
#define SD_RESOLVED_DNS ((guint64) (1LL << 0))
802

803 804
NMConnectivityCheckHandle *
nm_connectivity_check_start (NMConnectivity *self,
805
                             int addr_family,
806
                             int ifindex,
807 808 809 810 811 812
                             const char *iface,
                             NMConnectivityCheckCallback callback,
                             gpointer user_data)
{
	NMConnectivityPrivate *priv;
	NMConnectivityCheckHandle *cb_data;
813
	static guint64 request_counter = 0;
814

815 816 817 818
	g_return_val_if_fail (NM_IS_CONNECTIVITY (self), NULL);
	g_return_val_if_fail (callback, NULL);

	priv = NM_CONNECTIVITY_GET_PRIVATE (self);
819

820 821
	cb_data = g_slice_new0 (NMConnectivityCheckHandle);
	cb_data->self = self;
822
	cb_data->request_counter = ++request_counter;
823 824 825
	c_list_link_tail (&priv->handles_lst_head, &cb_data->handles_lst);
	cb_data->callback = callback;
	cb_data->user_data = user_data;
826
	cb_data->completed_state = NM_CONNECTIVITY_UNKNOWN;
827
	cb_data->addr_family = addr_family;
828
	if (iface)
829
		cb_data->ifspec = g_strdup_printf ("if!%s", iface);
830 831

#if WITH_CONCHECK
832

833 834
	cb_data->concheck.con_config = _con_config_ref (priv->con_config);

835 836 837 838
	if (   iface
	    && ifindex > 0
	    && priv->enabled
	    && priv->uri_valid) {
839
		gboolean has_systemd_resolved;
840

841
		cb_data->concheck.ch_ifindex = ifindex;
842

843 844 845 846 847 848
		/* 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
849 850 851 852 853 854 855 856 857 858 859 860
		 * 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. */
861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884
		has_systemd_resolved = nm_dns_manager_has_systemd_resolved (nm_dns_manager_get ());

		if (has_systemd_resolved) {
			GDBusConnection *dbus_connection;

			dbus_connection = nm_dbus_manager_get_dbus_connection (nm_dbus_manager_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->fail_reason_no_dbus_connection = TRUE;
				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 (nm_dbus_manager_get_dbus_connection (nm_dbus_manager_get ()),
			                        "org.freedesktop.resolve1",
			                        "/org/freedesktop/resolve1",
			                        "org.freedesktop.resolve1.Manager",
			                        "ResolveHostname",
			                        g_variant_new ("(isit)",
885 886
			                                       (gint32) cb_data->concheck.ch_ifindex,
			                                       cb_data->concheck.con_config->host,
887 888 889 890 891 892 893 894 895
			                                       (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)",
896 897
			        cb_data->concheck.con_config->uri,
			        cb_data->concheck.con_config->host);
898 899
		} else {
			_LOG2D ("start request to '%s' (systemd-resolved not available)",
900
			        cb_data->concheck.con_config->uri);
901
			do_curl_request (cb_data);
902 903
		}

904
		return cb_data;
905
	}
906
#endif
907

908 909
	_LOG2D ("start fake request");
	cb_data->timeout_id = g_idle_add (_idle_cb, cb_data);
910

911
	return cb_data;
912
}
913

914 915
void
nm_connectivity_check_cancel (NMConnectivityCheckHandle *cb_data)
916
{
917
	g_return_if_fail (cb_data);
918
	g_return_if_fail (NM_IS_CONNECTIVITY (cb_data->self));
919

920 921
	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));
922

923
	cb_data_complete (cb_data, NM_CONNECTIVITY_CANCELLED, "cancelled");
924 925
}

926 927
/*****************************************************************************/

928 929 930
gboolean
nm_connectivity_check_enabled (NMConnectivity *self)
{
931
	g_return_val_if_fail (NM_IS_CONNECTIVITY (self), FALSE);
932

933
	return NM_CONNECTIVITY_GET_PRIVATE (self)->enabled;
934 935
}

936
/*****************************************************************************/
937

938 939
guint
nm_connectivity_get_interval (NMConnectivity *self)
940
{
941 942 943
	return nm_connectivity_check_enabled (self)
	       ? NM_CONNECTIVITY_GET_PRIVATE (self)->interval
	       : 0;
944
}
945

946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977
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;
978
	*host = g_strndup (host_begin, host_len);
979 980 981 982 983 984 985 986 987

	/* port */
	if (*p++ == ':') {
		port_begin = p;
		while (*p && *p != '/') {
			port_len++;
			p++;
		}
		if (port_len)
988
			*port = g_strndup (port_begin, port_len);
989 990 991 992 993
	}

	return TRUE;
}

994
static void
995
update_config (NMConnectivity *self, NMConfigData *config_data)
996 997
{
	NMConnectivityPrivate *priv = NM_CONNECTIVITY_GET_PRIVATE (self);
998
	guint interval;
999
	gboolean enabled;
1000
	gboolean changed = FALSE;
1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015
	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);
1016
			gboolean is_https = FALSE;
1017 1018

			if (!scheme) {
1019 1020
				_LOGE ("invalid URI '%s' for connectivity check.", new_uri);
				new_uri_valid = FALSE;
1021
			} else if (g_ascii_strcasecmp (scheme, "https") == 0) {
1022
				_LOGW ("use of HTTPS for connectivity checking is not reliable and is discouraged (URI: %s)", new_uri);
1023
				is_https = TRUE;
1024
			} else if (g_ascii_strcasecmp (scheme, "http") != 0) {
1025 1026
				_LOGE ("scheme of '%s' uri doesn't use a scheme that is allowed for connectivity check.", new_uri);
				new_uri_valid = FALSE;
1027
			}
1028 1029 1030 1031 1032
			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;
1033 1034
				} else if (!new_port && is_https)
					new_port = g_strdup ("443");
1035 1036 1037
			}
		}

1038 1039
		if (   new_uri_valid
		    || priv->uri_valid != new_uri_valid)
1040
			changed = TRUE;
1041
	}
1042

1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062
	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),
		};
1063
	}
1064
	priv->uri_valid = new_uri_valid;
1065

1066
	interval = nm_config_data_get_connectivity_interval (config_data);
1067
	interval = MIN (interval, (7 * 24 * 3600));
1068 1069 1070 1071
	if (priv->interval != interval) {
		priv->interval = interval;
		changed = TRUE;
	}
1072

1073 1074
	enabled = FALSE;
#if WITH_CONCHECK
1075
	if (   priv->uri_valid
1076
	    && priv->interval)
1077 1078 1079
		enabled = nm_config_data_get_connectivity_enabled (config_data);
#endif

1080 1081 1082 1083 1084
	if (priv->enabled != enabled) {
		priv->enabled = enabled;
		changed = TRUE;
	}

1085 1086
	if (changed)
		g_signal_emit (self, signals[CONFIG_CHANGED], 0);
1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097
}

static void
config_changed_cb (NMConfig *config,
                   NMConfigData *config_data,
                   NMConfigChangeFlags changes,
                   NMConfigData *old_data,
                   NMConnectivity *self)
{
	update_config (self, config_data);
}
1098

1099 1100
/*****************************************************************************/

1101 1102 1103 1104
static void
nm_connectivity_init (NMConnectivity *self)
{
	NMConnectivityPrivate *priv = NM_CONNECTIVITY_GET_PRIVATE (self);
1105 1106 1107
#if WITH_CONCHECK
	CURLcode ret;
#endif
1108

1109
	c_list_init (&priv->handles_lst_head);
1110
	c_list_init (&priv->completed_handles_lst_head);
1111

1112 1113 1114 1115 1116 1117
	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);

1118
#if WITH_CONCHECK
1119
	ret = curl_global_init (CURL_GLOBAL_ALL);
1120
	if (ret != CURLE_OK) {
1121 1122
		_LOGE ("unable to init cURL, connectivity check will not work: (%d) %s",
		       ret, curl_easy_strerror (ret));
1123 1124 1125 1126
	}
#endif

	update_config (self, nm_config_get_data (priv->config));
1127
}
1128 1129

static void
1130
dispose (GObject *object)
1131
{
1132 1133
	NMConnectivity *self = NM_CONNECTIVITY (object);
	NMConnectivityPrivate *priv = NM_CONNECTIVITY_GET_PRIVATE (self);
1134
	NMConnectivityCheckHandle *cb_data;
1135 1136 1137 1138 1139 1140 1141

	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");
1142

1143
	nm_clear_pointer (&priv->con_config, _con_config_unref);
1144

1145 1146 1147 1148
#if WITH_CONCHECK
	curl_global_cleanup ();
#endif

1149 1150 1151 1152 1153
	if (priv->config) {
		g_signal_handlers_disconnect_by_func (priv->config, config_changed_cb, self);
		g_clear_object (&priv->config);
	}

1154
	G_OBJECT_CLASS (nm_connectivity_parent_class)->dispose (object);
1155 1156 1157 1158 1159 1160 1161
}

static void
nm_connectivity_class_init (NMConnectivityClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);

1162 1163
	signals[CONFIG_CHANGED] =
	    g_signal_new (NM_CONNECTIVITY_CONFIG_CHANGED,
1164 1165 1166 1167
	                  G_OBJECT_CLASS_TYPE (object_class),
	                  G_SIGNAL_RUN_FIRST,
	                  0, NULL, NULL, NULL,
	                  G_TYPE_NONE, 0);
1168

1169
	object_class->dispose = dispose;
1170
}