kernel-pfroute: Reinstall routes on interface/address changes
authorTobias Brunner <tobias@strongswan.org>
Wed, 10 Jul 2013 10:14:19 +0000 (12:14 +0200)
committerTobias Brunner <tobias@strongswan.org>
Wed, 17 Jul 2013 15:45:17 +0000 (17:45 +0200)
src/libhydra/plugins/kernel_pfroute/kernel_pfroute_net.c

index 88ccb97..28802ef 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2009-2012 Tobias Brunner
+ * Copyright (C) 2009-2013 Tobias Brunner
  * Hochschule fuer Technik Rapperswil
  *
  * This program is free software; you can redistribute it and/or modify it
@@ -53,6 +53,9 @@
 /** delay before firing roam events (ms) */
 #define ROAM_DELAY 100
 
+/** delay before reinstalling routes (ms) */
+#define ROUTE_DELAY 100
+
 typedef struct addr_entry_t addr_entry_t;
 
 /**
@@ -176,6 +179,110 @@ static bool addr_map_entry_match_up(addr_map_entry_t *a, addr_map_entry_t *b)
        return iface_entry_up(b->iface) && a->ip->ip_equals(a->ip, b->ip);
 }
 
+typedef struct route_entry_t route_entry_t;
+
+/**
+ * Installed routing entry
+ */
+struct route_entry_t {
+       /** Name of the interface the route is bound to */
+       char *if_name;
+
+       /** Gateway for this route */
+       host_t *gateway;
+
+       /** Destination net */
+       chunk_t dst_net;
+
+       /** Destination net prefixlen */
+       u_int8_t prefixlen;
+};
+
+/**
+ * Clone a route_entry_t object.
+ */
+static route_entry_t *route_entry_clone(route_entry_t *this)
+{
+       route_entry_t *route;
+
+       INIT(route,
+               .if_name = strdup(this->if_name),
+               .gateway = this->gateway ? this->gateway->clone(this->gateway) : NULL,
+               .dst_net = chunk_clone(this->dst_net),
+               .prefixlen = this->prefixlen,
+       );
+       return route;
+}
+
+/**
+ * Destroy a route_entry_t object
+ */
+static void route_entry_destroy(route_entry_t *this)
+{
+       free(this->if_name);
+       DESTROY_IF(this->gateway);
+       chunk_free(&this->dst_net);
+       free(this);
+}
+
+/**
+ * Hash a route_entry_t object
+ */
+static u_int route_entry_hash(route_entry_t *this)
+{
+       return chunk_hash_inc(chunk_from_thing(this->prefixlen),
+                                                 chunk_hash(this->dst_net));
+}
+
+/**
+ * Compare two route_entry_t objects
+ */
+static bool route_entry_equals(route_entry_t *a, route_entry_t *b)
+{
+       if (a->if_name && b->if_name && streq(a->if_name, b->if_name) &&
+               chunk_equals(a->dst_net, b->dst_net) && a->prefixlen == b->prefixlen)
+       {
+               return (!a->gateway && !b->gateway) || (a->gateway && b->gateway &&
+                                       a->gateway->ip_equals(a->gateway, b->gateway));
+       }
+       return FALSE;
+}
+
+typedef struct net_change_t net_change_t;
+
+/**
+ * Queued network changes
+ */
+struct net_change_t {
+       /** Name of the interface that got activated (or an IP appeared on) */
+       char *if_name;
+};
+
+/**
+ * Destroy a net_change_t object
+ */
+static void net_change_destroy(net_change_t *this)
+{
+       free(this->if_name);
+       free(this);
+}
+
+/**
+ * Hash a net_change_t object
+ */
+static u_int net_change_hash(net_change_t *this)
+{
+       return chunk_hash(chunk_create(this->if_name, strlen(this->if_name)));
+}
+
+/**
+ * Compare two net_change_t objects
+ */
+static bool net_change_equals(net_change_t *a, net_change_t *b)
+{
+       return streq(a->if_name, b->if_name);
+}
+
 typedef struct private_kernel_pfroute_net_t private_kernel_pfroute_net_t;
 
 /**
@@ -219,6 +326,31 @@ struct private_kernel_pfroute_net_t
        condvar_t *condvar;
 
        /**
+        * installed routes
+        */
+       hashtable_t *routes;
+
+       /**
+        * mutex for routes
+        */
+       mutex_t *routes_lock;
+
+       /**
+        * interface changes which may trigger route reinstallation
+        */
+       hashtable_t *net_changes;
+
+       /**
+        * mutex for route reinstallation triggers
+        */
+       mutex_t *net_changes_lock;
+
+       /**
+        * time of last route reinstallation
+        */
+       timeval_t last_route_reinstall;
+
+       /**
         * pid to send PF_ROUTE messages with
         */
        pid_t pid;
@@ -254,6 +386,101 @@ struct private_kernel_pfroute_net_t
        int vip_wait;
 };
 
+
+/**
+ * Forward declaration
+ */
+static status_t manage_route(private_kernel_pfroute_net_t *this, int op,
+                                                        chunk_t dst_net, u_int8_t prefixlen,
+                                                        host_t *gateway, char *if_name);
+
+/**
+ * Clear the queued network changes.
+ */
+static void net_changes_clear(private_kernel_pfroute_net_t *this)
+{
+       enumerator_t *enumerator;
+       net_change_t *change;
+
+       enumerator = this->net_changes->create_enumerator(this->net_changes);
+       while (enumerator->enumerate(enumerator, NULL, (void**)&change))
+       {
+               this->net_changes->remove_at(this->net_changes, enumerator);
+               net_change_destroy(change);
+       }
+       enumerator->destroy(enumerator);
+}
+
+/**
+ * Act upon queued network changes.
+ */
+static job_requeue_t reinstall_routes(private_kernel_pfroute_net_t *this)
+{
+       enumerator_t *enumerator;
+       route_entry_t *route;
+
+       this->net_changes_lock->lock(this->net_changes_lock);
+       this->routes_lock->lock(this->routes_lock);
+
+       enumerator = this->routes->create_enumerator(this->routes);
+       while (enumerator->enumerate(enumerator, NULL, (void**)&route))
+       {
+               net_change_t *change, lookup = {
+                       .if_name = route->if_name,
+               };
+               /* check if a change for the outgoing interface is queued */
+               change = this->net_changes->get(this->net_changes, &lookup);
+               if (change)
+               {
+                       manage_route(this, RTM_ADD, route->dst_net, route->prefixlen,
+                                                route->gateway, route->if_name);
+               }
+       }
+       enumerator->destroy(enumerator);
+       this->routes_lock->unlock(this->routes_lock);
+
+       net_changes_clear(this);
+       this->net_changes_lock->unlock(this->net_changes_lock);
+       return JOB_REQUEUE_NONE;
+}
+
+/**
+ * Queue route reinstallation caused by network changes for a given interface.
+ *
+ * The route reinstallation is delayed for a while and only done once for
+ * several calls during this delay, in order to avoid doing it too often.
+ * The interface name is freed.
+ */
+static void queue_route_reinstall(private_kernel_pfroute_net_t *this,
+                                                                 char *if_name)
+{
+       net_change_t *update, *found;
+       timeval_t now;
+       job_t *job;
+
+       INIT(update,
+               .if_name = if_name
+       );
+
+       this->net_changes_lock->lock(this->net_changes_lock);
+       found = this->net_changes->put(this->net_changes, update, update);
+       if (found)
+       {
+               net_change_destroy(found);
+       }
+       time_monotonic(&now);
+       if (timercmp(&now, &this->last_route_reinstall, >))
+       {
+               timeval_add_ms(&now, ROUTE_DELAY);
+               this->last_route_reinstall = now;
+
+               job = (job_t*)callback_job_create((callback_job_cb_t)reinstall_routes,
+                                                                                 this, NULL, NULL);
+               lib->scheduler->schedule_job_ms(lib->scheduler, job, ROUTE_DELAY);
+       }
+       this->net_changes_lock->unlock(this->net_changes_lock);
+}
+
 /**
  * Add an address map entry
  */
@@ -401,6 +628,7 @@ static void process_addr(private_kernel_pfroute_net_t *this,
        addr_entry_t *addr;
        bool found = FALSE, changed = FALSE, roam = FALSE;
        enumerator_t *enumerator;
+       char *ifname = NULL;
        int type;
 
        enumerator = create_rtmsg_enumerator(ifa, sizeof(*ifa));
@@ -453,6 +681,7 @@ static void process_addr(private_kernel_pfroute_net_t *this,
                                        .ip = host->clone(host),
                                );
                                changed = TRUE;
+                               ifname = strdup(iface->ifname);
                                iface->addrs->insert_last(iface->addrs, addr);
                                addr_map_entry_add(this, addr, iface);
                                if (iface->usable)
@@ -472,6 +701,15 @@ static void process_addr(private_kernel_pfroute_net_t *this,
        this->lock->unlock(this->lock);
        host->destroy(host);
 
+       if (roam && ifname)
+       {
+               queue_route_reinstall(this, ifname);
+       }
+       else
+       {
+               free(ifname);
+       }
+
        if (roam)
        {
                fire_roam_event(this, TRUE);
@@ -526,7 +764,7 @@ static void process_link(private_kernel_pfroute_net_t *this,
 {
        enumerator_t *enumerator;
        iface_entry_t *iface;
-       bool roam = FALSE, found = FALSE;
+       bool roam = FALSE, found = FALSE, update_routes = FALSE;
 
        this->lock->write_lock(this->lock);
        enumerator = this->ifaces->create_enumerator(this->ifaces);
@@ -538,7 +776,7 @@ static void process_link(private_kernel_pfroute_net_t *this,
                        {
                                if (!(iface->flags & IFF_UP) && (msg->ifm_flags & IFF_UP))
                                {
-                                       roam = TRUE;
+                                       roam = update_routes = TRUE;
                                        DBG1(DBG_KNL, "interface %s activated", iface->ifname);
                                }
                                else if ((iface->flags & IFF_UP) && !(msg->ifm_flags & IFF_UP))
@@ -571,7 +809,7 @@ static void process_link(private_kernel_pfroute_net_t *this,
                        this->ifaces->insert_last(this->ifaces, iface);
                        if (iface->usable)
                        {
-                               roam = TRUE;
+                               roam = update_routes = TRUE;
                        }
                }
                else
@@ -581,6 +819,11 @@ static void process_link(private_kernel_pfroute_net_t *this,
        }
        this->lock->unlock(this->lock);
 
+       if (update_routes)
+       {
+               queue_route_reinstall(this, strdup(iface->ifname));
+       }
+
        if (roam)
        {
                fire_roam_event(this, TRUE);
@@ -890,11 +1133,16 @@ METHOD(kernel_net_t, add_ip, status_t,
                                }
                        }
                        addrs->destroy(addrs);
+                       /* during IKEv1 reauthentication, children get moved from
+                        * old the new SA before the virtual IP is available. This
+                        * kills the route for our virtual IP, reinstall. */
+                       queue_route_reinstall(this, strdup(iface->ifname));
+                       break;
                }
        }
        ifaces->destroy(ifaces);
        /* lets do this while holding the lock, thus preventing another thread
-        * from deleting the TUN device concurrently, hopefully listeneres are quick
+        * from deleting the TUN device concurrently, hopefully listeners are quick
         * and cause no deadlocks */
        hydra->kernel_interface->tun(hydra->kernel_interface, tun, TRUE);
        this->lock->unlock(this->lock);
@@ -1098,14 +1346,53 @@ METHOD(kernel_net_t, add_route, status_t,
        private_kernel_pfroute_net_t *this, chunk_t dst_net, u_int8_t prefixlen,
        host_t *gateway, host_t *src_ip, char *if_name)
 {
-       return manage_route(this, RTM_ADD, dst_net, prefixlen, gateway, if_name);
+       status_t status;
+       route_entry_t *found, route = {
+               .dst_net = dst_net,
+               .prefixlen = prefixlen,
+               .gateway = gateway,
+               .if_name = if_name,
+       };
+
+       this->routes_lock->lock(this->routes_lock);
+       found = this->routes->get(this->routes, &route);
+       if (found)
+       {
+               this->routes_lock->unlock(this->routes_lock);
+               return ALREADY_DONE;
+       }
+       found = route_entry_clone(&route);
+       this->routes->put(this->routes, found, found);
+       status = manage_route(this, RTM_ADD, dst_net, prefixlen, gateway, if_name);
+       this->routes_lock->unlock(this->routes_lock);
+       return status;
 }
 
 METHOD(kernel_net_t, del_route, status_t,
        private_kernel_pfroute_net_t *this, chunk_t dst_net, u_int8_t prefixlen,
        host_t *gateway, host_t *src_ip, char *if_name)
 {
-       return manage_route(this, RTM_DELETE, dst_net, prefixlen, gateway, if_name);
+       status_t status;
+       route_entry_t *found, route = {
+               .dst_net = dst_net,
+               .prefixlen = prefixlen,
+               .gateway = gateway,
+               .if_name = if_name,
+       };
+
+       this->routes_lock->lock(this->routes_lock);
+       found = this->routes->get(this->routes, &route);
+       if (!found)
+       {
+               this->routes_lock->unlock(this->routes_lock);
+               return NOT_FOUND;
+       }
+       this->routes->remove(this->routes, found);
+       route_entry_destroy(found);
+       status = manage_route(this, RTM_DELETE, dst_net, prefixlen, gateway,
+                                                 if_name);
+       this->routes_lock->unlock(this->routes_lock);
+       return status;
 }
 
 /**
@@ -1340,12 +1627,29 @@ METHOD(kernel_net_t, destroy, void,
        private_kernel_pfroute_net_t *this)
 {
        enumerator_t *enumerator;
+       route_entry_t *route;
        addr_entry_t *addr;
 
+       enumerator = this->routes->create_enumerator(this->routes);
+       while (enumerator->enumerate(enumerator, NULL, (void**)&route))
+       {
+               manage_route(this, RTM_DELETE, route->dst_net, route->prefixlen,
+                                        route->gateway, route->if_name);
+               route_entry_destroy(route);
+       }
+       enumerator->destroy(enumerator);
+       this->routes->destroy(this->routes);
+       this->routes_lock->destroy(this->routes_lock);
+
        if (this->socket != -1)
        {
                close(this->socket);
        }
+
+       net_changes_clear(this);
+       this->net_changes->destroy(this->net_changes);
+       this->net_changes_lock->destroy(this->net_changes_lock);
+
        enumerator = this->addrs->create_enumerator(this->addrs);
        while (enumerator->enumerate(enumerator, NULL, (void**)&addr))
        {
@@ -1389,13 +1693,22 @@ kernel_pfroute_net_t *kernel_pfroute_net_create()
                .addrs = hashtable_create(
                                                                (hashtable_hash_t)addr_map_entry_hash,
                                                                (hashtable_equals_t)addr_map_entry_equals, 16),
+               .routes = hashtable_create((hashtable_hash_t)route_entry_hash,
+                                                                  (hashtable_equals_t)route_entry_equals, 16),
+               .net_changes = hashtable_create(
+                                                                  (hashtable_hash_t)net_change_hash,
+                                                                  (hashtable_equals_t)net_change_equals, 16),
                .tuns = linked_list_create(),
                .lock = rwlock_create(RWLOCK_TYPE_DEFAULT),
                .mutex = mutex_create(MUTEX_TYPE_DEFAULT),
                .condvar = condvar_create(CONDVAR_TYPE_DEFAULT),
+               .routes_lock = mutex_create(MUTEX_TYPE_DEFAULT),
+               .net_changes_lock = mutex_create(MUTEX_TYPE_DEFAULT),
                .vip_wait = lib->settings->get_int(lib->settings,
                                        "%s.plugins.kernel-pfroute.vip_wait", 1000, hydra->daemon),
        );
+       timerclear(&this->last_route_reinstall);
+       timerclear(&this->last_roam);
 
        /* create a PF_ROUTE socket to communicate with the kernel */
        this->socket = socket(PF_ROUTE, SOCK_RAW, AF_UNSPEC);