diff --git a/NEWS b/NEWS index 976f34c18..9a21f84e9 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,12 @@ as any previous strongSwan release) it must be explicitly enabled using the charon.make_before_break strongswan.conf option. +- The new connmark plugin allows a host to bind conntrack flows to a specific + CHILD_SA by applying and restoring the SA mark to conntrack entries. This + allows a peer to handle multiple transport mode connections coming over the + same NAT device for client-initiated flows. A common use case is to protect + L2TP/IPsec, as supported by some systems. + strongswan-5.2.2 ---------------- diff --git a/configure.ac b/configure.ac index 912a90801..11716d886 100644 --- a/configure.ac +++ b/configure.ac @@ -246,6 +246,7 @@ ARG_ENABL_SET([tnccs-dynamic], [enable dynamic TNCCS protocol discovery module. # misc plugins ARG_ENABL_SET([android-log], [enable Android specific logger plugin.]) ARG_ENABL_SET([certexpire], [enable CSV export of expiration dates of used certificates.]) +ARG_ENABL_SET([connmark], [enable connmark plugin using conntrack based marks to select return path SA.]) ARG_ENABL_SET([duplicheck], [advanced duplicate checking plugin using liveness checks.]) ARG_ENABL_SET([error-notify], [enable error notification plugin.]) ARG_ENABL_SET([farp], [enable ARP faking plugin that responds to ARP requests to peers virtual IP]) @@ -1268,6 +1269,7 @@ ADD_PLUGIN([resolve], [c charon cmd]) ADD_PLUGIN([socket-default], [c charon nm cmd]) ADD_PLUGIN([socket-dynamic], [c charon cmd]) ADD_PLUGIN([socket-win], [c charon]) +ADD_PLUGIN([connmark], [c charon]) ADD_PLUGIN([farp], [c charon]) ADD_PLUGIN([stroke], [c charon]) ADD_PLUGIN([vici], [c charon]) @@ -1475,6 +1477,7 @@ AM_CONDITIONAL(USE_IMV_SWID, test x$imv_swid = xtrue) AM_CONDITIONAL(USE_SOCKET_DEFAULT, test x$socket_default = xtrue) AM_CONDITIONAL(USE_SOCKET_DYNAMIC, test x$socket_dynamic = xtrue) AM_CONDITIONAL(USE_SOCKET_WIN, test x$socket_win = xtrue) +AM_CONDITIONAL(USE_CONNMARK, test x$connmark = xtrue) AM_CONDITIONAL(USE_FARP, test x$farp = xtrue) AM_CONDITIONAL(USE_ADDRBLOCK, test x$addrblock = xtrue) AM_CONDITIONAL(USE_UNITY, test x$unity = xtrue) @@ -1709,6 +1712,7 @@ AC_CONFIG_FILES([ src/libcharon/plugins/socket_default/Makefile src/libcharon/plugins/socket_dynamic/Makefile src/libcharon/plugins/socket_win/Makefile + src/libcharon/plugins/connmark/Makefile src/libcharon/plugins/farp/Makefile src/libcharon/plugins/smp/Makefile src/libcharon/plugins/sql/Makefile diff --git a/man/ipsec.conf.5.in b/man/ipsec.conf.5.in index f84e3313e..851bd1750 100644 --- a/man/ipsec.conf.5.in +++ b/man/ipsec.conf.5.in @@ -987,7 +987,9 @@ sets an XFRM mark in the inbound and outbound IPsec SAs and policies. If the mask is missing then a default mask of .B 0xffffffff -is assumed. +is assumed. The special value +.B %unique +assigns a unique value to each newly created IPsec SA. .TP .BR mark_in " = [/]" sets an XFRM mark in the inbound IPsec SA and diff --git a/scripts/test.sh b/scripts/test.sh index 685320714..ba3c1266f 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -50,7 +50,7 @@ all) DEPS="$DEPS libcurl4-gnutls-dev libsoup2.4-dev libunbound-dev libldns-dev libmysqlclient-dev libsqlite3-dev clearsilver-dev libfcgi-dev libnm-glib-dev libnm-glib-vpn-dev libpcsclite-dev libpam0g-dev - binutils-dev libunwind7-dev libjson0-dev" + binutils-dev libunwind7-dev libjson0-dev iptables-dev" ;; win*) CONFIG="--disable-defaults --enable-svc --enable-ikev2 diff --git a/src/libcharon/Makefile.am b/src/libcharon/Makefile.am index e7b7c6854..2ce463533 100644 --- a/src/libcharon/Makefile.am +++ b/src/libcharon/Makefile.am @@ -209,6 +209,13 @@ if MONOLITHIC endif endif +if USE_CONNMARK + SUBDIRS += plugins/connmark +if MONOLITHIC + libcharon_la_LIBADD += plugins/connmark/libstrongswan-connmark.la +endif +endif + if USE_FARP SUBDIRS += plugins/farp if MONOLITHIC diff --git a/src/libcharon/bus/bus.c b/src/libcharon/bus/bus.c index cb59f976b..7938f46cc 100644 --- a/src/libcharon/bus/bus.c +++ b/src/libcharon/bus/bus.c @@ -755,6 +755,33 @@ METHOD(bus_t, ike_rekey, void, this->mutex->unlock(this->mutex); } +METHOD(bus_t, ike_update, void, + private_bus_t *this, ike_sa_t *ike_sa, bool local, host_t *new) +{ + enumerator_t *enumerator; + entry_t *entry; + bool keep; + + this->mutex->lock(this->mutex); + enumerator = this->listeners->create_enumerator(this->listeners); + while (enumerator->enumerate(enumerator, &entry)) + { + if (entry->calling || !entry->listener->ike_update) + { + continue; + } + entry->calling++; + keep = entry->listener->ike_update(entry->listener, ike_sa, local, new); + entry->calling--; + if (!keep) + { + unregister_listener(this, entry, enumerator); + } + } + enumerator->destroy(enumerator); + this->mutex->unlock(this->mutex); +} + METHOD(bus_t, ike_reestablish_pre, void, private_bus_t *this, ike_sa_t *old, ike_sa_t *new) { @@ -1006,6 +1033,7 @@ bus_t *bus_create() .child_keys = _child_keys, .ike_updown = _ike_updown, .ike_rekey = _ike_rekey, + .ike_update = _ike_update, .ike_reestablish_pre = _ike_reestablish_pre, .ike_reestablish_post = _ike_reestablish_post, .child_updown = _child_updown, diff --git a/src/libcharon/bus/bus.h b/src/libcharon/bus/bus.h index e1d221ca5..051c429f9 100644 --- a/src/libcharon/bus/bus.h +++ b/src/libcharon/bus/bus.h @@ -381,6 +381,15 @@ struct bus_t { */ void (*ike_rekey)(bus_t *this, ike_sa_t *old, ike_sa_t *new); + /** + * IKE_SA peer endpoint update hook. + * + * @param ike_sa updated IKE_SA, having old endpoints set + * @param local TRUE if local endpoint gets updated, FALSE for remote + * @param new new endpoint address and port + */ + void (*ike_update)(bus_t *this, ike_sa_t *ike_sa, bool local, host_t *new); + /** * IKE_SA reestablishing hook (before resolving hosts). * diff --git a/src/libcharon/bus/listeners/listener.h b/src/libcharon/bus/listeners/listener.h index 0910cb361..3447d8f99 100644 --- a/src/libcharon/bus/listeners/listener.h +++ b/src/libcharon/bus/listeners/listener.h @@ -127,6 +127,17 @@ struct listener_t { */ bool (*ike_rekey)(listener_t *this, ike_sa_t *old, ike_sa_t *new); + /** + * Hook called for IKE_SA peer endpoint updates. + * + * @param ike_sa updated IKE_SA, having old endpoints set + * @param local TRUE if local endpoint gets updated, FALSE for remote + * @param new new endpoint address and port + * @return TRUE to stay registered, FALSE to unregister + */ + bool (*ike_update)(listener_t *this, ike_sa_t *ike_sa, + bool local, host_t *new); + /** * Hook called when an initiator reestablishes an IKE_SA. * diff --git a/src/libcharon/plugins/connmark/Makefile.am b/src/libcharon/plugins/connmark/Makefile.am new file mode 100644 index 000000000..c70f529ce --- /dev/null +++ b/src/libcharon/plugins/connmark/Makefile.am @@ -0,0 +1,20 @@ +AM_CPPFLAGS = \ + -I$(top_srcdir)/src/libstrongswan \ + -I$(top_srcdir)/src/libhydra \ + -I$(top_srcdir)/src/libcharon + +AM_CFLAGS = \ + $(PLUGIN_CFLAGS) + +if MONOLITHIC +noinst_LTLIBRARIES = libstrongswan-connmark.la +else +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 index 000000000..23df690e8 --- /dev/null +++ b/src/libcharon/plugins/connmark/connmark_listener.c @@ -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 . + * + * 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 + +#include +#include +#include +#include +#include +#include +#include + + +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 index 000000000..2d4098fb6 --- /dev/null +++ b/src/libcharon/plugins/connmark/connmark_listener.h @@ -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 . + * + * 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 + +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_ @}*/ diff --git a/src/libcharon/plugins/connmark/connmark_plugin.c b/src/libcharon/plugins/connmark/connmark_plugin.c new file mode 100644 index 000000000..3f276f93e --- /dev/null +++ b/src/libcharon/plugins/connmark/connmark_plugin.c @@ -0,0 +1,105 @@ +/* + * 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 . + * + * 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_plugin.h" +#include "connmark_listener.h" + +#include + +typedef struct private_connmark_plugin_t private_connmark_plugin_t; + +/** + * private data of connmark plugin + */ +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*, + private_connmark_plugin_t *this) +{ + return "connmark"; +} + +/** + * Register listener + */ +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; +} + +METHOD(plugin_t, get_features, int, + private_connmark_plugin_t *this, plugin_feature_t *features[]) +{ + static plugin_feature_t f[] = { + PLUGIN_CALLBACK((plugin_feature_callback_t)plugin_cb, NULL), + PLUGIN_PROVIDE(CUSTOM, "connmark"), + }; + *features = f; + return countof(f); +} + +METHOD(plugin_t, destroy, void, + private_connmark_plugin_t *this) +{ + this->listener->destroy(this->listener); + free(this); +} + +/** + * Plugin constructor + */ +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 = { + .get_name = _get_name, + .get_features = _get_features, + .destroy = _destroy, + }, + }, + .listener = connmark_listener_create(), + ); + + return &this->public.plugin; +} diff --git a/src/libcharon/plugins/connmark/connmark_plugin.h b/src/libcharon/plugins/connmark/connmark_plugin.h new file mode 100644 index 000000000..5b4ccebbe --- /dev/null +++ b/src/libcharon/plugins/connmark/connmark_plugin.h @@ -0,0 +1,42 @@ +/* + * 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 . + * + * 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 connmark + * @ingroup cplugins + * + * @defgroup connmark_plugin connmark_plugin + * @{ @ingroup connmark + */ + +#ifndef CONNMARK_PLUGIN_H_ +#define CONNMARK_PLUGIN_H_ + +#include + +typedef struct connmark_plugin_t connmark_plugin_t; + +/** + * Plugin using marks to select return path SA based on conntrack. + */ +struct connmark_plugin_t { + + /** + * implements plugin interface + */ + plugin_t plugin; +}; + +#endif /** CONNMARK_PLUGIN_H_ @}*/ diff --git a/src/libcharon/sa/ike_sa.c b/src/libcharon/sa/ike_sa.c index d4ef7b861..571c5c0ad 100644 --- a/src/libcharon/sa/ike_sa.c +++ b/src/libcharon/sa/ike_sa.c @@ -932,6 +932,7 @@ METHOD(ike_sa_t, update_hosts, void, /* update our address in any case */ if (force && !me->equals(me, this->my_host)) { + charon->bus->ike_update(charon->bus, &this->public, TRUE, me); set_my_host(this, me->clone(me)); update = TRUE; } @@ -945,6 +946,7 @@ METHOD(ike_sa_t, update_hosts, void, (!has_condition(this, COND_NAT_HERE) || !has_condition(this, COND_ORIGINAL_INITIATOR))) { + charon->bus->ike_update(charon->bus, &this->public, FALSE, other); set_other_host(this, other->clone(other)); update = TRUE; } diff --git a/src/libstrongswan/ipsec/ipsec_types.c b/src/libstrongswan/ipsec/ipsec_types.c index 4bbd918a0..f2ee11ee8 100644 --- a/src/libstrongswan/ipsec/ipsec_types.c +++ b/src/libstrongswan/ipsec/ipsec_types.c @@ -48,7 +48,15 @@ bool mark_from_string(const char *value, mark_t *mark) { return FALSE; } - mark->value = strtoul(value, &endptr, 0); + if (strcasepfx(value, "%unique")) + { + mark->value = MARK_UNIQUE; + endptr = (char*)value + strlen("%unique"); + } + else + { + mark->value = strtoul(value, &endptr, 0); + } if (*endptr) { if (*endptr != '/') diff --git a/src/swanctl/swanctl.opt b/src/swanctl/swanctl.opt index 588004562..01ff48e76 100644 --- a/src/swanctl/swanctl.opt +++ b/src/swanctl/swanctl.opt @@ -622,9 +622,10 @@ connections..children..reqid = 0 connections..children..mark_in = 0/0x00000000 Netfilter mark and mask for input traffic. - Netfilter mark and mask for input traffic. On Linux Netfilter may apply - marks to each packet coming from a tunnel having that option set. The - mark may then be used by Netfilter to match rules. + Netfilter mark and mask for input traffic. On Linux Netfilter may require + marks on each packet to match an SA having that option set. This allows + Netfilter rules to select specific tunnels for incoming traffic. The + special value _%unique_ sets a unique mark on each CHILD_SA instance. An additional mask may be appended to the mark, separated by _/_. The default mask if omitted is 0xffffffff. @@ -634,7 +635,8 @@ connections..children..mark_out = 0/0x00000000 Netfilter mark and mask for output traffic. On Linux Netfilter may require marks on each packet to match a policy having that option set. This allows - Netfilter rules to select specific tunnels for outgoing traffic. + Netfilter rules to select specific tunnels for outgoing traffic. The + special value _%unique_ sets a unique mark on each CHILD_SA instance. An additional mask may be appended to the mark, separated by _/_. The default mask if omitted is 0xffffffff. diff --git a/testing/scripts/build-baseimage b/testing/scripts/build-baseimage index 075fd8e1e..c927934f1 100755 --- a/testing/scripts/build-baseimage +++ b/testing/scripts/build-baseimage @@ -21,7 +21,7 @@ INC=$INC,less,acpid,acpi-support-base,libldns-dev,libunbound-dev,dnsutils,screen INC=$INC,gnat,gprbuild,libahven3-dev,libxmlada4.1-dev,libgmpada3-dev INC=$INC,libalog0.4.1-base-dev,hostapd,libsoup2.4-dev,ca-certificates,unzip INC=$INC,python,python-setuptools,python-dev,python-pip -INC=$INC,libjson0-dev,libxslt1-dev,libapache2-mod-wsgi +INC=$INC,libjson0-dev,libxslt1-dev,libapache2-mod-wsgi,iptables-dev SERVICES="apache2 dbus isc-dhcp-server slapd bind9" INC=$INC,${SERVICES// /,} diff --git a/testing/scripts/recipes/013_strongswan.mk b/testing/scripts/recipes/013_strongswan.mk index 221f4f312..0bcf0ac66 100644 --- a/testing/scripts/recipes/013_strongswan.mk +++ b/testing/scripts/recipes/013_strongswan.mk @@ -70,6 +70,7 @@ CONFIG_OPTS = \ --enable-socket-dynamic \ --enable-dhcp \ --enable-farp \ + --enable-connmark \ --enable-addrblock \ --enable-ctr \ --enable-ccm \ diff --git a/testing/tests/ikev2/host2host-transport-connmark/description.txt b/testing/tests/ikev2/host2host-transport-connmark/description.txt new file mode 100644 index 000000000..6660279c9 --- /dev/null +++ b/testing/tests/ikev2/host2host-transport-connmark/description.txt @@ -0,0 +1,8 @@ +An IPsec transport-mode connection between the natted host alice +and gateway sun is successfully set up. The client venus behind +the same NAT as client alice also establishes the same transport-mode +connection. sun uses the connmark plugin and a %unique mark on +the CHILD_SAs to select the correct return path SA using connection tracking. +This allows sun to talk to both nodes for client initiated flows, even +if the SAs are actually both over moon.
+To test the connection, both hosts establish an SSH connection to sun. diff --git a/testing/tests/ikev2/host2host-transport-connmark/evaltest.dat b/testing/tests/ikev2/host2host-transport-connmark/evaltest.dat new file mode 100644 index 000000000..04a35c10c --- /dev/null +++ b/testing/tests/ikev2/host2host-transport-connmark/evaltest.dat @@ -0,0 +1,7 @@ +sun:: ipsec status 2> /dev/null::nat-t.*ESTABLISHED.*sun.strongswan.org.*venus.strongswan.org::YES +sun:: ipsec status 2> /dev/null::nat-t.*ESTABLISHED.*sun.strongswan.org.*alice@strongswan.org::YES +alice::ipsec status 2> /dev/null::nat-t.*INSTALLED, TRANSPORT, reqid 1::YES +venus::ipsec status 2> /dev/null::nat-t.*INSTALLED, TRANSPORT, reqid 1::YES +alice::ssh 192.168.0.2 'echo alice-echo && exit'::alice-echo::YES +venus::ssh 192.168.0.2 'echo venus-echo && exit'::venus-echo::YES +sun::iptables -t mangle -L -n -v diff --git a/testing/tests/ikev2/host2host-transport-connmark/hosts/alice/etc/ipsec.conf b/testing/tests/ikev2/host2host-transport-connmark/hosts/alice/etc/ipsec.conf new file mode 100644 index 000000000..9000ebcfe --- /dev/null +++ b/testing/tests/ikev2/host2host-transport-connmark/hosts/alice/etc/ipsec.conf @@ -0,0 +1,17 @@ +# /etc/ipsec.conf - strongSwan IPsec configuration file + +config setup + +conn %default + ikelifetime=60m + keylife=20m + rekeymargin=3m + keyingtries=1 + +conn nat-t + leftcert=aliceCert.pem + leftid=alice@strongswan.org + right=192.168.0.2 + rightid=@sun.strongswan.org + type=transport + auto=add diff --git a/testing/tests/ikev2/host2host-transport-connmark/hosts/sun/etc/ipsec.conf b/testing/tests/ikev2/host2host-transport-connmark/hosts/sun/etc/ipsec.conf new file mode 100644 index 000000000..220059c43 --- /dev/null +++ b/testing/tests/ikev2/host2host-transport-connmark/hosts/sun/etc/ipsec.conf @@ -0,0 +1,18 @@ +# /etc/ipsec.conf - strongSwan IPsec configuration file + +config setup + +conn %default + ikelifetime=60m + keylife=20m + rekeymargin=3m + keyingtries=1 + left=192.168.0.2 + leftcert=sunCert.pem + leftid=@sun.strongswan.org + +conn nat-t + right=%any + type=transport + mark=%unique + auto=add diff --git a/testing/tests/ikev2/host2host-transport-connmark/hosts/sun/etc/strongswan.conf b/testing/tests/ikev2/host2host-transport-connmark/hosts/sun/etc/strongswan.conf new file mode 100644 index 000000000..1311e5b27 --- /dev/null +++ b/testing/tests/ikev2/host2host-transport-connmark/hosts/sun/etc/strongswan.conf @@ -0,0 +1,5 @@ +# /etc/strongswan.conf - strongSwan configuration file + +charon { + load = aes des sha1 sha2 md5 pem pkcs1 gmp random nonce x509 curl revocation hmac xcbc stroke kernel-netlink socket-default connmark +} diff --git a/testing/tests/ikev2/host2host-transport-connmark/hosts/venus/etc/ipsec.conf b/testing/tests/ikev2/host2host-transport-connmark/hosts/venus/etc/ipsec.conf new file mode 100644 index 000000000..cea239abe --- /dev/null +++ b/testing/tests/ikev2/host2host-transport-connmark/hosts/venus/etc/ipsec.conf @@ -0,0 +1,17 @@ +# /etc/ipsec.conf - strongSwan IPsec configuration file + +config setup + +conn %default + ikelifetime=60m + keylife=20m + rekeymargin=3m + keyingtries=1 + +conn nat-t + leftcert=venusCert.pem + leftid=venus@strongswan.org + right=192.168.0.2 + rightid=@sun.strongswan.org + type=transport + auto=add diff --git a/testing/tests/ikev2/host2host-transport-connmark/posttest.dat b/testing/tests/ikev2/host2host-transport-connmark/posttest.dat new file mode 100644 index 000000000..144be6c90 --- /dev/null +++ b/testing/tests/ikev2/host2host-transport-connmark/posttest.dat @@ -0,0 +1,5 @@ +alice::ipsec stop +venus::ipsec stop +sun::ipsec stop +moon::iptables-restore < /etc/iptables.flush +sun::iptables-restore < /etc/iptables.flush diff --git a/testing/tests/ikev2/host2host-transport-connmark/pretest.dat b/testing/tests/ikev2/host2host-transport-connmark/pretest.dat new file mode 100644 index 000000000..ab6408427 --- /dev/null +++ b/testing/tests/ikev2/host2host-transport-connmark/pretest.dat @@ -0,0 +1,11 @@ +moon::iptables-restore < /etc/iptables.rules +moon::iptables -t nat -A POSTROUTING -o eth0 -s 10.1.0.0/16 -j MASQUERADE +moon::iptables -A FORWARD -i eth1 -o eth0 -s 10.1.0.0/16 -j ACCEPT +moon::iptables -A FORWARD -i eth0 -o eth1 -d 10.1.0.0/16 -j ACCEPT +alice::ipsec start +venus::ipsec start +sun::ipsec start +alice::expect-connection nat-t +venus::expect-connection nat-t +alice::ipsec up nat-t +venus::ipsec up nat-t diff --git a/testing/tests/ikev2/host2host-transport-connmark/test.conf b/testing/tests/ikev2/host2host-transport-connmark/test.conf new file mode 100644 index 000000000..8c2facefd --- /dev/null +++ b/testing/tests/ikev2/host2host-transport-connmark/test.conf @@ -0,0 +1,21 @@ +#!/bin/bash +# +# This configuration file provides information on the +# guest instances used for this test + +# All guest instances that are required for this test +# +VIRTHOSTS="alice moon winnetou sun" + +# Corresponding block diagram +# +DIAGRAM="a-m-w-s-b.png" + +# Guest instances on which tcpdump is to be started +# +TCPDUMPHOSTS="sun alice venus moon" + +# Guest instances on which IPsec is started +# Used for IPsec logging purposes +# +IPSECHOSTS="alice venus sun"