blob: c41a187f67e9f865b58f6d897874f29da6524572 [file] [log] [blame]
/*
*
* Connection Manager
*
* Copyright (C) 2007-2009 Intel Corporation. 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 <unistd.h>
#include <sys/wait.h>
#include <glib/gstdio.h>
#include <errno.h>
#include <string.h>
#include <sys/ioctl.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <net/route.h>
#define CONNMAN_API_SUBJECT_TO_CHANGE
#include <connman/plugin.h>
#include <connman/element.h>
#include <connman/ipconfig.h>
#include <connman/inet.h>
#include <connman/dbus.h>
#include <connman/log.h>
#include <connman/profile.h>
#include <connman/resolver.h>
#define _DBG_DHCPCD(fmt, arg...) DBG(DBG_DHCLIENT, fmt, ## arg)
#define DHCPCD_INTF "org.chromium.dhcpcd"
#define DHCPCD_PATH "/org/chromium/dhcpcd"
#define MAXDNS 10 /* max DNS servers per lease */
#define MAXDOMAINS 10 /* max DNS search domains per lease */
#define ADDR_SIZE sizeof("255.255.255.255") /* NB: includes \0 */
/* TODO(sleffler) rewrite using task support */
struct dhcpcd_task {
GPid pid;
char *dbus_address;
gboolean killed;
struct connman_ipconfig *ipconfig;
};
static GSList *task_list = NULL;
static DBusConnection *connection;
static int dhcpcd_renew(struct connman_ipconfig *ipconfig);
static struct dhcpcd_task *find_task_by_pid(GPid pid)
{
GSList *list;
for (list = task_list; list; list = list->next) {
struct dhcpcd_task *task = list->data;
if (task->pid == pid)
return task;
}
return NULL;
}
static struct dhcpcd_task *find_task_by_ipconfig(
const struct connman_ipconfig *ipconfig)
{
GSList *list;
for (list = task_list; list; list = list->next) {
struct dhcpcd_task *task = list->data;
if (task->ipconfig == ipconfig && task->killed == FALSE)
return task;
}
return NULL;
}
static void kill_task(struct dhcpcd_task *task)
{
const char *ifname = connman_ipconfig_get_ifname(task->ipconfig);
_DBG_DHCPCD("task %p name %s pid %d", task, ifname, task->pid);
if (task->killed == FALSE && task->pid > 0) {
if (kill(task->pid, SIGTERM) < 0 && errno != ESRCH) {
connman_error("dhcpcd(%s): kill pid %d: %s",
ifname, task->pid, strerror(errno));
/* TODO(sleffler) send SIGKILL? */
}
task->killed = TRUE;
}
}
static void unlink_task(struct dhcpcd_task *task)
{
const char *ifname = connman_ipconfig_get_ifname(task->ipconfig);
gchar *pathname;
_DBG_DHCPCD("task %p name %s pid %d", task, ifname, task->pid);
pathname = g_strdup_printf("/var/run/dhcpcd/dhcpcd-%s.pid", ifname);
g_unlink(pathname);
g_free(pathname);
pathname = g_strdup_printf("/var/lib/dhcpcd/dhcpcd-%s.lease", ifname);
g_unlink(pathname);
g_free(pathname);
}
static int start_dhcpcd(struct dhcpcd_task *task);
static void task_died(GPid pid, gint status, gpointer data)
{
struct dhcpcd_task *task = data;
if (WIFEXITED(status))
_DBG_DHCPCD("exit status %d for %s", WEXITSTATUS(status),
connman_ipconfig_get_ifname(task->ipconfig));
else
_DBG_DHCPCD("signal %d killed %s", WTERMSIG(status),
connman_ipconfig_get_ifname(task->ipconfig));
g_spawn_close_pid(pid);
task->pid = 0;
task_list = g_slist_remove(task_list, task);
unlink_task(task);
g_free(task->dbus_address);
g_free(task);
}
static void task_setup(gpointer data)
{
struct dhcpcd_task *task = data;
_DBG_DHCPCD("task %p name %s", task,
connman_ipconfig_get_ifname(task->ipconfig));
task->killed = FALSE;
}
static int start_dhcpcd(struct dhcpcd_task *task)
{
const char *ifname = connman_ipconfig_get_ifname(task->ipconfig);
char *argv[5], *envp[1];
argv[0] = DHCPCD;
argv[1] = "-B"; /* foreground */
argv[2] = (char *) ifname; /* TODO(sleffler) __DECONST */
if (connman_profile_get_arpgateway()) {
argv[3] = "-R"; /* ARP for default gateway */
argv[4] = NULL;
} else
argv[3] = NULL;
envp[0] = NULL;
if (g_spawn_async(NULL, argv, envp, G_SPAWN_DO_NOT_REAP_CHILD,
task_setup, task, &task->pid, NULL) == FALSE) {
connman_error("%s: failed to spawn %s for %s",
__func__, DHCPCD, ifname);
return -1;
}
task_list = g_slist_append(task_list, task);
g_child_watch_add(task->pid, task_died, task);
_DBG_DHCPCD("spawn %s with pid %d", DHCPCD, task->pid);
return 0;
}
static int dhcpcd_request(struct connman_ipconfig *ipconfig)
{
struct dhcpcd_task *task;
const char *ifname;
ifname = connman_ipconfig_get_ifname(ipconfig);
_DBG_DHCPCD("request %s ipconfig %p", ifname, ipconfig);
task = find_task_by_ipconfig(ipconfig);
if (task != NULL) {
if (task->dbus_address != NULL) {
/*
* Just ask the existing process to renew it's current
* lease. If that fails it will fallback quicky to
* requesting a new lease.
*/
return dhcpcd_renew(ipconfig);
} else {
/*
* We are in an uncomfortable situation where
* we have a dhcpcd task running, but we don't
* (yet) have a dbus handle for it. Kill this
* (since we do know its PID), and start a new
* one.
*/
kill_task(task);
}
}
if (ifname == NULL)
return -EINVAL;
task = g_try_new0(struct dhcpcd_task, 1);
if (task == NULL)
return -ENOMEM;
task->ipconfig = ipconfig;
return start_dhcpcd(task);
}
static int dhcpcd_renew(struct connman_ipconfig *ipconfig)
{
DBusMessage *message, *reply;
DBusError error;
const char *ifname = connman_ipconfig_get_ifname(ipconfig);
struct dhcpcd_task *task;
_DBG_DHCPCD("renew %s", ifname);
if (ifname == NULL)
return -EINVAL;
task = find_task_by_ipconfig(ipconfig);
if (task == NULL)
return -EINVAL; /* XXX */
if (task->dbus_address == NULL)
return -EINVAL;
message = dbus_message_new_method_call(task->dbus_address, DHCPCD_PATH,
DHCPCD_INTF, "Rebind");
if (message == NULL)
return -ENOMEM;
dbus_error_init(&error);
/* TODO(njw): If dhcpcd changes to synchronously perform the
operation before returning, this will hang until it's done,
so it might be better to use the nonblocking call here. */
reply = dbus_connection_send_with_reply_and_block(connection, message,
-1, &error);
if (reply == NULL) {
if (dbus_error_is_set(&error) == TRUE) {
connman_error("%s: %s", __func__, error.message);
dbus_error_free(&error);
} else
connman_error("%s: failed", __func__);
dbus_message_unref(message);
return -EIO;
}
dbus_message_unref(message);
return 0;
}
static int dhcpcd_release(struct connman_ipconfig *ipconfig)
{
DBusMessage *message, *reply;
DBusError error;
const char *ifname = connman_ipconfig_get_ifname(ipconfig);
struct dhcpcd_task *task;
_DBG_DHCPCD("ipconfig %p ifname %s", ipconfig, ifname);
task = find_task_by_ipconfig(ipconfig);
if (task == NULL)
return -EINVAL;
if (task->dbus_address == NULL) {
kill_task(task);
return -EINVAL;
}
message = dbus_message_new_method_call(task->dbus_address, DHCPCD_PATH,
DHCPCD_INTF, "Release");
if (message == NULL)
return -ENOMEM;
dbus_error_init(&error);
/* TODO(njw): See dhcpcd_renew() */
reply = dbus_connection_send_with_reply_and_block(connection, message,
-1, &error);
if (reply == NULL) {
if (dbus_error_is_set(&error) == TRUE) {
connman_error("%s: %s", __func__, error.message);
dbus_error_free(&error);
} else
connman_error("%s: failed", __func__);
dbus_message_unref(message);
return -EIO;
}
dbus_message_unref(message);
kill_task(task);
return 0;
}
static struct connman_ipconfig_driver dhcpcd_driver = {
.name = "dhcpcd",
.type = CONNMAN_IPCONFIG_TYPE_DHCP,
.priority = CONNMAN_IPCONFIG_PRIORITY_DEFAULT,
.request = dhcpcd_request,
.release = dhcpcd_release,
.renew = dhcpcd_renew,
};
static DBusHandlerResult bad_type(const char *key, int type_expect, int type_got)
{
connman_error("dhcpcd_filter: bad d-bus type for key %s: "
"got 0x%x, expected 0x%x", key, type_got, type_expect);
return DBUS_HANDLER_RESULT_HANDLED;
}
static DBusHandlerResult bad_array_size(const char *key, int count, int max)
{
connman_error("dhcpcd_filter: bad d-bus array size for key %s: "
"got %d elements, max %d", key, count, max);
return DBUS_HANDLER_RESULT_HANDLED;
}
/*
* D-Bus helper functions for parsing basic values enclosed in a variant.
*/
static int _get_basic(DBusMessageIter *iter, const char *key,
int basic_type, void *value)
{
DBusMessageIter entry;
int type;
dbus_message_iter_recurse(iter, &entry);
type = dbus_message_iter_get_arg_type(&entry);
if (type != basic_type) {
(void) bad_type(key, basic_type, type);
return FALSE;
} else {
dbus_message_iter_get_basic(&entry, value);
return TRUE;
}
}
static int get_byte(DBusMessageIter *iter, const char *key, uint8_t *value)
{
int status = _get_basic(iter, key, DBUS_TYPE_BYTE, value);
_DBG_DHCPCD("%s = 0x%x (byte)", key, *value);
return status;
}
static int get_uint16(DBusMessageIter *iter, const char *key, uint16_t *value)
{
int status = _get_basic(iter, key, DBUS_TYPE_UINT16, value);
_DBG_DHCPCD("%s = 0x%x (uint16)", key, *value);
return status;
}
#if 0
static int get_uint32(DBusMessageIter *iter, const char *key, uint32_t *value)
{
int status = _get_basic(iter, key, DBUS_TYPE_UINT32, value);
_DBG_DHCPCD("%s = 0x%x (uint32)", key, *value);
return status;
}
#endif
static int get_string(DBusMessageIter *iter, const char *key, char **value)
{
int status = _get_basic(iter, key, DBUS_TYPE_STRING, value);
_DBG_DHCPCD("%s = %s (string)", key, *value);
return status;
}
/*
* Convert an IPv4 address in network byte order to dot'd quad string.
* Return 1 if successful, 0 if there is a problem in the conversion.
*/
static int _cvt_ipv4(char str[ADDR_SIZE], uint32_t addr)
{
return (inet_ntop(AF_INET, &addr, str, ADDR_SIZE) != NULL);
}
/*
* Get an IPv4 address and return as a string. Return TRUE on
* success, FALSE if a protocol or conversion botch.
*/
static int get_addr(DBusMessageIter *iter, const char *key,
char local_addr[ADDR_SIZE])
{
uint32_t value;
if (_get_basic(iter, key, DBUS_TYPE_UINT32, &value) == TRUE &&
_cvt_ipv4(local_addr, value) == TRUE) {
_DBG_DHCPCD("%s = %s (0x%x)", key, local_addr, value);
return TRUE;
} else
return FALSE;
}
/*
* D-Bus helper functions for parsing an array of basic values.
*/
static int _get_array_basic(DBusMessageIter *iter, const char *key,
int basic_type, void *value, int *nelems, int max_elems)
{
DBusMessageIter array, entry;
int type;
*nelems = 0;
dbus_message_iter_recurse(iter, &array);
type = dbus_message_iter_get_arg_type(&array);
if (type != DBUS_TYPE_ARRAY) {
(void) bad_type(key, DBUS_TYPE_ARRAY, type);
return FALSE;
}
dbus_message_iter_recurse(&array, &entry);
/* TODO(sleffler) verify dbus_type_is_fixed */
type = dbus_message_iter_get_arg_type(&entry);
if (type != basic_type) {
(void) bad_type(key, basic_type, type);
return FALSE;
} else {
dbus_message_iter_get_fixed_array(&entry, value, nelems);
if ((*nelems) > max_elems)
return bad_array_size(key, *nelems, max_elems);
return TRUE;
}
}
static int get_array_uint32(DBusMessageIter *iter, const char *key,
uint32_t **value, int *nelems, int max_elems)
{
int status = _get_array_basic(iter, key, DBUS_TYPE_UINT32,
value, nelems, max_elems);
_DBG_DHCPCD("%s = [%d]{ 0x%x, 0x%x, 0x%x, 0x%x, 0x%x }", key, *nelems
, (*nelems > 0) ? (*value)[0] : -1
, (*nelems > 1) ? (*value)[1] : -1
, (*nelems > 2) ? (*value)[2] : -1
, (*nelems > 3) ? (*value)[3] : -1
, (*nelems > 4) ? (*value)[4] : -1
);
return status;
}
/*
* Parse and return an array of strings. The returned data are references
* to the d-bus data and most be copied. The array size is returned in nelems.
* The max_elems parameter specifies the maximum number of elements to accept;
* if more elements are present in the d-bus data then FALSE is returned.
* Otherwise TRUE is returned on success or FALSE if the wire format is
* malformed.
*/
static int get_array_string(DBusMessageIter *iter, const char *key,
char **value, int *nelems, int max_elems)
{
DBusMessageIter array, entry;
int type;
*nelems = 0;
dbus_message_iter_recurse(iter, &array);
type = dbus_message_iter_get_arg_type(&array);
if (type != DBUS_TYPE_ARRAY) {
(void) bad_type(key, DBUS_TYPE_ARRAY, type);
return FALSE;
}
type = dbus_message_iter_get_element_type(&array);
if (type != DBUS_TYPE_STRING)
return bad_type(key, DBUS_TYPE_STRING, type);
dbus_message_iter_recurse(&array, &entry);
while (dbus_message_iter_get_arg_type(&entry) != DBUS_TYPE_INVALID) {
if ((*nelems) >= max_elems)
return bad_array_size(key, (*nelems), max_elems);
dbus_message_iter_get_basic(&entry, &value[*nelems]);
_DBG_DHCPCD("%s = [%d]{ %s }", key, *nelems, value[*nelems]);
(*nelems)++;
dbus_message_iter_next(&entry);
}
return TRUE;
}
static DBusHandlerResult dhcpcd_filter(DBusConnection *conn,
DBusMessage *msg, void *data)
{
#define iseq(_a, _b) (g_ascii_strcasecmp((_a), (_b)) == 0)
DBusMessageIter iter, dict;
dbus_uint32_t pid;
struct dhcpcd_task *task;
const char *reason, *key;
char *value;
struct connman_ipaddress ipaddr;
char local_addr[ADDR_SIZE];
char bcast_addr[ADDR_SIZE];
char gateway_addr[ADDR_SIZE];
char name_servers[MAXDNS][ADDR_SIZE], *dns_servers[MAXDNS+1];
char *search_domains[MAXDOMAINS+1];
uint32_t *uint32_array;
int i, nelems;
dbus_bool_t is_event, is_status;
is_event = dbus_message_is_signal(msg, DHCPCD_INTF, "Event");
is_status = dbus_message_is_signal(msg, DHCPCD_INTF, "StatusChanged");
/* Accept StatusChanged events long enough that we can get dbus addr */
if (is_event == FALSE && is_status == FALSE)
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
dbus_message_iter_init(msg, &iter);
/* PID of dhcpcd process */
dbus_message_iter_get_basic(&iter, &pid);
task = find_task_by_pid(pid);
if (task == NULL) {
connman_error("%s: no task for pid %d", __func__, pid);
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
/* Save the DBus address of the sender if we don't have it yet */
if (task->dbus_address == NULL) {
const char *message_dbus_address;
message_dbus_address = dbus_message_get_sender(msg);
task->dbus_address = g_strdup(message_dbus_address);
_DBG_DHCPCD("pid %d has DBus address %s", pid, task->dbus_address);
}
/* Past here, only events are handled */
if (is_event == FALSE)
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
dbus_message_iter_next(&iter);
/* reason for message */
dbus_message_iter_get_basic(&iter, &reason);
dbus_message_iter_next(&iter);
_DBG_DHCPCD("change %s state to %s",
connman_ipconfig_get_ifname(task->ipconfig), reason);
memset(&ipaddr, 0, sizeof(ipaddr));
ipaddr.af = AF_INET;
ipaddr.mask |= CONNMAN_IPCONFIG_AF;
dbus_message_iter_recurse(&iter, &dict);
while (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY) {
DBusMessageIter entry;
int type;
dbus_message_iter_recurse(&dict, &entry);
dbus_message_iter_get_basic(&entry, &key);
dbus_message_iter_next(&entry);
type = dbus_message_iter_get_arg_type(&entry);
if (type != DBUS_TYPE_VARIANT)
return bad_type(key, DBUS_TYPE_VARIANT, type);
if (iseq(key, "IPAddress")) {
if (get_addr(&entry, key, local_addr) == FALSE)
return DBUS_HANDLER_RESULT_HANDLED;
ipaddr.local = local_addr;
ipaddr.mask |= CONNMAN_IPCONFIG_LOCAL;
} else if (iseq(key, "SubnetCIDR")) {
uint8_t byte_value;
if (get_byte(&entry, key, &byte_value) == FALSE)
return DBUS_HANDLER_RESULT_HANDLED;
ipaddr.prefixlen = byte_value;
ipaddr.mask |= CONNMAN_IPCONFIG_PREFIX;
} else if (iseq(key, "BroadcastAddress")) {
if (get_addr(&entry, key, bcast_addr) == FALSE)
return DBUS_HANDLER_RESULT_HANDLED;
ipaddr.broadcast = bcast_addr;
ipaddr.mask |= CONNMAN_IPCONFIG_BCAST;
} else if (iseq(key, "Routers")) {
if (get_array_uint32(&entry, key,
/* NB: 1000 is just "big" */
&uint32_array, &nelems, 1000) == FALSE)
return DBUS_HANDLER_RESULT_HANDLED;
/* NB: silently discard extra routers */
if (nelems < 1)
return bad_array_size(key, nelems, 1);
/* TODO(sleffler) check return */
(void) _cvt_ipv4(gateway_addr, uint32_array[0]);
ipaddr.gateway = gateway_addr;
ipaddr.mask |= CONNMAN_IPCONFIG_GW;
} else if (iseq(key, "DomainNameServers")) {
if (get_array_uint32(&entry, key,
&uint32_array, &nelems, 10) == FALSE)
return DBUS_HANDLER_RESULT_HANDLED;
for (i = 0; i < nelems; i++) {
dns_servers[i] = &name_servers[i][0];
_cvt_ipv4(dns_servers[i], uint32_array[i]);
}
dns_servers[i] = NULL; /* NB: for g_strdupv */
ipaddr.dns_servers = dns_servers;
ipaddr.mask |= CONNMAN_IPCONFIG_DNS;
} else if (iseq(key, "DomainName")) {
if (get_string(&entry, key, &value) == FALSE)
return DBUS_HANDLER_RESULT_HANDLED;
ipaddr.domain_name = value;
ipaddr.mask |= CONNMAN_IPCONFIG_DOMAIN;
} else if (iseq(key, "DomainSearch")) {
if (get_array_string(&entry, key,
search_domains, &nelems, 10) == FALSE)
return DBUS_HANDLER_RESULT_HANDLED;
search_domains[nelems] = NULL; /* NB: for g_strdupv */
ipaddr.search_domains = search_domains;
ipaddr.mask |= CONNMAN_IPCONFIG_SEARCH;
} else if (iseq(key, "Hostname")) {
if (get_string(&entry, key, &value) == FALSE)
return DBUS_HANDLER_RESULT_HANDLED;
/* TODO(sleffler) add support */
} else if (iseq(key, "InterfaceMTU")) {
uint16_t uint16_value;
if (get_uint16(&entry, key, &uint16_value) == FALSE)
return DBUS_HANDLER_RESULT_HANDLED;
/* NB: restrict min MTU per dhcpcd-script */
if (576 <= uint16_value) {
ipaddr.mtu = uint16_value;
ipaddr.mask |= CONNMAN_IPCONFIG_MTU;
} else
_DBG_DHCPCD("%s = %u (skipped)", key,
uint16_value);
} else
_DBG_DHCPCD("ignore key %s", key);
dbus_message_iter_next(&dict);
}
if (g_ascii_strcasecmp(reason, "BOUND") == 0
|| g_ascii_strcasecmp(reason, "REBOOT") == 0
/*
* Even if IP state hasn't changed, this is the path to
* bringing the service from CONFIGURATION state to READY.
*/
|| g_ascii_strcasecmp(reason, "RENEW") == 0
|| g_ascii_strcasecmp(reason, "REBIND") == 0
) {
connman_ipconfig_bind(task->ipconfig, &ipaddr);
} else if (g_ascii_strcasecmp(reason, "FAIL") == 0) {
connman_ipconfig_set_error(task->ipconfig,
CONNMAN_ELEMENT_ERROR_DHCP_FAILED);
}
return DBUS_HANDLER_RESULT_HANDLED;
#undef iseq
}
static const char *dhcpcd_rule = "path=" DHCPCD_PATH
",interface=" DHCPCD_INTF;
static int dhcpcd_init(void)
{
int err;
/* TODO(sleffler) check return values */
connection = connman_dbus_get_connection();
dbus_connection_add_filter(connection, dhcpcd_filter, NULL, NULL);
dbus_bus_add_match(connection, dhcpcd_rule, NULL);
err = connman_ipconfig_driver_register(&dhcpcd_driver);
if (err < 0)
dbus_connection_unref(connection);
return err;
}
static void dhcpcd_exit(void)
{
GSList *list;
for (list = task_list; list; list = list->next) {
struct dhcpcd_task *task = list->data;
_DBG_DHCPCD("kill process %d", task->pid);
kill_task(task);
unlink_task(task);
}
g_slist_free(task_list);
connman_ipconfig_driver_unregister(&dhcpcd_driver);
dbus_bus_remove_match(connection, dhcpcd_rule, NULL);
dbus_connection_remove_filter(connection, dhcpcd_filter, NULL);
dbus_connection_unref(connection);
}
CONNMAN_PLUGIN_DEFINE(dhcpcd, "Chrome OS DHCP client", VERSION,
CONNMAN_PLUGIN_PRIORITY_DEFAULT, dhcpcd_init, dhcpcd_exit)