connmark: Add CONNMARK rules to select correct output SA based on conntrack
authorMartin Willi <martin@revosec.ch>
Fri, 14 Nov 2014 11:57:53 +0000 (12:57 +0100)
committerMartin Willi <martin@revosec.ch>
Fri, 20 Feb 2015 15:34:53 +0000 (16:34 +0100)
Currently supports transport mode connections using IPv4 only, and requires
a unique mark configured on the connection.

To select the correct outbound SA when multiple connections match (i.e.
multiple peers connected from the same IP address / NAT router) marks must be
configured. This mark should usually be unique, which can be configured in
ipsec.conf using mark=0xffffffff.

The plugin inserts CONNMARK netfilter target rules: Any peer-initiated flow
is tagged with the assigned mark as connmark. On the return path, the mark
gets restored from the conntrack entry to select the correct outbound SA.

src/libcharon/plugins/connmark/Makefile.am
src/libcharon/plugins/connmark/connmark_listener.c [new file with mode: 0644]
src/libcharon/plugins/connmark/connmark_listener.h [new file with mode: 0644]
src/libcharon/plugins/connmark/connmark_plugin.c

index d7e1f68..c70f529 100644 (file)
@@ -13,6 +13,8 @@ plugin_LTLIBRARIES = libstrongswan-connmark.la
 endif
 
 libstrongswan_connmark_la_SOURCES = \
+       connmark_listener.h connmark_listener.c \
        connmark_plugin.h connmark_plugin.c
 
 libstrongswan_connmark_la_LDFLAGS = -module -avoid-version
+libstrongswan_connmark_la_LIBADD = -lip4tc
diff --git a/src/libcharon/plugins/connmark/connmark_listener.c b/src/libcharon/plugins/connmark/connmark_listener.c
new file mode 100644 (file)
index 0000000..23df690
--- /dev/null
@@ -0,0 +1,538 @@
+/*
+ * Copyright (C) 2014 Martin Willi
+ * Copyright (C) 2014 revosec AG
+ *
+ * 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.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * 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.
+ */
+
+#include "connmark_listener.h"
+
+#include <daemon.h>
+
+#include <errno.h>
+#include <libiptc/libiptc.h>
+#include <linux/netfilter/xt_esp.h>
+#include <linux/netfilter/xt_tcpudp.h>
+#include <linux/netfilter/xt_MARK.h>
+#include <linux/netfilter/xt_policy.h>
+#include <linux/netfilter/xt_CONNMARK.h>
+
+
+typedef struct private_connmark_listener_t private_connmark_listener_t;
+
+/**
+ * Private data of an connmark_listener_t object.
+ */
+struct private_connmark_listener_t {
+
+       /**
+        * Public connmark_listener_t interface.
+        */
+       connmark_listener_t public;
+};
+
+/**
+ * Convert an (IPv4) traffic selector to an address and mask
+ */
+static bool ts2in(traffic_selector_t *ts,
+                                 struct in_addr *addr, struct in_addr *mask)
+{
+       u_int8_t bits;
+       host_t *net;
+
+       if (ts->get_type(ts) == TS_IPV4_ADDR_RANGE &&
+               ts->to_subnet(ts, &net, &bits))
+       {
+               memcpy(&addr->s_addr, net->get_address(net).ptr, 4);
+               net->destroy(net);
+               mask->s_addr = htonl(0xffffffffU << (32 - bits));
+               return TRUE;
+       }
+       return FALSE;
+}
+
+/**
+ * Convert an (IPv4) host to an address with mask
+ */
+static bool host2in(host_t *host, struct in_addr *addr, struct in_addr *mask)
+{
+       if (host->get_family(host) == AF_INET)
+       {
+               memcpy(&addr->s_addr, host->get_address(host).ptr, 4);
+               mask->s_addr = ~0;
+               return TRUE;
+       }
+       return FALSE;
+}
+
+/**
+ * Add or remove a rule to/from the specified chain
+ */
+static bool manage_rule(struct iptc_handle *ipth, const char *chain,
+                                               bool add, struct ipt_entry *e)
+{
+       if (add)
+       {
+               if (!iptc_insert_entry(chain, e, 0, ipth))
+               {
+                       DBG1(DBG_CFG, "appending %s rule failed: %s",
+                                chain, iptc_strerror(errno));
+                       return FALSE;
+               }
+       }
+       else
+       {
+               if (!iptc_delete_entry(chain, e, "", ipth))
+               {
+                       DBG1(DBG_CFG, "deleting %s rule failed: %s",
+                                chain, iptc_strerror(errno));
+                       return FALSE;
+               }
+       }
+       return TRUE;
+}
+
+/**
+ * Add rule marking UDP-encapsulated ESP packets to match the correct policy
+ */
+static bool manage_pre_esp_in_udp(private_connmark_listener_t *this,
+                                                                 struct iptc_handle *ipth, bool add,
+                                                                 u_int mark, u_int32_t spi,
+                                                                 host_t *dst, host_t *src)
+{
+       struct {
+               struct ipt_entry e;
+               struct ipt_entry_match m;
+               struct xt_udp udp;
+               struct ipt_entry_target t;
+               struct xt_mark_tginfo2 tm;
+       } ipt = {
+               .e  = {
+                       .target_offset = XT_ALIGN(sizeof(ipt.e) + sizeof(ipt.m) +
+                                                                         sizeof(ipt.udp)),
+                       .next_offset = sizeof(ipt),
+                       .ip = {
+                               .proto = IPPROTO_UDP,
+                       },
+               },
+               .m = {
+                       .u = {
+                               .user = {
+                                       .match_size = XT_ALIGN(sizeof(ipt.m) + sizeof(ipt.udp)),
+                                       .name = "udp",
+                               },
+                       },
+               },
+               .udp = {
+                       .spts = { src->get_port(src), src->get_port(src) },
+                       .dpts = { dst->get_port(dst), dst->get_port(dst) },
+               },
+               .t = {
+                       .u = {
+                               .user = {
+                                       .target_size = XT_ALIGN(sizeof(ipt.t) + sizeof(ipt.tm)),
+                                       .name = "MARK",
+                                       .revision = 2,
+                               },
+                       },
+               },
+               .tm = {
+                       .mark = mark,
+                       .mask = ~0,
+               },
+       };
+
+       if (!host2in(dst, &ipt.e.ip.dst, &ipt.e.ip.dmsk) ||
+               !host2in(src, &ipt.e.ip.src, &ipt.e.ip.smsk))
+       {
+               return FALSE;
+       }
+       return manage_rule(ipth, "PREROUTING", add, &ipt.e);
+}
+
+/**
+ * Add rule marking non-encapsulated ESP packets to match the correct policy
+ */
+static bool manage_pre_esp(private_connmark_listener_t *this,
+                                                  struct iptc_handle *ipth, bool add,
+                                                  u_int mark, u_int32_t spi,
+                                                  host_t *dst, host_t *src)
+{
+       struct {
+               struct ipt_entry e;
+               struct ipt_entry_match m;
+               struct xt_esp esp;
+               struct ipt_entry_target t;
+               struct xt_mark_tginfo2 tm;
+       } ipt = {
+               .e  = {
+                       .target_offset = XT_ALIGN(sizeof(ipt.e) + sizeof(ipt.m) +
+                                                                         sizeof(ipt.esp)),
+                       .next_offset = sizeof(ipt),
+                       .ip = {
+                               .proto = IPPROTO_ESP,
+                       },
+               },
+               .m = {
+                       .u = {
+                               .user = {
+                                       .match_size = XT_ALIGN(sizeof(ipt.m) + sizeof(ipt.esp)),
+                                       .name = "esp",
+                               },
+                       },
+               },
+               .esp = {
+                       .spis = { htonl(spi), htonl(spi) },
+               },
+               .t = {
+                       .u = {
+                               .user = {
+                                       .target_size = XT_ALIGN(sizeof(ipt.t) + sizeof(ipt.tm)),
+                                       .name = "MARK",
+                                       .revision = 2,
+                               },
+                       },
+               },
+               .tm = {
+                       .mark = mark,
+                       .mask = ~0,
+               },
+       };
+
+       if (!host2in(dst, &ipt.e.ip.dst, &ipt.e.ip.dmsk) ||
+               !host2in(src, &ipt.e.ip.src, &ipt.e.ip.smsk))
+       {
+               return FALSE;
+       }
+       return manage_rule(ipth, "PREROUTING", add, &ipt.e);
+}
+
+/**
+ * Add rule marking ESP packets to match the correct policy
+ */
+static bool manage_pre(private_connmark_listener_t *this,
+                                          struct iptc_handle *ipth, bool add,
+                                          u_int mark, u_int32_t spi, bool encap,
+                                          host_t *dst, host_t *src)
+{
+       if (encap)
+       {
+               return manage_pre_esp_in_udp(this, ipth, add, mark, spi, dst, src);
+       }
+       return manage_pre_esp(this, ipth, add, mark, spi, dst, src);
+}
+
+/**
+ * Add inbound rule applying CONNMARK to matching traffic
+ */
+static bool manage_in(private_connmark_listener_t *this,
+                                         struct iptc_handle *ipth, bool add,
+                                         u_int mark, u_int32_t spi,
+                                         traffic_selector_t *dst, traffic_selector_t *src)
+{
+       struct {
+               struct ipt_entry e;
+               struct ipt_entry_match m;
+               struct xt_policy_info p;
+               struct ipt_entry_target t;
+               struct xt_connmark_tginfo1 cm;
+       } ipt = {
+               .e  = {
+                       .target_offset = XT_ALIGN(sizeof(ipt.e) + sizeof(ipt.m) +
+                                                                         sizeof(ipt.p)),
+                       .next_offset = sizeof(ipt),
+               },
+               .m = {
+                       .u = {
+                               .user = {
+                                       .match_size = XT_ALIGN(sizeof(ipt.m) + sizeof(ipt.p)),
+                                       .name = "policy",
+                               },
+                       },
+               },
+               .p = {
+                       .pol = {
+                               {
+                                       .spi = spi,
+                                       .match.spi = 1,
+                               },
+                       },
+                       .len = 1,
+                       .flags = XT_POLICY_MATCH_IN,
+               },
+               .t = {
+                       .u = {
+                               .user = {
+                                       .target_size = XT_ALIGN(sizeof(ipt.t) + sizeof(ipt.cm)),
+                                       .name = "CONNMARK",
+                                       .revision = 1,
+                               },
+                       },
+               },
+               .cm = {
+                       .ctmark = mark,
+                       .ctmask = ~0,
+                       .nfmask = ~0,
+                       .mode = XT_CONNMARK_SET,
+               },
+       };
+
+       if (!ts2in(dst, &ipt.e.ip.dst, &ipt.e.ip.dmsk) ||
+               !ts2in(src, &ipt.e.ip.src, &ipt.e.ip.smsk))
+       {
+               return FALSE;
+       }
+       return manage_rule(ipth, "INPUT", add, &ipt.e);
+}
+
+/**
+ * Add outbund rule restoring CONNMARK on matching traffic
+ */
+static bool manage_out(private_connmark_listener_t *this,
+                                          struct iptc_handle *ipth, bool add,
+                                          traffic_selector_t *dst, traffic_selector_t *src)
+{
+       struct {
+               struct ipt_entry e;
+               struct ipt_entry_target t;
+               struct xt_connmark_tginfo1 cm;
+       } ipt = {
+               .e  = {
+                       .target_offset = XT_ALIGN(sizeof(ipt.e)),
+                       .next_offset = sizeof(ipt),
+               },
+               .t = {
+                       .u = {
+                               .user = {
+                                       .target_size = XT_ALIGN(sizeof(ipt.t) + sizeof(ipt.cm)),
+                                       .name = "CONNMARK",
+                                       .revision = 1,
+                               },
+                       },
+               },
+               .cm = {
+                       .ctmask = ~0,
+                       .nfmask = ~0,
+                       .mode = XT_CONNMARK_RESTORE,
+               },
+       };
+
+       if (!ts2in(dst, &ipt.e.ip.dst, &ipt.e.ip.dmsk) ||
+               !ts2in(src, &ipt.e.ip.src, &ipt.e.ip.smsk))
+       {
+               return FALSE;
+       }
+       return manage_rule(ipth, "OUTPUT", add, &ipt.e);
+}
+
+/**
+ * Initialize iptables handle, log error
+ */
+static struct iptc_handle* init_handle()
+{
+       struct iptc_handle *ipth;
+
+       ipth = iptc_init("mangle");
+       if (ipth)
+       {
+               return ipth;
+       }
+       DBG1(DBG_CFG, "initializing iptables failed: %s", iptc_strerror(errno));
+       return NULL;
+}
+
+/**
+ * Commit iptables rules, log error
+ */
+static bool commit_handle(struct iptc_handle *ipth)
+{
+       if (iptc_commit(ipth))
+       {
+               return TRUE;
+       }
+       DBG1(DBG_CFG, "forecast iptables commit failed: %s", iptc_strerror(errno));
+       return FALSE;
+}
+
+/**
+ * Add/Remove policies for a CHILD_SA using a iptables handle
+ */
+static bool manage_policies(private_connmark_listener_t *this,
+                                               struct iptc_handle *ipth, host_t *dst, host_t *src,
+                                               bool encap, child_sa_t *child_sa, bool add)
+{
+       traffic_selector_t *local, *remote;
+       enumerator_t *enumerator;
+       u_int32_t spi;
+       u_int mark;
+       bool done = TRUE;
+
+       spi = child_sa->get_spi(child_sa, TRUE);
+       mark = child_sa->get_mark(child_sa, TRUE).value;
+
+       enumerator = child_sa->create_policy_enumerator(child_sa);
+       while (enumerator->enumerate(enumerator, &local, &remote))
+       {
+               if (!manage_pre(this, ipth, add, mark, spi, encap, dst, src) ||
+                       !manage_in(this, ipth, add, mark, spi, local, remote) ||
+                       !manage_out(this, ipth, add, remote, local))
+               {
+                       done = FALSE;
+                       break;
+               }
+       }
+       enumerator->destroy(enumerator);
+
+       return done;
+}
+
+/**
+ * Check if rules should be installed for given CHILD_SA
+ */
+static bool handle_sa(child_sa_t *child_sa)
+{
+       return child_sa->get_mark(child_sa, TRUE).value &&
+                  child_sa->get_mark(child_sa, FALSE).value &&
+                  child_sa->get_mode(child_sa) == MODE_TRANSPORT &&
+                  child_sa->get_protocol(child_sa) == PROTO_ESP;
+}
+
+METHOD(listener_t, child_updown, bool,
+       private_connmark_listener_t *this, ike_sa_t *ike_sa, child_sa_t *child_sa,
+       bool up)
+{
+       struct iptc_handle *ipth;
+       host_t *dst, *src;
+       bool encap;
+
+       dst = ike_sa->get_my_host(ike_sa);
+       src = ike_sa->get_other_host(ike_sa);
+       encap = child_sa->has_encap(child_sa);
+
+       if (handle_sa(child_sa))
+       {
+               ipth = init_handle();
+               if (ipth)
+               {
+                       if (manage_policies(this, ipth, dst, src, encap, child_sa, up))
+                       {
+                               commit_handle(ipth);
+                       }
+                       iptc_free(ipth);
+               }
+       }
+       return TRUE;
+}
+
+METHOD(listener_t, child_rekey, bool,
+       private_connmark_listener_t *this, ike_sa_t *ike_sa,
+       child_sa_t *old, child_sa_t *new)
+{
+       struct iptc_handle *ipth;
+       host_t *dst, *src;
+       bool oldencap, newencap;
+
+       dst = ike_sa->get_my_host(ike_sa);
+       src = ike_sa->get_other_host(ike_sa);
+       oldencap = old->has_encap(old);
+       newencap = new->has_encap(new);
+
+       if (handle_sa(old))
+       {
+               ipth = init_handle();
+               if (ipth)
+               {
+                       if (manage_policies(this, ipth, dst, src, oldencap, old, FALSE) &&
+                               manage_policies(this, ipth, dst, src, newencap, new, TRUE))
+                       {
+                               commit_handle(ipth);
+                       }
+                       iptc_free(ipth);
+               }
+       }
+       return TRUE;
+}
+
+METHOD(listener_t, ike_update, bool,
+       private_connmark_listener_t *this, ike_sa_t *ike_sa,
+       bool local, host_t *new)
+{
+       struct iptc_handle *ipth;
+       enumerator_t *enumerator;
+       child_sa_t *child_sa;
+       host_t *dst, *src;
+       bool oldencap, newencap;
+
+       if (local)
+       {
+               dst = new;
+               src = ike_sa->get_other_host(ike_sa);
+       }
+       else
+       {
+               dst = ike_sa->get_my_host(ike_sa);
+               src = new;
+       }
+       /* during ike_update(), has_encap() on the CHILD_SA has not yet been
+        * updated, but shows the old state. */
+       newencap = ike_sa->has_condition(ike_sa, COND_NAT_ANY);
+
+       enumerator = ike_sa->create_child_sa_enumerator(ike_sa);
+       while (enumerator->enumerate(enumerator, &child_sa))
+       {
+               if (handle_sa(child_sa))
+               {
+                       oldencap = child_sa->has_encap(child_sa);
+                       ipth = init_handle();
+                       if (ipth)
+                       {
+                               if (manage_policies(this, ipth, dst, src, oldencap,
+                                                                       child_sa, FALSE) &&
+                                       manage_policies(this, ipth, dst, src, newencap,
+                                                                       child_sa, TRUE))
+                               {
+                                       commit_handle(ipth);
+                               }
+                               iptc_free(ipth);
+                       }
+               }
+       }
+       enumerator->destroy(enumerator);
+
+       return TRUE;
+}
+
+METHOD(connmark_listener_t, destroy, void,
+       private_connmark_listener_t *this)
+{
+       free(this);
+}
+
+/**
+ * See header
+ */
+connmark_listener_t *connmark_listener_create()
+{
+       private_connmark_listener_t *this;
+
+       INIT(this,
+               .public = {
+                       .listener = {
+                               .ike_update = _ike_update,
+                               .child_updown = _child_updown,
+                               .child_rekey = _child_rekey,
+                       },
+                       .destroy = _destroy,
+               },
+       );
+
+       return &this->public;
+}
diff --git a/src/libcharon/plugins/connmark/connmark_listener.h b/src/libcharon/plugins/connmark/connmark_listener.h
new file mode 100644 (file)
index 0000000..2d4098f
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2014 Martin Willi
+ * Copyright (C) 2014 revosec AG
+ *
+ * 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.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * 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.
+ */
+
+/**
+ * @defgroup connmark_listener connmark_listener
+ * @{ @ingroup connmark
+ */
+
+#ifndef CONNMARK_LISTENER_H_
+#define CONNMARK_LISTENER_H_
+
+#include <bus/listeners/listener.h>
+
+typedef struct connmark_listener_t connmark_listener_t;
+
+/**
+ * Listener to install Netfilter rules
+ */
+struct connmark_listener_t {
+
+       /**
+        * Implements listener_t interface.
+        */
+       listener_t listener;
+
+       /**
+        * Destroy a connmark_listener_t.
+        */
+       void (*destroy)(connmark_listener_t *this);
+};
+
+/**
+ * Create a connmark_listener instance.
+ */
+connmark_listener_t *connmark_listener_create();
+
+#endif /** CONNMARK_LISTENER_H_ @}*/
index 270a631..3f276f9 100644 (file)
@@ -14,6 +14,7 @@
  */
 
 #include "connmark_plugin.h"
+#include "connmark_listener.h"
 
 #include <daemon.h>
 
@@ -28,6 +29,11 @@ struct private_connmark_plugin_t {
         * implements plugin interface
         */
        connmark_plugin_t public;
+
+       /**
+        * Listener installing netfilter rules
+        */
+       connmark_listener_t *listener;
 };
 
 METHOD(plugin_t, get_name, char*,
@@ -42,6 +48,14 @@ METHOD(plugin_t, get_name, char*,
 static bool plugin_cb(private_connmark_plugin_t *this,
                                          plugin_feature_t *feature, bool reg, void *cb_data)
 {
+       if (reg)
+       {
+               charon->bus->add_listener(charon->bus, &this->listener->listener);
+       }
+       else
+       {
+               charon->bus->remove_listener(charon->bus, &this->listener->listener);
+       }
        return TRUE;
 }
 
@@ -59,6 +73,7 @@ METHOD(plugin_t, get_features, int,
 METHOD(plugin_t, destroy, void,
        private_connmark_plugin_t *this)
 {
+       this->listener->destroy(this->listener);
        free(this);
 }
 
@@ -69,6 +84,12 @@ plugin_t *connmark_plugin_create()
 {
        private_connmark_plugin_t *this;
 
+       if (!lib->caps->keep(lib->caps, CAP_NET_ADMIN))
+       {
+               DBG1(DBG_NET, "connmark plugin requires CAP_NET_ADMIN capability");
+               return NULL;
+       }
+
        INIT(this,
                .public = {
                        .plugin = {
@@ -77,6 +98,7 @@ plugin_t *connmark_plugin_create()
                                .destroy = _destroy,
                        },
                },
+               .listener = connmark_listener_create(),
        );
 
        return &this->public.plugin;