blob: b66f999ed79ffe4fbac94b9932ee4173c604ca47 [file] [log] [blame]
/*
* Hostroute - create host routes for cellular services based on each
* service's Cellular.UsageUrl property.
*
* This file initially created by Google, Inc.
* Copyright (C) 2011 The Chromium OS Authors. All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* 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 St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <glib.h>
#define CONNMAN_API_SUBJECT_TO_CHANGE
#include <connman/assert.h>
#include <connman/dns_client.h>
#include <connman/plugin.h>
#include <connman/log.h>
#include <connman/notifier.h>
#include <connman/service.h>
#define _DBG_HOSTROUTE(fmt, arg...) DBG(DBG_HOSTROUTE, fmt, ## arg)
/*
* Represents a pending DNS request for |hostname|. We will use the result
* to install a host route through the interface associated with |service|.
*/
struct dns_request {
struct connman_service *service;
char *hostname;
connman_dns_client_request_t handle; /* opaque to us */
};
/*
* List of pending asynchronous name resolution requests. We expect the number
* of pending requests to be small, probably one, because we allow only one
* pending request per cellular service, and there is typically one active
* cellular service per device.
*/
static GList *pending_dns_requests = NULL;
/*
* Destroy a dns_request struct, freeing the resources allocated in
* submit_dns_request. The request must already have been removed from the
* |pending_dns_requests| list.
*/
static void destroy_dns_request(struct dns_request *request)
{
_DBG_HOSTROUTE("request %p", request);
connman_dns_client_cancel_request(request->handle);
connman_service_unref(request->service);
g_free(request->hostname);
g_free(request);
}
/*
* Callback that is invoked by c-ares when an asynchronous name resolution
* request that we have previously initiated is complete.
*/
static void dns_request_cb(void *data,
connman_dns_client_status_t status,
struct sockaddr *ip_addr)
{
struct dns_request *request = data;
char ip_addr_string[INET_ADDRSTRLEN];
struct sockaddr_in *sin;
_DBG_HOSTROUTE("request %p: status = %d", request, status);
pending_dns_requests = g_list_remove(pending_dns_requests, request);
if (status != CONNMAN_DNS_CLIENT_SUCCESS) {
connman_error("%s: ares request for '%s' failed: %s", __func__,
request->hostname,
connman_dns_client_strerror(status));
destroy_dns_request(request);
return;
}
/*
* We only accept IPv4 results because the sticky route code and the
* underlying host route code that it uses only work with IPv4 addrs.
*/
if (ip_addr->sa_family != AF_INET) {
connman_error("%s: invalid addr family: %d", __func__,
ip_addr->sa_family);
destroy_dns_request(request);
return;
}
/* Convert sockaddr into a dotted decimal string. */
sin = (struct sockaddr_in *) ip_addr;
if (inet_ntop(sin->sin_family, &sin->sin_addr, ip_addr_string,
sizeof(ip_addr_string)) == NULL) {
connman_error("%s: could not convert address to string: %s",
__func__, strerror(errno));
destroy_dns_request(request);
return;
}
_DBG_HOSTROUTE("dns request for '%s' succeeded: %s", request->hostname,
ip_addr_string);
/* Ask the flimflam service core to create and maintain a host route. */
connman_service_create_sticky_route(request->service, ip_addr_string);
destroy_dns_request(request);
}
/*
* Initiate an asynchronous name resolution request for |hostname| in the
* context of |service|. Returns TRUE on success and FALSE on failure. Success
* indicates only that the request was initiated, not the success of the
* request itself.
*/
static gboolean submit_dns_request(struct connman_service *service,
const char *hostname)
{
struct dns_request *request;
struct connman_device *device;
int timeout_ms = 0; /* use default timeout */
_DBG_HOSTROUTE("service %p: hostname = %s", service, hostname);
request = g_malloc0(sizeof(struct dns_request));
request->service = connman_service_ref(service);
request->hostname = g_strdup(hostname);
/*
* We want to send DNS requests on the interface associated with the
* cellular service.
*/
device = connman_service_get_device(request->service);
request->handle = connman_dns_client_submit_request(request->hostname,
device,
timeout_ms,
dns_request_cb,
request);
if (request->handle == NULL) {
_DBG_HOSTROUTE("could not submit dns request for \"%s\"",
request->hostname);
destroy_dns_request(request);
return FALSE;
}
pending_dns_requests = g_list_append(pending_dns_requests, request);
return TRUE;
}
/* Cancel any in-progress dns request for |service|. */
static void cancel_dns_request(struct connman_service *service)
{
GList *node;
struct dns_request *request;
_DBG_HOSTROUTE("service %p", service);
for (node = pending_dns_requests; node != NULL;
node = g_list_next(node)) {
request = node->data;
if (request->service == service)
break;
}
if (node == NULL)
return;
pending_dns_requests = g_list_delete_link(pending_dns_requests, node);
destroy_dns_request(request);
}
/* Cancel all in-progress dns requests. */
static void cancel_all_dns_requests()
{
GList *node;
struct dns_request *request;
_DBG_HOSTROUTE("");
while ((node = g_list_first(pending_dns_requests)) != NULL) {
request = node->data;
pending_dns_requests = g_list_delete_link(pending_dns_requests,
node);
destroy_dns_request(request);
}
}
/*
* Parse the hostname component from a url.
* Returns a dynamically allocated string in |*hostname_out| that
* must be g_freed by the caller when no longer needed.
*/
static void hostname_from_url(const char *url, char **hostname_out)
{
const char *start, *end;
const char *path, *params, *query, *fragment, *port;
size_t num_bytes_to_copy;
/* Ignore protocol prefix. */
start = strstr(url, "://");
if (start != NULL)
start += strlen("://");
else
start = url;
/*
* Isolate netloc component of the url.
* We search for the earliest delimiter representing the start of a
* path, params, query, or fragment component of the url.
* See RFC 1808 for details on url syntax.
*/
end = url + strlen(url);
path = strchr(start, '/');
if (path != NULL)
end = path;
params = strchr(start, ';');
if (params != NULL && params < end)
end = params;
query = strchr(start, '?');
if (query != NULL && query < end)
end = query;
fragment = strchr(start, '#');
if (fragment != NULL && fragment < end)
end = fragment;
/* Strip off port if it's part of netloc, leaving hostname. */
port = strchr(start, ':');
if (port != NULL && port < end)
end = port;
num_bytes_to_copy = end - start;
*hostname_out = g_strndup(start, num_bytes_to_copy);
}
/*
* Notification handler that is called by flimflam core when service state
* changes. We begin the process of installing a sticky host route if the
* service is cellular, connected, and doesn't already have one. Once a host
* route is installed, it is managed by the flimflam core for the lifetime of
* the service.
*/
static void hostroute_service_state_changed(struct connman_service *service)
{
const char *type;
const char *state;
const char *usage_url;
const char *sticky_route;
char *hostname;
_DBG_HOSTROUTE("service %p", service);
sticky_route = connman_service_get_sticky_route(service);
if (sticky_route != NULL)
return;
type = connman_service_get_type(service);
if (g_strcmp0(type, "cellular") != 0)
return;
state = connman_service_get_state(service);
if (g_strcmp0(state, "ready") != 0 &&
g_strcmp0(state, "online") != 0 &&
g_strcmp0(state, "portal") != 0) {
cancel_dns_request(service);
return;
}
usage_url = connman_service_get_usage_url(service);
if (usage_url == NULL) {
connman_error("%s: service %p: no usage url", __func__,
service);
return;
}
_DBG_HOSTROUTE("usage_url = %s", usage_url);
hostname_from_url(usage_url, &hostname);
_DBG_HOSTROUTE("hostname = %s", hostname);
cancel_dns_request(service);
if (!submit_dns_request(service, hostname)) {
connman_error("%s: service %p: couldn't submit dns request for "
"%s", __func__, service, hostname);
/* fall through for cleanup */
}
g_free(hostname);
/*
* Host route installation continues in dns_request_cb when the name
* resolution that we've just initiated completes.
*/
}
/* Define a notifier that will notify us on service state changes. */
static struct connman_notifier hostroute_notifier = {
.name = "hostroute",
.priority = CONNMAN_NOTIFIER_PRIORITY_DEFAULT,
.service_state_changed = hostroute_service_state_changed
};
/* Initialize the plugin. */
static int hostroute_init(void)
{
_DBG_HOSTROUTE("");
if (connman_notifier_register(&hostroute_notifier) < 0) {
connman_error("%s: Failed to register hostroute notifier",
__func__);
return -1;
}
return 0;
}
/* Clean up. */
static void hostroute_exit(void)
{
_DBG_HOSTROUTE("");
connman_notifier_unregister(&hostroute_notifier);
cancel_all_dns_requests();
}
/* Register this plugin with the flimflam plugin system. */
CONNMAN_PLUGIN_DEFINE(hostroute, "Hostroute plugin", VERSION,
CONNMAN_PLUGIN_PRIORITY_DEFAULT, hostroute_init, hostroute_exit)