]> git.ipfire.org Git - thirdparty/strongswan.git/commitdiff
kernel-netlink: Offload bypass policies for IKE ports on interfaces
authorTobias Brunner <tobias@strongswan.org>
Fri, 27 Jan 2023 13:08:41 +0000 (14:08 +0100)
committerTobias Brunner <tobias@strongswan.org>
Thu, 16 Feb 2023 13:45:57 +0000 (14:45 +0100)
While this uses the same mechanism, it's not necessary to explicitly
enable port_bypass, the regular socket policies work fine to bypass any
software policies.

src/libcharon/plugins/kernel_netlink/kernel_netlink_ipsec.c

index 0a22692f1f9a9849d360030fde66c62f0571dac6..1e7ecef17222072903b35f9b6df65e44b0ae467a 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2006-2022 Tobias Brunner
+ * Copyright (C) 2006-2023 Tobias Brunner
  * Copyright (C) 2005-2009 Martin Willi
  * Copyright (C) 2008-2016 Andreas Steffen
  * Copyright (C) 2006-2007 Fabian Hartmann, Noah Heusser
@@ -358,10 +358,34 @@ struct private_kernel_netlink_ipsec_t {
        bool policy_update;
 
        /**
-        * Installed port based IKE bypass policies, as bypass_t
+        * Whether to use port-based policies instead of socket policies for the
+        * IKE sockets/ports
+        */
+       bool port_bypass;
+
+       /**
+        * Installed port-based IKE bypass policies, as bypass_t
+        *
+        * If they are potentially offloaded, the offload mutex has to be locked
+        * when modifying it
         */
        array_t *bypass;
 
+       /**
+        * Interfaces that potentially support HW offloading, as offload_iface_t
+        */
+       hashtable_t *offload_interfaces;
+
+       /**
+        * Mutex to safely access the interfaces and bypasses
+        */
+       mutex_t *offload_mutex;
+
+       /**
+        * Netlink routing socket to receive link events
+        */
+       netlink_event_socket_t *socket_link_events;
+
        /**
         * Custom priority calculation function
         */
@@ -1352,7 +1376,7 @@ static struct {
 /**
  * Check if kernel supports HW offload and determine feature flag
  */
-static void netlink_find_offload_feature(const char *ifname)
+static bool netlink_find_offload_feature(const char *ifname)
 {
        struct ethtool_sset_info *sset_info;
        struct ethtool_gstrings *cmd = NULL;
@@ -1364,7 +1388,7 @@ static void netlink_find_offload_feature(const char *ifname)
        query_socket = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_XFRM);
        if (query_socket < 0)
        {
-               return;
+               return FALSE;
        }
 
        /* determine number of device features */
@@ -1416,10 +1440,11 @@ out:
        free(sset_info);
        free(cmd);
        close(query_socket);
+       return netlink_hw_offload.supported;
 }
 
 /**
- * Check if interface supported HW offload
+ * Check if interface supports HW offload
  */
 static bool netlink_detect_offload(const char *ifname)
 {
@@ -1460,11 +1485,6 @@ static bool netlink_detect_offload(const char *ifname)
                        ret = TRUE;
                }
        }
-
-       if (!ret)
-       {
-               DBG1(DBG_KNL, "HW offload is not supported by device");
-       }
        free(cmd);
        close(query_socket);
        return ret;
@@ -1472,8 +1492,9 @@ static bool netlink_detect_offload(const char *ifname)
 
 #else
 
-static void netlink_find_offload_feature(const char *ifname)
+static bool netlink_find_offload_feature(const char *ifname)
 {
+       return FALSE;
 }
 
 static bool netlink_detect_offload(const char *ifname)
@@ -1526,6 +1547,7 @@ static bool add_hw_offload(struct nlmsghdr *hdr, int buflen, host_t *local,
        /* check if interface supports hw_offload */
        if (!netlink_detect_offload(ifname))
        {
+               DBG1(DBG_KNL, "HW offload is not supported by device %s", ifname);
                goto out;
        }
 
@@ -3424,6 +3446,44 @@ static bool add_socket_bypass(private_kernel_netlink_ipsec_t *this,
        return TRUE;
 }
 
+/**
+ * Keep track of interface and its offload support
+ */
+typedef struct {
+
+       /**
+        * Interface index
+        */
+       int ifindex;
+
+       /**
+        * Name of the interface
+        */
+       char ifname[IFNAMSIZ];
+
+       /**
+        * Interface flags
+        */
+       u_int flags;
+
+       /**
+        * Offload state
+        */
+       enum {
+               /** Offload support unknown */
+               IFACE_OFFLOAD_UNKNOWN,
+               /** No offload supported */
+               IFACE_OFFLOAD_NONE,
+               /** Interface supports at least crypto offload */
+               IFACE_OFFLOAD_DETECTED,
+               /** Interface supports crypto offload, but no packet and policy offload */
+               IFACE_OFFLOAD_CRYPTO,
+               /** Packet and policy offload supported */
+               IFACE_OFFLOAD_PACKET,
+       } offload;
+
+} offload_iface_t;
+
 /**
  * Port based IKE bypass policy
  */
@@ -3437,13 +3497,16 @@ typedef struct {
 } bypass_t;
 
 /**
- * Add or remove a bypass policy from/to kernel
+ * Add or remove a bypass policy from/to kernel. If an interface is given,
+ * the policy is tried to be offloaded to that interface.
  */
 static bool manage_bypass(private_kernel_netlink_ipsec_t *this,
-                                                 int type, policy_dir_t dir, bypass_t *bypass)
+                                                 int type, policy_dir_t dir, bypass_t *bypass,
+                                                 char *ifname)
 {
        netlink_buf_t request;
        struct xfrm_selector *sel;
+       struct xfrm_user_offload *offload = NULL;
        struct nlmsghdr *hdr;
 
        memset(&request, 0, sizeof(request));
@@ -3469,6 +3532,13 @@ static bool manage_bypass(private_kernel_netlink_ipsec_t *this,
                policy->lft.hard_packet_limit = XFRM_INF;
 
                sel = &policy->sel;
+
+               if (ifname &&
+                       !add_hw_offload(hdr, sizeof(request), NULL, ifname,
+                                                       HW_OFFLOAD_PACKET, &offload))
+               {
+                       return FALSE;
+               }
        }
        else /* XFRM_MSG_DELPOLICY */
        {
@@ -3494,14 +3564,137 @@ static bool manage_bypass(private_kernel_netlink_ipsec_t *this,
                sel->sport = bypass->port;
                sel->sport_mask = 0xffff;
        }
+       if (ifname)
+       {
+               sel->ifindex = if_nametoindex(ifname);
+       }
        return this->socket_xfrm->send_ack(this->socket_xfrm, hdr) == SUCCESS;
 }
 
+CALLBACK(remove_port_bypass, void,
+       bypass_t *bypass, int idx, void *user)
+{
+       private_kernel_netlink_ipsec_t *this = user;
+       enumerator_t *enumerator;
+       offload_iface_t *iface;
+
+       if (this->port_bypass)
+       {
+               manage_bypass(this, XFRM_MSG_DELPOLICY, POLICY_OUT, bypass, NULL);
+               manage_bypass(this, XFRM_MSG_DELPOLICY, POLICY_IN, bypass, NULL);
+       }
+       if (this->offload_interfaces)
+       {
+               enumerator = this->offload_interfaces->create_enumerator(this->offload_interfaces);
+               while (enumerator->enumerate(enumerator, NULL, &iface))
+               {
+                       if (iface->offload == IFACE_OFFLOAD_PACKET &&
+                               iface->flags & IFF_UP)
+                       {
+                               manage_bypass(this, XFRM_MSG_DELPOLICY, POLICY_OUT, bypass,
+                                                         iface->ifname);
+                               manage_bypass(this, XFRM_MSG_DELPOLICY, POLICY_IN, bypass,
+                                                         iface->ifname);
+                       }
+               }
+               enumerator->destroy(enumerator);
+       }
+}
+
 /**
- * Bypass socket using a port-based bypass policy
+ * Bypass socket using a port-based bypass policy, optionally offloaded to a
+ * given interface
  */
 static bool add_port_bypass(private_kernel_netlink_ipsec_t *this,
-                                                       int fd, int family)
+                                                       bypass_t *bypass, char *ifname)
+{
+       if (!manage_bypass(this, XFRM_MSG_NEWPOLICY, POLICY_IN, bypass, ifname))
+       {
+               return FALSE;
+       }
+       if (!manage_bypass(this, XFRM_MSG_NEWPOLICY, POLICY_OUT, bypass, ifname))
+       {
+               manage_bypass(this, XFRM_MSG_DELPOLICY, POLICY_IN, bypass, ifname);
+               return FALSE;
+       }
+       return TRUE;
+}
+
+/**
+ * Offload the given port-based bypass policy to the given interface if possible.
+ *
+ * offload_mutex is assumed to be locked.
+ */
+static bool offload_bypass_iface(private_kernel_netlink_ipsec_t *this,
+                                                                bypass_t *bypass, offload_iface_t *iface)
+{
+       if ((iface->offload == IFACE_OFFLOAD_DETECTED ||
+                iface->offload == IFACE_OFFLOAD_PACKET))
+       {
+               if (add_port_bypass(this, bypass, iface->ifname))
+               {
+                       iface->offload = IFACE_OFFLOAD_PACKET;
+                       return TRUE;
+               }
+               else if (iface->offload == IFACE_OFFLOAD_DETECTED)
+               {
+                       iface->offload = IFACE_OFFLOAD_CRYPTO;
+               }
+       }
+       return FALSE;
+}
+
+/**
+ * Offload all known port-based bypass policies to the given interface.
+ *
+ * offload_mutex is assumed to be locked.
+ */
+static void offload_bypasses(private_kernel_netlink_ipsec_t *this,
+                                                        offload_iface_t *iface)
+{
+       enumerator_t *enumerator;
+       bypass_t *bypass;
+
+       enumerator = array_create_enumerator(this->bypass);
+       while (enumerator->enumerate(enumerator, &bypass))
+       {
+               if (!offload_bypass_iface(this, bypass, iface))
+               {       /* could indicate a failure but generally means that the interface
+                        * does not support offloading */
+                       break;
+               }
+       }
+       enumerator->destroy(enumerator);
+}
+
+/**
+ * Offload a new port-based bypass policy to all known interfaces.
+ *
+ * offload_mutex is assumed to be locked.
+ */
+static void offload_bypass(private_kernel_netlink_ipsec_t *this,
+                                                  bypass_t *bypass)
+{
+       enumerator_t *enumerator;
+       offload_iface_t *iface;
+
+       enumerator = this->offload_interfaces->create_enumerator(this->offload_interfaces);
+       while (enumerator->enumerate(enumerator, NULL, &iface))
+       {
+               if (iface->flags & IFF_UP)
+               {
+                       offload_bypass_iface(this, bypass, iface);
+               }
+       }
+       enumerator->destroy(enumerator);
+}
+
+/**
+ * Offload a bypass policy on supported hardware if the kernel supports it and
+ * optionally install a port-based bypass policy in software.
+ */
+static bool add_and_offload_port_bypass(private_kernel_netlink_ipsec_t *this,
+                                                                               int fd, int family)
 {
        union {
                struct sockaddr sa;
@@ -3537,39 +3730,38 @@ static bool add_port_bypass(private_kernel_netlink_ipsec_t *this,
                        return FALSE;
        }
 
-       if (!manage_bypass(this, XFRM_MSG_NEWPOLICY, POLICY_IN, &bypass))
+       if (this->port_bypass &&
+               !add_port_bypass(this, &bypass, NULL))
        {
                return FALSE;
        }
-       if (!manage_bypass(this, XFRM_MSG_NEWPOLICY, POLICY_OUT, &bypass))
+       if (this->offload_interfaces)
        {
-               manage_bypass(this, XFRM_MSG_DELPOLICY, POLICY_IN, &bypass);
-               return FALSE;
+               this->offload_mutex->lock(this->offload_mutex);
+               offload_bypass(this, &bypass);
+               /* store it even if no policy was offloaded because an interface that
+                * supports offloading might get activated later */
+               array_insert_create_value(&this->bypass, sizeof(bypass_t),
+                                                                 ARRAY_TAIL, &bypass);
+               this->offload_mutex->unlock(this->offload_mutex);
+       }
+       else
+       {
+               array_insert_create_value(&this->bypass, sizeof(bypass_t),
+                                                                 ARRAY_TAIL, &bypass);
        }
-       array_insert(this->bypass, ARRAY_TAIL, &bypass);
-
        return TRUE;
 }
 
-/**
- * Remove installed port based bypass policy
- */
-static void remove_port_bypass(bypass_t *bypass, int idx,
-                                                          private_kernel_netlink_ipsec_t *this)
-{
-       manage_bypass(this, XFRM_MSG_DELPOLICY, POLICY_OUT, bypass);
-       manage_bypass(this, XFRM_MSG_DELPOLICY, POLICY_IN, bypass);
-}
-
 METHOD(kernel_ipsec_t, bypass_socket, bool,
        private_kernel_netlink_ipsec_t *this, int fd, int family)
 {
-       if (lib->settings->get_bool(lib->settings,
-                                       "%s.plugins.kernel-netlink.port_bypass", FALSE, lib->ns))
+       if ((this->offload_interfaces || this->port_bypass) &&
+               !add_and_offload_port_bypass(this, fd, family))
        {
-               return add_port_bypass(this, fd, family);
+               return FALSE;
        }
-       return add_socket_bypass(this, fd, family);
+       return this->port_bypass || add_socket_bypass(this, fd, family);
 }
 
 METHOD(kernel_ipsec_t, enable_udp_decap, bool,
@@ -3585,26 +3777,161 @@ METHOD(kernel_ipsec_t, enable_udp_decap, bool,
        return TRUE;
 }
 
+CALLBACK(receive_link_events, void,
+       private_kernel_netlink_ipsec_t *this, struct nlmsghdr *hdr)
+{
+       struct ifinfomsg *msg = NLMSG_DATA(hdr);
+       struct rtattr *rta = IFLA_RTA(msg);
+       size_t rtasize = IFLA_PAYLOAD (hdr);
+       offload_iface_t *iface = NULL;
+       char *name = NULL;
+
+       if (hdr->nlmsg_type != RTM_NEWLINK &&
+               hdr->nlmsg_type != RTM_DELLINK)
+       {
+               return;
+       }
+
+       while (RTA_OK(rta, rtasize))
+       {
+               switch (rta->rta_type)
+               {
+                       case IFLA_IFNAME:
+                               name = RTA_DATA(rta);
+                               break;
+               }
+               rta = RTA_NEXT(rta, rtasize);
+       }
+       if (!name)
+       {
+               return;
+       }
+
+       this->offload_mutex->lock(this->offload_mutex);
+       if (hdr->nlmsg_type == RTM_NEWLINK)
+       {
+               iface = this->offload_interfaces->get(this->offload_interfaces,
+                                                                                         (void*)(uintptr_t)msg->ifi_index);
+               if (!iface)
+               {
+                       INIT(iface,
+                               .ifindex = msg->ifi_index
+                       );
+                       this->offload_interfaces->put(this->offload_interfaces,
+                                                                                 (void*)(uintptr_t)msg->ifi_index,
+                                                                                 iface);
+               }
+               /* update name in case an interface is renamed */
+               strncpy(iface->ifname, name, IFNAMSIZ-1);
+               iface->ifname[IFNAMSIZ-1] = '\0';
+
+               if (iface->offload == IFACE_OFFLOAD_UNKNOWN)
+               {
+                       if (netlink_detect_offload(iface->ifname))
+                       {
+                               iface->offload = IFACE_OFFLOAD_DETECTED;
+                       }
+                       else
+                       {
+                               iface->offload = IFACE_OFFLOAD_NONE;
+                       }
+               }
+
+               /* if an interface is activated or newly detected, try to offload known
+                * IKE bypass policies. we don't have to do anything if the interface
+                * goes down as the kernel automatically removes the state it has for
+                * offloaded policies */
+               if (!(iface->flags & IFF_UP) && (msg->ifi_flags & IFF_UP))
+               {
+                       offload_bypasses(this, iface);
+               }
+               iface->flags = msg->ifi_flags;
+       }
+       else
+       {
+               iface = this->offload_interfaces->remove(this->offload_interfaces,
+                                                                                                (void*)(uintptr_t)msg->ifi_index);
+               free(iface);
+       }
+       this->offload_mutex->unlock(this->offload_mutex);
+}
+
+/**
+ * Enumerate all interfaces and check if they support offloading
+ */
+static bool init_offload_interfaces(private_kernel_netlink_ipsec_t *this)
+{
+       netlink_buf_t request;
+       netlink_socket_t *socket;
+       struct nlmsghdr *out, *current, *in;
+       struct rtgenmsg *msg;
+       size_t len;
+
+       socket = netlink_socket_create(NETLINK_ROUTE, NULL, FALSE);
+       if (!socket)
+       {
+               return FALSE;
+       }
+
+       memset(&request, 0, sizeof(request));
+
+       in = &request.hdr;
+       in->nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
+       in->nlmsg_type = RTM_GETLINK;
+       in->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtgenmsg));
+
+       msg = NLMSG_DATA(in);
+       msg->rtgen_family = AF_UNSPEC;
+
+       if (socket->send(socket, in, &out, &len) != SUCCESS)
+       {
+               socket->destroy(socket);
+               return FALSE;
+       }
+
+       current = out;
+       while (NLMSG_OK(current, len))
+       {
+               receive_link_events(this, current);
+               current = NLMSG_NEXT(current, len);
+       }
+       free(out);
+       socket->destroy(socket);
+       return TRUE;
+}
+
 METHOD(kernel_ipsec_t, destroy, void,
        private_kernel_netlink_ipsec_t *this)
 {
        enumerator_t *enumerator;
        policy_entry_t *policy;
+       offload_iface_t *iface;
 
-       array_destroy_function(this->bypass,
-                                                  (array_callback_t)remove_port_bypass, this);
+       DESTROY_IF(this->socket_link_events);
        DESTROY_IF(this->socket_xfrm_events);
+       array_destroy_function(this->bypass, remove_port_bypass, this);
        DESTROY_IF(this->socket_xfrm);
        enumerator = this->policies->create_enumerator(this->policies);
-       while (enumerator->enumerate(enumerator, &policy, &policy))
+       while (enumerator->enumerate(enumerator, NULL, &policy))
        {
                policy_entry_destroy(this, policy);
        }
        enumerator->destroy(enumerator);
        this->policies->destroy(this->policies);
        this->sas->destroy(this->sas);
+       if (this->offload_interfaces)
+       {
+               enumerator = this->offload_interfaces->create_enumerator(this->offload_interfaces);
+               while (enumerator->enumerate(enumerator, NULL, &iface))
+               {
+                       free(iface);
+               }
+               enumerator->destroy(enumerator);
+               this->offload_interfaces->destroy(this->offload_interfaces);
+       }
        this->condvar->destroy(this->condvar);
        this->mutex->destroy(this->mutex);
+       DESTROY_IF(this->offload_mutex);
        free(this);
 }
 
@@ -3742,7 +4069,6 @@ kernel_netlink_ipsec_t *kernel_netlink_ipsec_create()
                                                                         (hashtable_equals_t)policy_equals, 32),
                .sas = hashtable_create((hashtable_hash_t)ipsec_sa_hash,
                                                                (hashtable_equals_t)ipsec_sa_equals, 32),
-               .bypass = array_create(sizeof(bypass_t), 0),
                .mutex = mutex_create(MUTEX_TYPE_DEFAULT),
                .condvar = condvar_create(CONDVAR_TYPE_DEFAULT),
                .get_priority = dlsym(RTLD_DEFAULT,
@@ -3754,6 +4080,8 @@ kernel_netlink_ipsec_t *kernel_netlink_ipsec_create()
                .proto_port_transport = lib->settings->get_bool(lib->settings,
                                                "%s.plugins.kernel-netlink.set_proto_port_transport_sa",
                                                FALSE, lib->ns),
+               .port_bypass = lib->settings->get_bool(lib->settings,
+                                               "%s.plugins.kernel-netlink.port_bypass", FALSE, lib->ns),
        );
 
        this->socket_xfrm = netlink_socket_create(NETLINK_XFRM, xfrm_msg_names,
@@ -3778,9 +4106,22 @@ kernel_netlink_ipsec_t *kernel_netlink_ipsec_create()
                return NULL;
        }
 
-       netlink_find_offload_feature(lib->settings->get_str(lib->settings,
+       if (netlink_find_offload_feature(lib->settings->get_str(lib->settings,
                                        "%s.plugins.kernel-netlink.hw_offload_feature_interface",
-                                       "lo", lib->ns));
-
+                                       "lo", lib->ns)))
+       {
+               this->offload_interfaces = hashtable_create(hashtable_hash_ptr,
+                                                                                                       hashtable_equals_ptr, 8);
+               this->offload_mutex = mutex_create(MUTEX_TYPE_DEFAULT);
+               this->socket_link_events = netlink_event_socket_create(NETLINK_ROUTE,
+                                                                                                       nl_group(RTNLGRP_LINK),
+                                                                                                       receive_link_events, this);
+               if (!this->socket_link_events ||
+                       !init_offload_interfaces(this))
+               {
+                       destroy(this);
+                       return NULL;
+               }
+       }
        return &this->public;
 }