blob: d6204d36a308e4ce6c1b63e1f797970f6c012d47 [file] [log] [blame]
/*
*
* Connection Manager
*
* Copyright (C) 2007-2010 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
*
*/
/*
* Support for vpn plugins. Common code to manage a provider object,
* tun device and a task associated with an external vpn process. The
* vpn plugin is responsible for launching the external process and
* handling notification callbacks to clock the provider state machine.
*
* TODO(sleffler) seems to make more sense in src than plugins
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
#include <stdint.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <linux/if_tun.h>
#include <net/if.h>
#include <dbus/dbus.h>
#include <glib.h>
#include <connman/provider.h>
#include <connman/log.h>
#include <connman/rtnl.h>
#include <connman/task.h>
#include <connman/inet.h>
#include "vpn.h"
#define _DBG_VPN(fmt, arg...) DBG(DBG_VPN, fmt, ## arg)
struct vpn_data {
struct connman_provider *provider;
char *if_name;
unsigned flags;
struct connman_rtnl rtnl;
enum vpn_state state;
struct connman_task *task;
void *vpn_specific_data;
};
struct vpn_driver_data {
const char *name;
const char *program;
struct vpn_driver *vpn_driver;
struct connman_provider_driver provider_driver;
};
static GHashTable *driver_hash = NULL;
static int kill_tun(struct connman_provider *provider)
{
struct vpn_data *data = connman_provider_get_data(provider);
struct vpn_driver_data *vpn_driver_data;
const char *name;
struct ifreq ifr;
int fd, err;
if (data == NULL)
return -1;
name = connman_provider_get_driver_name(provider);
vpn_driver_data = g_hash_table_lookup(driver_hash, name);
if (vpn_driver_data != NULL && vpn_driver_data->vpn_driver !=NULL &&
vpn_driver_data->vpn_driver->flags == VPN_FLAG_NO_TUN)
return 0;
memset(&ifr, 0, sizeof(ifr));
ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
strncpy(ifr.ifr_name, data->if_name, sizeof(ifr.ifr_name));
fd = open("/dev/net/tun", O_RDWR);
if (fd < 0) {
err = -errno;
connman_error("Failed to open /dev/net/tun to device %s: %s",
data->if_name, strerror(errno));
return err;
}
if (ioctl(fd, TUNSETIFF, (void *)&ifr)) {
err = -errno;
connman_error("Failed to TUNSETIFF for device %s to it: %s",
data->if_name, strerror(errno));
close(fd);
return err;
}
if (ioctl(fd, TUNSETPERSIST, 0)) {
err = -errno;
connman_error("Failed to set tun device %s nonpersistent: %s",
data->if_name, strerror(errno));
close(fd);
return err;
}
close(fd);
_DBG_VPN("Killed tun device %s", data->if_name);
return 0;
}
void vpn_died(struct connman_task *task, void *user_data,
enum connman_provider_error error)
{
struct connman_provider *provider = user_data;
struct vpn_data *data = connman_provider_get_data(provider);
_DBG_VPN("provider %p data %p", provider, data);
if (data != NULL) {
kill_tun(provider);
connman_provider_set_data(provider, NULL);
connman_rtnl_unregister(&data->rtnl);
_DBG_VPN("provider %p vpn state %d", provider, data->state);
switch (data->state) {
case VPN_STATE_CONNECT:
case VPN_STATE_RECONNECT:
connman_provider_indicate_error(provider, error);
break;
default:
connman_provider_set_state(provider,
CONNMAN_PROVIDER_STATE_IDLE);
break;
}
}
connman_provider_set_index(provider, -1);
connman_provider_set_interface(provider, NULL);
if (data != NULL) {
connman_provider_unref(data->provider);
g_free(data);
}
connman_task_destroy(task);
}
int vpn_set_ifname(struct connman_provider *provider, const char *ifname)
{
struct vpn_data *data = connman_provider_get_data(provider);
int index;
_DBG_VPN("provider %p ifname %s", provider, ifname);
if (data == NULL) {
_DBG_VPN("%s: provider data not accessible", __func__);
return -EIO;
}
if (ifname == NULL) {
_DBG_VPN("%s: ifname not provided", __func__);
return -EIO;
}
index = connman_inet_ifindex(ifname);
if (index < 0) {
_DBG_VPN("%s: could not get ifindex from %s", __func__, ifname);
return -EIO;
}
data->if_name = (char *)g_strdup(ifname);
connman_provider_set_index(provider, index);
connman_provider_set_interface(provider, data->if_name);
return 0;
}
void *vpn_get_specific_data(struct connman_provider *vpn)
{
struct vpn_data *data;
data = connman_provider_get_data(vpn);
if (data == NULL)
return NULL;
return data->vpn_specific_data;
}
void vpn_set_specific_data(struct connman_provider *vpn,
void *vpn_specific_data)
{
struct vpn_data *data;
data = connman_provider_get_data(vpn);
if (data == NULL)
return;
data->vpn_specific_data = vpn_specific_data;
}
static void vpn_newlink(void *user_data, int index, unsigned short type,
const char *ifname, unsigned flags, int change)
{
struct connman_provider *provider = user_data;
struct vpn_data *data;
data = connman_provider_get_data(provider);
_DBG_VPN("provider %p vpn state %d cur flags 0x%x new flags 0x%x",
provider, data->state, data->flags, flags);
if ((data->flags & IFF_UP) != (flags & IFF_UP)) {
if (flags & IFF_UP) {
data->state = VPN_STATE_READY;
connman_provider_set_state(provider,
CONNMAN_PROVIDER_STATE_READY);
}
}
data->flags = flags;
}
static DBusMessage *vpn_notify(struct connman_task *task,
DBusMessage *msg, void *user_data)
{
struct connman_provider *provider = user_data;
struct vpn_data *data = connman_provider_get_data(provider);
struct vpn_driver_data *vpn_driver_data;
const char *name;
enum vpn_state state;
name = connman_provider_get_driver_name(provider);
vpn_driver_data = g_hash_table_lookup(driver_hash, name);
if (vpn_driver_data == NULL)
return NULL;
/*
* NB: Drivers return a state value but it's just used
* to determine success/failure (i.e. it does not, for
* example, indicate the new state of the vpn). This is
* inherited from upstream and should be re-done...
*/
state = vpn_driver_data->vpn_driver->notify(msg, provider);
if (state != VPN_STATE_CONNECT) {
connman_provider_set_state(provider,
CONNMAN_PROVIDER_STATE_DISCONNECT);
return NULL;
}
if (data->state == VPN_STATE_CONNECT) {
data->rtnl.index = connman_provider_get_index(provider);
connman_rtnl_register(&data->rtnl);
connman_inet_ifup(data->rtnl.index);
} else if (data->state == VPN_STATE_RECONNECT) {
data->state = VPN_STATE_READY;
connman_provider_set_state(provider,
CONNMAN_PROVIDER_STATE_READY);
}
return NULL;
}
void vpn_reconnect(struct connman_provider *provider)
{
struct vpn_data *data = connman_provider_get_data(provider);
if (data == NULL) {
connman_error("%s: no vpn data for provider %p",
__func__, provider);
return;
}
/* drop default route, et al so vpn can re-resolve remote hostnames */
connman_provider_ipconfig_clear(provider);
data->state = VPN_STATE_RECONNECT;
connman_provider_set_state(provider, CONNMAN_PROVIDER_STATE_CONNECT);
}
static int vpn_create_tun(struct connman_provider *provider)
{
struct vpn_data *data = connman_provider_get_data(provider);
struct ifreq ifr;
int i, fd, index;
int ret = 0;
if (data == NULL) {
connman_error("%s: called out of order", __func__);
return -EIO;
}
fd = open("/dev/net/tun", O_RDWR);
if (fd < 0) {
i = -errno;
connman_error("%s: failed to open /dev/net/tun: %s",
__func__, strerror(errno));
ret = i;
goto exist_err;
}
memset(&ifr, 0, sizeof(ifr));
ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
for (i = 0; i < 256; i++) {
sprintf(ifr.ifr_name, "vpn%d", i);
if (!ioctl(fd, TUNSETIFF, (void *)&ifr))
break;
}
if (i == 256) {
connman_error("%s: failed to find available tun device",
__func__);
close(fd);
ret = -ENODEV;
goto exist_err;
}
data->if_name = (char *)g_strdup(ifr.ifr_name);
if (!data->if_name) {
ret = -ENOMEM;
goto exist_err;
}
if (ioctl(fd, TUNSETPERSIST, 1)) {
i = -errno;
connman_error("%s: failed to set tun persistent: %s",
__func__, strerror(errno));
close(fd);
ret = i;
goto exist_err;
}
close(fd);
index = connman_inet_ifindex(data->if_name);
if (index < 0) {
connman_error("%s: failed to get tun ifindex", __func__);
kill_tun(provider);
ret = -EIO;
goto exist_err;
}
connman_provider_set_index(provider, index);
connman_provider_set_interface(provider, data->if_name);
return 0;
exist_err:
return ret;
}
static int vpn_connect(struct connman_provider *provider)
{
struct vpn_data *data = connman_provider_get_data(provider);
struct vpn_driver_data *vpn_driver_data;
const char *name;
int ret = 0;
if (data != NULL) {
_DBG_VPN("%s: data != NULL", __func__);
return -EISCONN;
}
data = g_try_new0(struct vpn_data, 1);
if (data == NULL)
return -ENOMEM;
data->provider = connman_provider_ref(provider);
data->flags = 0;
data->task = NULL;
data->state = VPN_STATE_IDLE;
/* NB: index set when register'ing, may not be available here */
RTNL_INIT(&data->rtnl,
connman_provider_get_ident(provider), /* name */
CONNMAN_RTNL_PRIORITY_DEFAULT, /* priority */
CONNMAN_RTNL_DEVICE_ANY,
provider /* private */
);
data->rtnl.newlink = vpn_newlink;
connman_provider_set_data(provider, data);
name = connman_provider_get_driver_name(provider);
vpn_driver_data = g_hash_table_lookup(driver_hash, name);
if (vpn_driver_data != NULL && vpn_driver_data->vpn_driver != NULL &&
vpn_driver_data->vpn_driver->flags != VPN_FLAG_NO_TUN) {
ret = vpn_create_tun(provider);
if (ret < 0)
goto exist_err;
}
data->task = connman_task_create(vpn_driver_data->program);
if (data->task == NULL) {
ret = -ENOMEM;
kill_tun(provider);
goto exist_err;
}
if (connman_task_set_notify(data->task, "notify",
vpn_notify, provider)) {
ret = -ENOMEM;
kill_tun(provider);
connman_task_destroy(data->task);
data->task = NULL;
goto exist_err;
}
ret = vpn_driver_data->vpn_driver->connect(provider, data->task,
data->if_name);
if (ret < 0) {
kill_tun(provider);
connman_task_destroy(data->task);
data->task = NULL;
goto exist_err;
}
_DBG_VPN("%s started with dev %s",
vpn_driver_data->provider_driver.name, data->if_name);
data->state = VPN_STATE_CONNECT;
connman_provider_set_state(provider, CONNMAN_PROVIDER_STATE_CONNECT);
return -EINPROGRESS;
exist_err:
connman_provider_set_index(provider, -1);
connman_provider_set_interface(provider, NULL);
connman_provider_set_data(provider, NULL);
connman_provider_unref(data->provider);
g_free(data);
return ret;
}
static int vpn_probe(struct connman_provider *provider)
{
return 0;
}
static int vpn_disconnect(struct connman_provider *provider)
{
struct vpn_data *data = connman_provider_get_data(provider);
struct vpn_driver_data *vpn_driver_data;
const char *name;
_DBG_VPN("disconnect provider %p data %p", provider, data);
if (data == NULL)
return 0;
name = connman_provider_get_driver_name(provider);
vpn_driver_data = g_hash_table_lookup(driver_hash, name);
if (vpn_driver_data->vpn_driver->disconnect)
vpn_driver_data->vpn_driver->disconnect();
connman_rtnl_unregister(&data->rtnl);
data->state = VPN_STATE_DISCONNECT;
connman_task_stop(data->task);
return 0;
}
static int vpn_remove(struct connman_provider *provider)
{
struct vpn_data *data;
data = connman_provider_get_data(provider);
if (data == NULL)
return 0;
connman_rtnl_unregister(&data->rtnl);
connman_task_stop(data->task);
g_usleep(G_USEC_PER_SEC);
kill_tun(provider);
connman_provider_set_data(provider, NULL);
return 0;
}
int vpn_register(const char *name, struct vpn_driver *vpn_driver,
const char *program)
{
struct vpn_driver_data *data;
_DBG_VPN("name %s program %s", name, program);
data = g_try_new0(struct vpn_driver_data, 1);
if (data == NULL)
return -ENOMEM;
data->name = name;
data->program = program;
data->vpn_driver = vpn_driver;
data->provider_driver.name = name;
data->provider_driver.disconnect = vpn_disconnect;
data->provider_driver.connect = vpn_connect;
data->provider_driver.probe = vpn_probe;
data->provider_driver.remove = vpn_remove;
data->provider_driver.append_props = vpn_driver->append_props;
data->provider_driver.save_props = vpn_driver->save_props;
data->provider_driver.load_props = vpn_driver->load_props;
if (driver_hash == NULL) {
driver_hash = g_hash_table_new_full(g_str_hash,
g_str_equal,
NULL, g_free);
}
g_hash_table_insert(driver_hash, (char *)name, data);
connman_provider_driver_register(&data->provider_driver);
return 0;
}
void vpn_unregister(const char *name)
{
struct vpn_driver_data *data;
data = g_hash_table_lookup(driver_hash, name);
if (data == NULL)
return;
connman_provider_driver_unregister(&data->provider_driver);
g_hash_table_remove(driver_hash, name);
if (g_hash_table_size(driver_hash) == 0)
g_hash_table_destroy(driver_hash);
}