[PATCH 3/3] net: airoha: add EIP93-backed ESP XFRM offload
Jihong Min
hurryman2212 at gmail.com
Sat May 23 05:15:22 PDT 2026
Wire Airoha GDM netdevs and DSA user ports to the EIP93 ESP packet
backend through xfrmdev_ops.
Gate netdev feature advertisement on backend capability, add TX and RX
submit paths, preserve opt-out builds, and handle SA lifetime across
feature changes, DSA detach, and EIP93 provider loss.
Assisted-by: Codex:gpt-5.5
Signed-off-by: Jihong Min <hurryman2212 at gmail.com>
---
drivers/net/ethernet/airoha/Kconfig | 11 +
drivers/net/ethernet/airoha/Makefile | 1 +
drivers/net/ethernet/airoha/airoha_eth.c | 51 +-
drivers/net/ethernet/airoha/airoha_eth.h | 69 +
drivers/net/ethernet/airoha/airoha_xfrm.c | 1474 +++++++++++++++++++++
5 files changed, 1605 insertions(+), 1 deletion(-)
create mode 100644 drivers/net/ethernet/airoha/airoha_xfrm.c
diff --git a/drivers/net/ethernet/airoha/Kconfig b/drivers/net/ethernet/airoha/Kconfig
index ad3ce501e7a5..302534c89fdd 100644
--- a/drivers/net/ethernet/airoha/Kconfig
+++ b/drivers/net/ethernet/airoha/Kconfig
@@ -31,4 +31,15 @@ config NET_AIROHA_FLOW_STATS
help
Enable Aiorha flowtable statistic counters.
+config NET_AIROHA_XFRM
+ bool "Airoha ESP XFRM offload support"
+ depends on NET_AIROHA
+ default y
+ help
+ Enable ESP XFRM offload support for Airoha Ethernet netdevs.
+
+ If unsure, say Y. Say N to opt out of advertising ESP hardware
+ offload from the Airoha Ethernet driver even when the EIP93 IPsec
+ packet backend and XFRM offload support are available.
+
endif #NET_VENDOR_AIROHA
diff --git a/drivers/net/ethernet/airoha/Makefile b/drivers/net/ethernet/airoha/Makefile
index 94468053e34b..15386665bb27 100644
--- a/drivers/net/ethernet/airoha/Makefile
+++ b/drivers/net/ethernet/airoha/Makefile
@@ -5,5 +5,6 @@
obj-$(CONFIG_NET_AIROHA) += airoha-eth.o
airoha-eth-y := airoha_eth.o airoha_ppe.o
+airoha-eth-$(CONFIG_NET_AIROHA_XFRM) += airoha_xfrm.o
airoha-eth-$(CONFIG_DEBUG_FS) += airoha_ppe_debugfs.o
obj-$(CONFIG_NET_AIROHA_NPU) += airoha_npu.o
diff --git a/drivers/net/ethernet/airoha/airoha_eth.c b/drivers/net/ethernet/airoha/airoha_eth.c
index cecd66251dba..877002c03738 100644
--- a/drivers/net/ethernet/airoha/airoha_eth.c
+++ b/drivers/net/ethernet/airoha/airoha_eth.c
@@ -684,6 +684,14 @@ static int airoha_qdma_rx_process(struct airoha_queue *q, int budget)
false);
done++;
+#if IS_ENABLED(CONFIG_NET_AIROHA_XFRM)
+ if (airoha_xfrm_in_active(port) &&
+ airoha_xfrm_rx_skb(port, q->skb)) {
+ q->skb = NULL;
+ continue;
+ }
+#endif
+
napi_gro_receive(&q->napi, q->skb);
q->skb = NULL;
continue;
@@ -2010,6 +2018,19 @@ static netdev_tx_t airoha_dev_xmit(struct sk_buff *skb,
void *data;
u16 index;
u8 fport;
+#if IS_ENABLED(CONFIG_NET_AIROHA_XFRM)
+ int err;
+
+ if (airoha_xfrm_out_active(port)) {
+ err = airoha_xfrm_encrypt_skb(port, skb);
+ if (err == -EINPROGRESS)
+ return NETDEV_TX_OK;
+ if (err == -EBUSY)
+ return NETDEV_TX_BUSY;
+ if (err)
+ goto error;
+ }
+#endif
qid = airoha_qdma_get_txq(qdma, skb_get_queue_mapping(skb));
tag = airoha_get_dsa_tag(skb, dev);
@@ -2895,6 +2916,8 @@ static const struct net_device_ops airoha_netdev_ops = {
.ndo_stop = airoha_dev_stop,
.ndo_change_mtu = airoha_dev_change_mtu,
.ndo_select_queue = airoha_dev_select_queue,
+ .ndo_fix_features = airoha_xfrm_fix_features,
+ .ndo_set_features = airoha_xfrm_set_features,
.ndo_start_xmit = airoha_dev_xmit,
.ndo_get_stats64 = airoha_dev_get_stats64,
.ndo_set_mac_address = airoha_dev_set_macaddr,
@@ -3025,6 +3048,7 @@ static int airoha_alloc_gdm_port(struct airoha_eth *eth,
/* XXX: Read nbq from DTS */
port->nbq = id == AIROHA_GDM3_IDX && airoha_is_7581(eth) ? 4 : 0;
eth->ports[p] = port;
+ airoha_xfrm_build_netdev(dev);
return airoha_metadata_dst_alloc(port);
}
@@ -3155,6 +3179,7 @@ static int airoha_probe(struct platform_device *pdev)
if (port->dev->reg_state == NETREG_REGISTERED)
unregister_netdev(port->dev);
+ airoha_xfrm_teardown_netdev(port->dev);
airoha_metadata_dst_free(port);
}
airoha_hw_cleanup(eth);
@@ -3180,6 +3205,7 @@ static void airoha_remove(struct platform_device *pdev)
continue;
unregister_netdev(port->dev);
+ airoha_xfrm_teardown_netdev(port->dev);
airoha_metadata_dst_free(port);
}
airoha_hw_cleanup(eth);
@@ -3328,7 +3354,30 @@ static struct platform_driver airoha_driver = {
.of_match_table = of_airoha_match,
},
};
-module_platform_driver(airoha_driver);
+
+static int __init airoha_init(void)
+{
+ int err;
+
+ err = airoha_xfrm_register_notifier();
+ if (err)
+ return err;
+
+ err = platform_driver_register(&airoha_driver);
+ if (err)
+ airoha_xfrm_unregister_notifier();
+
+ return err;
+}
+
+static void __exit airoha_exit(void)
+{
+ platform_driver_unregister(&airoha_driver);
+ airoha_xfrm_unregister_notifier();
+}
+
+module_init(airoha_init);
+module_exit(airoha_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Lorenzo Bianconi <lorenzo at kernel.org>");
diff --git a/drivers/net/ethernet/airoha/airoha_eth.h b/drivers/net/ethernet/airoha/airoha_eth.h
index 4fad3acc3ccf..4fe04c763271 100644
--- a/drivers/net/ethernet/airoha/airoha_eth.h
+++ b/drivers/net/ethernet/airoha/airoha_eth.h
@@ -11,6 +11,8 @@
#include <linux/etherdevice.h>
#include <linux/iopoll.h>
#include <linux/kernel.h>
+#include <linux/kconfig.h>
+#include <linux/jump_label.h>
#include <linux/netdevice.h>
#include <linux/reset.h>
#include <linux/soc/airoha/airoha_offload.h>
@@ -533,6 +535,12 @@ struct airoha_qdma {
struct airoha_queue q_rx[AIROHA_NUM_RX_RING];
};
+#if IS_ENABLED(CONFIG_NET_AIROHA_XFRM)
+struct eip93_ipsec;
+DECLARE_STATIC_KEY_FALSE(airoha_xfrm_in_state_key);
+DECLARE_STATIC_KEY_FALSE(airoha_xfrm_out_state_key);
+#endif
+
struct airoha_gdm_port {
struct airoha_qdma *qdma;
struct airoha_eth *eth;
@@ -549,6 +557,13 @@ struct airoha_gdm_port {
u64 fwd_tx_packets;
struct metadata_dst *dsa_meta[AIROHA_MAX_DSA_PORTS];
+
+#if IS_ENABLED(CONFIG_NET_AIROHA_XFRM)
+ struct eip93_ipsec *xfrm_ipsec;
+ atomic_t xfrm_state_count;
+ atomic_t xfrm_out_state_count;
+ atomic_t xfrm_in_state_count;
+#endif
};
#define AIROHA_RXD4_PPE_CPU_REASON GENMASK(20, 16)
@@ -683,4 +698,58 @@ static inline int airoha_ppe_debugfs_init(struct airoha_ppe *ppe)
}
#endif
+#if IS_ENABLED(CONFIG_NET_AIROHA_XFRM)
+static inline bool airoha_xfrm_in_active(struct airoha_gdm_port *port)
+{
+ return static_branch_unlikely(&airoha_xfrm_in_state_key) &&
+ atomic_read(&port->xfrm_in_state_count);
+}
+
+static inline bool airoha_xfrm_out_active(struct airoha_gdm_port *port)
+{
+ return static_branch_unlikely(&airoha_xfrm_out_state_key) &&
+ atomic_read(&port->xfrm_out_state_count);
+}
+
+void airoha_xfrm_build_netdev(struct net_device *dev);
+void airoha_xfrm_teardown_netdev(struct net_device *dev);
+netdev_features_t airoha_xfrm_fix_features(struct net_device *dev,
+ netdev_features_t features);
+int airoha_xfrm_set_features(struct net_device *dev,
+ netdev_features_t features);
+bool airoha_xfrm_rx_skb(struct airoha_gdm_port *port, struct sk_buff *skb);
+int airoha_xfrm_encrypt_skb(struct airoha_gdm_port *port, struct sk_buff *skb);
+int airoha_xfrm_register_notifier(void);
+void airoha_xfrm_unregister_notifier(void);
+#else
+static inline void airoha_xfrm_build_netdev(struct net_device *dev)
+{
+}
+
+static inline void airoha_xfrm_teardown_netdev(struct net_device *dev)
+{
+}
+
+static inline netdev_features_t
+airoha_xfrm_fix_features(struct net_device *dev, netdev_features_t features)
+{
+ return features;
+}
+
+static inline int airoha_xfrm_set_features(struct net_device *dev,
+ netdev_features_t features)
+{
+ return 0;
+}
+
+static inline int airoha_xfrm_register_notifier(void)
+{
+ return 0;
+}
+
+static inline void airoha_xfrm_unregister_notifier(void)
+{
+}
+#endif
+
#endif /* AIROHA_ETH_H */
diff --git a/drivers/net/ethernet/airoha/airoha_xfrm.c b/drivers/net/ethernet/airoha/airoha_xfrm.c
new file mode 100644
index 000000000000..58461954d098
--- /dev/null
+++ b/drivers/net/ethernet/airoha/airoha_xfrm.c
@@ -0,0 +1,1474 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026 Jihong Min <hurryman2212 at gmail.com>
+ */
+#include <crypto/eip93-ipsec.h>
+#include <linux/err.h>
+#include <linux/kmod.h>
+#include <linux/rtnetlink.h>
+#include <linux/slab.h>
+#include <linux/udp.h>
+#include <net/dst_metadata.h>
+#include <net/esp.h>
+#include <net/ip.h>
+#include <net/ip6_checksum.h>
+#include <net/ipv6.h>
+#include <net/net_namespace.h>
+#include <net/xfrm.h>
+
+#include "airoha_eth.h"
+
+#if IS_ENABLED(CONFIG_NET_AIROHA_XFRM)
+DEFINE_STATIC_KEY_FALSE(airoha_xfrm_in_state_key);
+DEFINE_STATIC_KEY_FALSE(airoha_xfrm_out_state_key);
+#endif
+
+#if IS_ENABLED(CONFIG_NET_AIROHA_XFRM) && \
+ IS_REACHABLE(CONFIG_CRYPTO_DEV_EIP93) && \
+ IS_ENABLED(CONFIG_CRYPTO_DEV_EIP93_IPSEC) && \
+ IS_REACHABLE(CONFIG_INET_ESP) && \
+ IS_REACHABLE(CONFIG_INET_ESP_OFFLOAD) && \
+ IS_ENABLED(CONFIG_XFRM_OFFLOAD)
+#define AIROHA_XFRM_FEATURES \
+ (NETIF_F_HW_ESP | NETIF_F_HW_ESP_TX_CSUM | NETIF_F_GSO_ESP)
+
+struct airoha_xfrm_state {
+ struct airoha_gdm_port *port;
+ struct eip93_ipsec_sa *sa;
+};
+
+static netdev_features_t airoha_xfrm_ipsec_features(struct eip93_ipsec *ipsec)
+{
+ netdev_features_t features = 0;
+ u32 ipsec_features;
+
+ ipsec_features = eip93_ipsec_features(ipsec);
+ if (ipsec_features & EIP93_IPSEC_FEATURE_ESP)
+ features |= NETIF_F_HW_ESP;
+ if (ipsec_features & EIP93_IPSEC_FEATURE_HW_ESP_TX_CSUM)
+ features |= NETIF_F_HW_ESP_TX_CSUM;
+ if (ipsec_features & EIP93_IPSEC_FEATURE_GSO_ESP)
+ features |= NETIF_F_GSO_ESP;
+
+ return features;
+}
+
+static int airoha_xfrm_request_module(struct net_device *dev,
+ const char *module_name)
+{
+ int err;
+
+ err = request_module("%s", module_name);
+ if (err) {
+ netdev_err(dev, "failed requesting module %s: %d\n",
+ module_name, err);
+ return err < 0 ? err : -ENOENT;
+ }
+
+ return 0;
+}
+
+static int airoha_xfrm_request_modules(struct net_device *dev)
+{
+ int err;
+
+ if (IS_MODULE(CONFIG_INET_ESP)) {
+ err = airoha_xfrm_request_module(dev, "esp4");
+ if (err)
+ return err;
+ }
+
+ if (IS_MODULE(CONFIG_INET_ESP_OFFLOAD)) {
+ err = airoha_xfrm_request_module(dev, "esp4_offload");
+ if (err)
+ return err;
+ }
+
+#if IS_REACHABLE(CONFIG_INET6_ESP)
+ if (IS_MODULE(CONFIG_INET6_ESP)) {
+ err = airoha_xfrm_request_module(dev, "esp6");
+ if (err)
+ return err;
+ }
+#endif
+
+#if IS_REACHABLE(CONFIG_INET6_ESP_OFFLOAD)
+ if (IS_MODULE(CONFIG_INET6_ESP_OFFLOAD)) {
+ err = airoha_xfrm_request_module(dev, "esp6_offload");
+ if (err)
+ return err;
+ }
+#endif
+
+ if (IS_MODULE(CONFIG_CRYPTO_DEV_EIP93)) {
+ err = airoha_xfrm_request_module(dev, "crypto-hw-eip93");
+ if (err)
+ return err;
+ }
+
+ return 0;
+}
+
+static int airoha_xfrm_prepare_ipsec(struct net_device *dev)
+{
+ struct airoha_gdm_port *port = netdev_priv(dev);
+ struct eip93_ipsec *ipsec;
+ int err;
+
+ if (port->xfrm_ipsec)
+ return eip93_ipsec_available(port->xfrm_ipsec) ? 0 : -ENODEV;
+
+ err = airoha_xfrm_request_modules(dev);
+ if (err)
+ return err;
+
+ ipsec = eip93_ipsec_get(port->eth->dev);
+ if (IS_ERR(ipsec)) {
+ netdev_dbg(dev,
+ "EIP93 ESP packet backend is unavailable: %ld\n",
+ PTR_ERR(ipsec));
+ return PTR_ERR(ipsec);
+ }
+
+ port->xfrm_ipsec = ipsec;
+ netdev_info(dev, "ESP HW offload available via EIP93 packet backend\n");
+
+ return 0;
+}
+
+static bool airoha_xfrm_state_supported(struct xfrm_state *x,
+ struct netlink_ext_ack *extack)
+{
+ if (x->xso.type != XFRM_DEV_OFFLOAD_CRYPTO) {
+ NL_SET_ERR_MSG_MOD(extack,
+ "only XFRM crypto offload is supported");
+ return false;
+ }
+
+ switch (x->xso.dir) {
+ case XFRM_DEV_OFFLOAD_OUT:
+ case XFRM_DEV_OFFLOAD_IN:
+ break;
+ default:
+ NL_SET_ERR_MSG_MOD(extack, "only in/out SAs are supported");
+ return false;
+ }
+
+ switch (x->props.family) {
+ case AF_INET:
+ break;
+#if IS_REACHABLE(CONFIG_INET6_ESP) && IS_REACHABLE(CONFIG_INET6_ESP_OFFLOAD)
+ case AF_INET6:
+ break;
+#endif
+ default:
+ NL_SET_ERR_MSG_MOD(extack,
+ "only IPv4/IPv6 ESP offload is supported");
+ return false;
+ }
+
+ if (x->outer_mode.family != x->props.family) {
+ NL_SET_ERR_MSG_MOD(extack,
+ "only same-family ESP offload is supported");
+ return false;
+ }
+
+ if (x->id.proto != IPPROTO_ESP) {
+ NL_SET_ERR_MSG_MOD(extack, "only ESP offload is supported");
+ return false;
+ }
+
+ switch (x->props.mode) {
+ case XFRM_MODE_TUNNEL:
+ case XFRM_MODE_TRANSPORT:
+ break;
+ default:
+ NL_SET_ERR_MSG_MOD(extack,
+ "only tunnel/transport modes are supported");
+ return false;
+ }
+
+ if (x->outer_mode.encap != x->props.mode) {
+ NL_SET_ERR_MSG_MOD(extack,
+ "outer ESP mode does not match state mode");
+ return false;
+ }
+
+ if (x->encap) {
+ NL_SET_ERR_MSG_MOD(extack,
+ "NAT-T is unsupported by EIP93 packet ESP");
+ return false;
+ }
+
+ if (x->tfcpad) {
+ NL_SET_ERR_MSG_MOD(extack, "TFC padding is not supported");
+ return false;
+ }
+
+ if (x->aead) {
+ NL_SET_ERR_MSG_MOD(extack, "AEAD SAs are unsupported");
+ return false;
+ }
+
+ if (!x->ealg || !x->aalg) {
+ NL_SET_ERR_MSG_MOD(extack,
+ "encryption/authentication required");
+ return false;
+ }
+
+ return true;
+}
+
+static const struct xfrmdev_ops airoha_xfrmdev_ops;
+
+#if IS_ENABLED(CONFIG_NET_DSA)
+static struct airoha_gdm_port *airoha_xfrm_dsa_dev_port(struct net_device *dev)
+{
+ struct net_device *conduit;
+ struct dsa_port *dp;
+
+ if (!dsa_user_dev_check(dev))
+ return NULL;
+
+ dp = dsa_port_from_netdev(dev);
+ if (IS_ERR(dp))
+ return NULL;
+
+ conduit = dsa_port_to_conduit(dp);
+ if (!conduit || conduit->xfrmdev_ops != &airoha_xfrmdev_ops)
+ return NULL;
+
+ return netdev_priv(conduit);
+}
+
+static struct net_device *airoha_xfrm_dsa_rx_dev(struct airoha_gdm_port *port,
+ struct sk_buff *skb)
+{
+ struct metadata_dst *md_dst = skb_metadata_dst(skb);
+ struct dsa_port *cpu_dp = port->dev->dsa_ptr;
+ struct dsa_port *dp;
+ u32 source_port;
+
+ if (!md_dst || md_dst->type != METADATA_HW_PORT_MUX)
+ return port->dev;
+
+ if (!cpu_dp || !cpu_dp->dst)
+ return NULL;
+
+ source_port = md_dst->u.port_info.port_id;
+ list_for_each_entry(dp, &cpu_dp->dst->ports, list) {
+ if (dp->type != DSA_PORT_TYPE_USER ||
+ dp->index != source_port || dp->cpu_dp != cpu_dp ||
+ dsa_port_to_conduit(dp) != port->dev || !dp->user)
+ continue;
+
+ return dp->user;
+ }
+
+ return NULL;
+}
+
+static bool airoha_xfrm_dsa_user_matches_port(struct net_device *user,
+ struct net_device *conduit)
+{
+ struct dsa_port *dp;
+
+ if (!dsa_user_dev_check(user))
+ return false;
+
+ dp = dsa_port_from_netdev(user);
+ if (IS_ERR(dp))
+ return false;
+
+ return dsa_port_to_conduit(dp) == conduit;
+}
+#else
+static struct airoha_gdm_port *airoha_xfrm_dsa_dev_port(struct net_device *dev)
+{
+ return NULL;
+}
+
+static struct net_device *airoha_xfrm_dsa_rx_dev(struct airoha_gdm_port *port,
+ struct sk_buff *skb)
+{
+ return port->dev;
+}
+#endif
+
+static struct airoha_gdm_port *airoha_xfrm_dev_port(struct net_device *dev)
+{
+ struct airoha_gdm_port *port;
+
+ if (dev->xfrmdev_ops != &airoha_xfrmdev_ops)
+ return NULL;
+
+ port = airoha_xfrm_dsa_dev_port(dev);
+ if (port)
+ return port;
+
+ return netdev_priv(dev);
+}
+
+static netdev_features_t airoha_xfrm_dev_features(struct net_device *dev)
+{
+ struct airoha_gdm_port *port = airoha_xfrm_dev_port(dev);
+
+ if (!port || !port->xfrm_ipsec)
+ return 0;
+
+ return airoha_xfrm_ipsec_features(port->xfrm_ipsec);
+}
+
+static struct net_device *airoha_xfrm_rx_dev(struct airoha_gdm_port *port,
+ struct sk_buff *skb)
+{
+ if (!netdev_uses_dsa(port->dev))
+ return port->dev;
+
+ return airoha_xfrm_dsa_rx_dev(port, skb);
+}
+
+static void airoha_xfrm_state_advance_esn(struct xfrm_state *x)
+{
+ struct airoha_xfrm_state *state;
+
+ state = (struct airoha_xfrm_state *)x->xso.offload_handle;
+ if (state)
+ eip93_ipsec_state_advance_esn(state->sa, x);
+}
+
+static int airoha_xfrm_state_add(struct net_device *dev, struct xfrm_state *x,
+ struct netlink_ext_ack *extack)
+{
+ struct airoha_gdm_port *port = airoha_xfrm_dev_port(dev);
+ struct airoha_xfrm_state *state;
+ int err;
+
+ if (!port) {
+ NL_SET_ERR_MSG_MOD(extack, "device lacks Airoha ESP offload");
+ return -EOPNOTSUPP;
+ }
+
+ if (!(dev->features & NETIF_F_HW_ESP)) {
+ NL_SET_ERR_MSG_MOD(extack,
+ "ESP HW offload is disabled on device");
+ return -EOPNOTSUPP;
+ }
+
+ if (!port->xfrm_ipsec || !eip93_ipsec_available(port->xfrm_ipsec)) {
+ NL_SET_ERR_MSG_MOD(extack,
+ "EIP93 packet backend is unavailable");
+ return -EOPNOTSUPP;
+ }
+
+ if (!airoha_xfrm_state_supported(x, extack))
+ return -EOPNOTSUPP;
+
+ state = kzalloc(sizeof(*state), GFP_KERNEL);
+ if (!state)
+ return -ENOMEM;
+
+ state->port = port;
+ err = eip93_ipsec_state_add(port->xfrm_ipsec, x, extack, &state->sa);
+ if (err) {
+ kfree(state);
+ return err;
+ }
+
+ x->xso.offload_handle = (unsigned long)state;
+ atomic_inc(&port->xfrm_state_count);
+ if (x->xso.dir == XFRM_DEV_OFFLOAD_OUT) {
+ atomic_inc(&port->xfrm_out_state_count);
+ static_branch_inc(&airoha_xfrm_out_state_key);
+ } else {
+ atomic_inc(&port->xfrm_in_state_count);
+ static_branch_inc(&airoha_xfrm_in_state_key);
+ }
+
+ return 0;
+}
+
+static void airoha_xfrm_state_delete(struct net_device *dev,
+ struct xfrm_state *x)
+{
+ struct airoha_xfrm_state *state;
+ struct airoha_gdm_port *port;
+
+ state = (struct airoha_xfrm_state *)x->xso.offload_handle;
+ if (!state)
+ return;
+
+ port = state->port;
+ x->xso.offload_handle = 0;
+ atomic_dec(&port->xfrm_state_count);
+ if (x->xso.dir == XFRM_DEV_OFFLOAD_OUT) {
+ atomic_dec(&port->xfrm_out_state_count);
+ static_branch_dec(&airoha_xfrm_out_state_key);
+ } else if (x->xso.dir == XFRM_DEV_OFFLOAD_IN) {
+ atomic_dec(&port->xfrm_in_state_count);
+ static_branch_dec(&airoha_xfrm_in_state_key);
+ }
+
+ eip93_ipsec_state_delete(state->sa);
+ kfree(state);
+}
+
+static bool airoha_xfrm_offload_ok(struct sk_buff *skb, struct xfrm_state *x)
+{
+ struct net_device *dev = skb->dev;
+ struct airoha_xfrm_state *state;
+ struct airoha_gdm_port *port;
+
+ if (!dev)
+ return false;
+
+ port = airoha_xfrm_dev_port(dev);
+ if (!port)
+ return false;
+
+ if (unlikely(x->xso.dir != XFRM_DEV_OFFLOAD_OUT ||
+ x->xso.type != XFRM_DEV_OFFLOAD_CRYPTO ||
+ !(dev->features & NETIF_F_HW_ESP) || x->xso.dev != dev))
+ return false;
+
+ state = (struct airoha_xfrm_state *)x->xso.offload_handle;
+ if (!state || state->port != port)
+ return false;
+
+ if (unlikely(skb_is_gso(skb)))
+ return false;
+
+ return true;
+}
+
+/*
+ * EIP93 packet-out mode creates ESP padding, trailer and ICV. The generic ESP
+ * xmit path should reserve tailroom only for plain, non-GSO ESP packets.
+ */
+static bool airoha_xfrm_esp_tx_hw_trailer(struct sk_buff *skb,
+ struct xfrm_state *x)
+{
+ return x->xso.dir == XFRM_DEV_OFFLOAD_OUT &&
+ x->xso.type == XFRM_DEV_OFFLOAD_CRYPTO && !x->encap &&
+ !skb_is_gso(skb);
+}
+
+static const struct xfrmdev_ops airoha_xfrmdev_ops = {
+ .xdo_dev_state_add = airoha_xfrm_state_add,
+ .xdo_dev_state_delete = airoha_xfrm_state_delete,
+ .xdo_dev_state_free = airoha_xfrm_state_delete,
+ .xdo_dev_offload_ok = airoha_xfrm_offload_ok,
+ .xdo_dev_esp_tx_hw_trailer = airoha_xfrm_esp_tx_hw_trailer,
+ .xdo_dev_state_advance_esn = airoha_xfrm_state_advance_esn,
+};
+
+void airoha_xfrm_build_netdev(struct net_device *dev)
+{
+ struct airoha_gdm_port *port = netdev_priv(dev);
+ netdev_features_t features;
+
+ atomic_set(&port->xfrm_state_count, 0);
+ atomic_set(&port->xfrm_out_state_count, 0);
+ atomic_set(&port->xfrm_in_state_count, 0);
+ if (airoha_xfrm_prepare_ipsec(dev))
+ return;
+
+ features = airoha_xfrm_ipsec_features(port->xfrm_ipsec);
+ if (!(features & NETIF_F_HW_ESP)) {
+ eip93_ipsec_put(port->xfrm_ipsec);
+ port->xfrm_ipsec = NULL;
+ return;
+ }
+
+ dev->xfrmdev_ops = &airoha_xfrmdev_ops;
+ dev->hw_features |= features;
+ dev->hw_enc_features |= features;
+ dev->gso_partial_features |= features & NETIF_F_GSO_ESP;
+}
+
+void airoha_xfrm_teardown_netdev(struct net_device *dev)
+{
+ struct airoha_gdm_port *port = netdev_priv(dev);
+
+ if (port->xfrm_ipsec) {
+ eip93_ipsec_put(port->xfrm_ipsec);
+ port->xfrm_ipsec = NULL;
+ }
+}
+
+/* Airoha TX checksum/GSO offloads run after EIP93 has encrypted the skb, so
+ * they cannot operate on plaintext ESP payloads or build per-segment ESP data.
+ */
+netdev_features_t airoha_xfrm_fix_features(struct net_device *dev,
+ netdev_features_t features)
+{
+ netdev_features_t supported = airoha_xfrm_dev_features(dev);
+ netdev_features_t unsupported = AIROHA_XFRM_FEATURES & ~supported;
+
+ if (features & unsupported)
+ features &= ~unsupported;
+
+ if (!(features & NETIF_F_HW_ESP))
+ features &= ~(NETIF_F_HW_ESP_TX_CSUM | NETIF_F_GSO_ESP);
+
+ return features;
+}
+
+int airoha_xfrm_set_features(struct net_device *dev, netdev_features_t features)
+{
+ netdev_features_t changed = (dev->features ^ features) &
+ AIROHA_XFRM_FEATURES;
+ netdev_features_t requested = features & AIROHA_XFRM_FEATURES;
+ struct airoha_gdm_port *port = netdev_priv(dev);
+ netdev_features_t supported;
+ int err;
+
+ if (!changed)
+ return 0;
+
+ if (requested & NETIF_F_HW_ESP) {
+ err = airoha_xfrm_prepare_ipsec(dev);
+ if (err)
+ return err;
+ }
+
+ supported = airoha_xfrm_dev_features(dev);
+ if (requested & ~supported)
+ return -EOPNOTSUPP;
+
+ if (atomic_read(&port->xfrm_state_count)) {
+ netdev_err(dev, "cannot change ESP features with active SAs\n");
+ return -EBUSY;
+ }
+
+ if (!(features & NETIF_F_HW_ESP))
+ netdev_info(dev, "ESP HW offload disabled\n");
+
+ return 0;
+}
+
+struct airoha_xfrm_rx_info {
+ unsigned short family;
+ int encap_type;
+ int esp_offset;
+ int packet_len;
+ __be32 spi;
+ __be32 seq;
+};
+
+struct airoha_xfrm_rx_ctx {
+ struct sk_buff *skb;
+ struct net_device *dev;
+};
+
+static bool airoha_xfrm_parse_rx_ipv4(struct sk_buff *skb,
+ struct airoha_xfrm_rx_info *info)
+{
+ struct ip_esp_hdr *esph;
+ struct iphdr *iph;
+ int packet_len;
+ int iphlen;
+
+ if (!pskb_may_pull(skb, sizeof(*iph)))
+ return false;
+
+ iph = ip_hdr(skb);
+ if (iph->version != 4)
+ return false;
+
+ iphlen = iph->ihl * 4;
+ if (iphlen < sizeof(*iph) || !pskb_may_pull(skb, iphlen))
+ return false;
+
+ if (ip_is_fragment(iph))
+ return false;
+
+ packet_len = ntohs(iph->tot_len);
+ if (packet_len < iphlen || packet_len > skb->len)
+ return false;
+
+ switch (iph->protocol) {
+ case IPPROTO_ESP:
+ info->encap_type = 0;
+ info->esp_offset = iphlen;
+ info->packet_len = packet_len;
+ break;
+ case IPPROTO_UDP: {
+ struct udphdr *uh;
+ int udp_len;
+ __be32 marker;
+
+ if (!pskb_may_pull(skb, iphlen + sizeof(*uh) + sizeof(*esph)))
+ return false;
+
+ uh = (struct udphdr *)(skb->data + iphlen);
+ udp_len = ntohs(uh->len);
+ if (udp_len <= sizeof(*uh) + sizeof(*esph) ||
+ iphlen + udp_len > packet_len)
+ return false;
+
+ memcpy(&marker, skb->data + iphlen + sizeof(*uh),
+ sizeof(marker));
+ if (!marker)
+ return false;
+
+ info->encap_type = UDP_ENCAP_ESPINUDP;
+ info->esp_offset = iphlen + sizeof(*uh);
+ info->packet_len = iphlen + udp_len;
+ break;
+ }
+ default:
+ return false;
+ }
+
+ if (info->esp_offset + sizeof(*esph) > info->packet_len ||
+ !pskb_may_pull(skb, info->esp_offset + sizeof(*esph)))
+ return false;
+
+ esph = (struct ip_esp_hdr *)(skb->data + info->esp_offset);
+ info->family = AF_INET;
+ info->spi = esph->spi;
+ info->seq = esph->seq_no;
+
+ return !!info->spi;
+}
+
+#if IS_ENABLED(CONFIG_IPV6)
+static bool airoha_xfrm_parse_rx_ipv6(struct sk_buff *skb,
+ struct airoha_xfrm_rx_info *info)
+{
+ struct ip_esp_hdr *esph;
+ struct ipv6hdr *ip6h;
+ __be16 frag_off;
+ int packet_len;
+ int offset;
+ u8 nexthdr;
+
+ if (!pskb_may_pull(skb, sizeof(*ip6h)))
+ return false;
+
+ ip6h = ipv6_hdr(skb);
+ if (ip6h->version != 6)
+ return false;
+
+ if (!ip6h->payload_len)
+ return false;
+
+ packet_len = sizeof(*ip6h) + ntohs(ip6h->payload_len);
+ if (packet_len < sizeof(*ip6h) || packet_len > skb->len)
+ return false;
+
+ nexthdr = ip6h->nexthdr;
+ offset = ipv6_skip_exthdr(skb, sizeof(*ip6h), &nexthdr, &frag_off);
+ if (offset < 0 || frag_off)
+ return false;
+
+ switch (nexthdr) {
+ case NEXTHDR_ESP:
+ info->encap_type = 0;
+ info->esp_offset = offset;
+ info->packet_len = packet_len;
+ break;
+ case NEXTHDR_UDP: {
+ struct udphdr *uh;
+ int udp_len;
+ __be32 marker;
+
+ if (!pskb_may_pull(skb, offset + sizeof(*uh) + sizeof(*esph)))
+ return false;
+
+ uh = (struct udphdr *)(skb->data + offset);
+ udp_len = ntohs(uh->len);
+ if (udp_len <= sizeof(*uh) + sizeof(*esph) ||
+ offset + udp_len > packet_len)
+ return false;
+
+ memcpy(&marker, skb->data + offset + sizeof(*uh),
+ sizeof(marker));
+ if (!marker)
+ return false;
+
+ info->encap_type = UDP_ENCAP_ESPINUDP;
+ info->esp_offset = offset + sizeof(*uh);
+ info->packet_len = offset + udp_len;
+ break;
+ }
+ default:
+ return false;
+ }
+
+ if (info->esp_offset + sizeof(*esph) > info->packet_len ||
+ !pskb_may_pull(skb, info->esp_offset + sizeof(*esph)))
+ return false;
+
+ esph = (struct ip_esp_hdr *)(skb->data + info->esp_offset);
+ info->family = AF_INET6;
+ info->spi = esph->spi;
+ info->seq = esph->seq_no;
+
+ return !!info->spi;
+}
+#else
+static bool airoha_xfrm_parse_rx_ipv6(struct sk_buff *skb,
+ struct airoha_xfrm_rx_info *info)
+{
+ return false;
+}
+#endif
+
+static bool airoha_xfrm_parse_rx_skb(struct sk_buff *skb,
+ struct airoha_xfrm_rx_info *info)
+{
+ switch (skb->protocol) {
+ case htons(ETH_P_IP):
+ return airoha_xfrm_parse_rx_ipv4(skb, info);
+ case htons(ETH_P_IPV6):
+ return airoha_xfrm_parse_rx_ipv6(skb, info);
+ default:
+ return false;
+ }
+}
+
+static struct xfrm_state *
+airoha_xfrm_rx_state_lookup(struct airoha_gdm_port *port, struct sk_buff *skb,
+ const struct airoha_xfrm_rx_info *info)
+{
+ struct airoha_xfrm_state *state;
+ xfrm_address_t daddr = {};
+ struct net_device *dev;
+ struct xfrm_state *x;
+
+ dev = airoha_xfrm_rx_dev(port, skb);
+ if (!dev)
+ return NULL;
+
+ switch (info->family) {
+ case AF_INET:
+ daddr.a4 = ip_hdr(skb)->daddr;
+ break;
+ case AF_INET6:
+ daddr.in6 = ipv6_hdr(skb)->daddr;
+ break;
+ default:
+ return NULL;
+ }
+
+ x = xfrm_input_state_lookup(dev_net(dev), skb->mark, &daddr, info->spi,
+ IPPROTO_ESP, info->family);
+ if (!x)
+ return NULL;
+
+ if (x->dir && x->dir != XFRM_SA_DIR_IN)
+ goto err_put;
+
+ if (x->xso.dir != XFRM_DEV_OFFLOAD_IN ||
+ x->xso.type != XFRM_DEV_OFFLOAD_CRYPTO || x->xso.dev != dev ||
+ !(dev->features & NETIF_F_HW_ESP) || !x->type_offload ||
+ !x->type_offload->input_tail)
+ goto err_put;
+
+ state = (struct airoha_xfrm_state *)x->xso.offload_handle;
+ if (!state || state->port != port)
+ goto err_put;
+
+ if ((x->encap ? x->encap->encap_type : 0) != info->encap_type)
+ goto err_put;
+
+ return x;
+
+err_put:
+ xfrm_state_put(x);
+ return NULL;
+}
+
+static u32 airoha_xfrm_rx_status(int err, struct xfrm_state *x)
+{
+ if (!err)
+ return CRYPTO_SUCCESS;
+
+ if (err == -EBADMSG) {
+ if (x->props.mode == XFRM_MODE_TUNNEL)
+ return CRYPTO_TUNNEL_ESP_AUTH_FAILED;
+
+ return CRYPTO_TRANSPORT_ESP_AUTH_FAILED;
+ }
+
+ if (err == -EINVAL)
+ return CRYPTO_INVALID_PACKET_SYNTAX;
+
+ return CRYPTO_GENERIC_ERROR;
+}
+
+static int airoha_xfrm_rx_apply_result(struct sk_buff *skb,
+ struct xfrm_state *x,
+ struct eip93_ipsec_result result)
+{
+ struct xfrm_offload *xo = xfrm_offload(skb);
+
+ if (!x || !result.packet_len || result.packet_len > skb->len || !xo)
+ return -EINVAL;
+
+ /*
+ * EIP93 inbound ESP mode removes the ESP pad/trailer/ICV and reports
+ * the decapsulated outer packet length plus the recovered next-header.
+ */
+ xo->proto = result.nexthdr;
+ xo->flags |= XFRM_ESP_NO_TRAILER;
+ if (pskb_trim(skb, result.packet_len))
+ return -EINVAL;
+
+ if (x->props.family == AF_INET) {
+ ip_hdr(skb)->tot_len = htons(skb->len);
+ ip_send_check(ip_hdr(skb));
+ } else if (x->props.family == AF_INET6) {
+ int len = skb->len - skb_network_offset(skb) -
+ sizeof(struct ipv6hdr);
+
+ if (len < 0)
+ return -EINVAL;
+
+ ipv6_hdr(skb)->payload_len = len > IPV6_MAXPLEN ? 0 :
+ htons(len);
+ }
+
+ return 0;
+}
+
+static void airoha_xfrm_rx_free_ctx(struct airoha_xfrm_rx_ctx *ctx)
+{
+ kfree(ctx);
+}
+
+static void airoha_xfrm_rx_finish(void *data, int err,
+ struct eip93_ipsec_result result)
+{
+ struct airoha_xfrm_rx_ctx *ctx = data;
+ struct net_device *dev = ctx->dev;
+ struct sk_buff *skb = ctx->skb;
+ struct xfrm_offload *xo;
+ struct xfrm_state *x;
+
+ x = xfrm_input_state(skb);
+ xo = xfrm_offload(skb);
+ if (!err)
+ err = airoha_xfrm_rx_apply_result(skb, x, result);
+ if (xo) {
+ xo->flags |= CRYPTO_DONE;
+ xo->status = airoha_xfrm_rx_status(err, x);
+ }
+
+ airoha_xfrm_rx_free_ctx(ctx);
+ netif_receive_skb(skb);
+ dev_put(dev);
+}
+
+static bool airoha_xfrm_tx_esp_offset(struct sk_buff *skb, struct xfrm_state *x,
+ unsigned int *esp_offset)
+{
+ u8 *esph = (u8 *)ip_esp_hdr(skb);
+
+ if (x->encap)
+ esph += sizeof(struct udphdr);
+
+ if (esph < skb->data ||
+ esph + sizeof(struct ip_esp_hdr) > skb_tail_pointer(skb))
+ return false;
+
+ *esp_offset = esph - skb->data;
+
+ return true;
+}
+
+static void airoha_xfrm_tx_update_outer_len(struct sk_buff *skb)
+{
+ struct iphdr *iph = ip_hdr(skb);
+
+ if (iph->version == 4) {
+ iph->tot_len = htons(skb->len - skb_network_offset(skb));
+ ip_send_check(iph);
+ } else if (iph->version == 6) {
+ int len = skb->len - skb_network_offset(skb) -
+ sizeof(struct ipv6hdr);
+
+ if (len < 0)
+ return;
+
+ ipv6_hdr(skb)->payload_len = len > IPV6_MAXPLEN ? 0 :
+ htons(len);
+ }
+}
+
+static void airoha_xfrm_tx_udp6_csum(struct sk_buff *skb,
+ struct xfrm_state *x)
+{
+#if IS_ENABLED(CONFIG_IPV6)
+ struct udphdr *uh;
+ struct ipv6hdr *ip6h;
+ unsigned int offset;
+ __wsum csum;
+ int len;
+
+ if (x->props.family != AF_INET6 || !x->encap ||
+ x->encap->encap_type != UDP_ENCAP_ESPINUDP)
+ return;
+
+ offset = skb_transport_offset(skb);
+ if (offset + sizeof(*uh) > skb->len)
+ return;
+
+ uh = udp_hdr(skb);
+ ip6h = ipv6_hdr(skb);
+ len = ntohs(uh->len);
+ if (len < sizeof(*uh) || len > skb->len - offset)
+ return;
+
+ uh->check = 0;
+ csum = skb_checksum(skb, offset, len, 0);
+ uh->check = csum_ipv6_magic(&ip6h->saddr, &ip6h->daddr, len,
+ IPPROTO_UDP, csum);
+ if (!uh->check)
+ uh->check = CSUM_MANGLED_0;
+ #endif
+}
+
+static int airoha_xfrm_tx_apply_result(struct sk_buff *skb,
+ struct xfrm_state *x,
+ struct eip93_ipsec_result result)
+{
+ unsigned int current_esp_len;
+ unsigned int esp_offset;
+ unsigned int new_len;
+
+ if (!result.packet_len ||
+ !airoha_xfrm_tx_esp_offset(skb, x, &esp_offset))
+ return -EINVAL;
+
+ current_esp_len = skb->len - esp_offset;
+ if (result.packet_len == current_esp_len)
+ return 0;
+
+ new_len = esp_offset + result.packet_len;
+ if (new_len < esp_offset)
+ return -EINVAL;
+
+ /*
+ * EIP93 outbound ESP mode reports the generated ESP packet length.
+ * Reflect it in skb->len before the packet resumes into the Ethernet
+ * TX path, because generic ESP left hardware-generated trailer bytes
+ * outside skb->len.
+ */
+ if (new_len > skb->len) {
+ unsigned int delta = new_len - skb->len;
+
+ if (delta > skb_tailroom(skb))
+ return -ENOMEM;
+ skb_put(skb, delta);
+
+ return 0;
+ }
+
+ return pskb_trim(skb, new_len);
+}
+
+bool airoha_xfrm_rx_skb(struct airoha_gdm_port *port, struct sk_buff *skb)
+{
+ struct airoha_xfrm_rx_info info;
+ struct airoha_xfrm_state *state;
+ struct airoha_xfrm_rx_ctx *ctx;
+ struct sk_buff *trailer;
+ struct xfrm_offload *xo;
+ struct xfrm_state *x;
+ struct sec_path *sp;
+ int err;
+ u32 mark = skb->mark;
+
+ if (!airoha_xfrm_parse_rx_skb(skb, &info))
+ return false;
+
+ x = airoha_xfrm_rx_state_lookup(port, skb, &info);
+ if (!x)
+ return false;
+
+ sp = secpath_set(skb);
+ if (!sp)
+ goto err_put_state;
+
+ if (sp->len == XFRM_MAX_DEPTH) {
+ secpath_reset(skb);
+ goto err_put_state;
+ }
+
+ skb->mark = xfrm_smark_get(mark, x);
+ sp->xvec[sp->len++] = x;
+ sp->olen++;
+ XFRM_SKB_CB(skb)->seq.input.low = info.seq;
+ XFRM_SKB_CB(skb)->seq.input.hi = htonl(xfrm_replay_seqhi(x, info.seq));
+ XFRM_SPI_SKB_CB(skb)->family = info.family;
+ XFRM_SPI_SKB_CB(skb)->seq = info.seq;
+ if (info.family == AF_INET) {
+ XFRM_SPI_SKB_CB(skb)->daddroff = offsetof(struct iphdr, daddr);
+ XFRM_TUNNEL_SKB_CB(skb)->tunnel.ip4 = NULL;
+ } else {
+ XFRM_SPI_SKB_CB(skb)->daddroff =
+ offsetof(struct ipv6hdr, daddr);
+ XFRM_TUNNEL_SKB_CB(skb)->tunnel.ip6 = NULL;
+ }
+
+ xo = xfrm_offload(skb);
+ if (!xo)
+ goto err_reset;
+
+ state = (struct airoha_xfrm_state *)x->xso.offload_handle;
+ if (!state || state->port != port)
+ goto err_reset;
+
+ if (skb_cloned(skb) || skb_is_nonlinear(skb)) {
+ err = skb_cow_data(skb, 0, &trailer);
+ if (err < 0)
+ goto err_reset;
+
+ if (skb_is_nonlinear(skb)) {
+ err = skb_linearize(skb);
+ if (err)
+ goto err_reset;
+ }
+ }
+
+ ctx = kmalloc(sizeof(*ctx), GFP_ATOMIC);
+ if (!ctx)
+ goto err_reset;
+
+ if (!skb->dev)
+ goto err_free_ctx;
+
+ ctx->skb = skb;
+ ctx->dev = skb->dev;
+ skb->ip_summed = CHECKSUM_NONE;
+
+ dev_hold(ctx->dev);
+ err = eip93_ipsec_receive(state->sa, skb, info.packet_len,
+ airoha_xfrm_rx_finish, ctx);
+ if (err == -EINPROGRESS)
+ return true;
+
+ dev_put(ctx->dev);
+ airoha_xfrm_rx_free_ctx(ctx);
+ skb->mark = mark;
+ secpath_reset(skb);
+
+ return false;
+
+err_free_ctx:
+ airoha_xfrm_rx_free_ctx(ctx);
+err_reset:
+ skb->mark = mark;
+ secpath_reset(skb);
+ return false;
+
+err_put_state:
+ xfrm_state_put(x);
+ return false;
+}
+
+static void airoha_xfrm_tx_done(void *data, int err,
+ struct eip93_ipsec_result result)
+{
+ struct sk_buff *skb = data;
+ struct xfrm_offload *xo = xfrm_offload(skb);
+ struct sec_path *sp = skb_sec_path(skb);
+ struct xfrm_state *x;
+
+ if (!xo || !sp || !sp->len) {
+ kfree_skb(skb);
+ return;
+ }
+
+ x = sp->xvec[sp->len - 1];
+ if (!err)
+ err = airoha_xfrm_tx_apply_result(skb, x, result);
+ if (err) {
+ XFRM_INC_STATS(xs_net(x), LINUX_MIB_XFRMOUTSTATEPROTOERROR);
+ kfree_skb(skb);
+ return;
+ }
+
+ airoha_xfrm_tx_update_outer_len(skb);
+ airoha_xfrm_tx_udp6_csum(skb, x);
+ xo->flags |= CRYPTO_DONE;
+ xo->status = CRYPTO_SUCCESS;
+ skb_push(skb, skb->data - skb_mac_header(skb));
+ secpath_reset(skb);
+ xfrm_dev_resume(skb);
+}
+
+int airoha_xfrm_encrypt_skb(struct airoha_gdm_port *port, struct sk_buff *skb)
+{
+ struct xfrm_offload *xo = xfrm_offload(skb);
+ struct airoha_xfrm_state *state;
+ struct net_device *dev;
+ struct xfrm_state *x;
+ struct sec_path *sp;
+ struct ip_esp_hdr *esph;
+ struct sk_buff *trailer;
+ unsigned int esp_offset;
+ unsigned int tailen;
+ int err;
+
+ if (!xo || !(xo->flags & XFRM_XMIT) || (xo->flags & CRYPTO_DONE))
+ return 0;
+
+ sp = skb_sec_path(skb);
+ if (!sp || !sp->len)
+ return -EINVAL;
+
+ x = sp->xvec[sp->len - 1];
+ dev = x->xso.dev;
+ if (unlikely(x->xso.dir != XFRM_DEV_OFFLOAD_OUT ||
+ x->xso.type != XFRM_DEV_OFFLOAD_CRYPTO || !dev ||
+ !(dev->features & NETIF_F_HW_ESP)))
+ return -EOPNOTSUPP;
+
+ state = (struct airoha_xfrm_state *)x->xso.offload_handle;
+ if (!state || state->port != port)
+ return -EOPNOTSUPP;
+
+ if (unlikely(skb_is_gso(skb)))
+ return -EOPNOTSUPP;
+
+ if (unlikely(skb->ip_summed == CHECKSUM_PARTIAL)) {
+ err = skb_checksum_help(skb);
+ if (err)
+ return err;
+ }
+
+ tailen = xo->esp_tx_tailen;
+ if (skb_cloned(skb) || skb_is_nonlinear(skb)) {
+ err = skb_cow_data(skb, tailen, &trailer);
+ if (err < 0)
+ return err;
+
+ if (skb_is_nonlinear(skb)) {
+ err = skb_linearize(skb);
+ if (err)
+ return err;
+ }
+ }
+ /*
+ * Generic ESP reserves this tailroom before the skb reaches us. Keep a
+ * small guard here because COW/linearization can replace the skb head.
+ */
+ if (tailen && skb_tailroom(skb) < tailen) {
+ err = pskb_expand_head(skb, 0, tailen - skb_tailroom(skb),
+ GFP_ATOMIC);
+ if (err)
+ return err;
+ }
+
+ if (!airoha_xfrm_tx_esp_offset(skb, x, &esp_offset))
+ return -EINVAL;
+
+ esph = (struct ip_esp_hdr *)(skb->data + esp_offset);
+ esph->seq_no = htonl(xo->seq.low);
+
+ return eip93_ipsec_xmit(state->sa, skb, esp_offset, airoha_xfrm_tx_done,
+ skb);
+}
+
+static void airoha_xfrm_flush_dev(struct net_device *dev)
+{
+ xfrm_dev_state_flush(dev_net(dev), dev, true);
+ xfrm_dev_policy_flush(dev_net(dev), dev, true);
+}
+
+static void airoha_xfrm_link_change(struct net_device *dev)
+{
+ struct airoha_gdm_port *port = airoha_xfrm_dev_port(dev);
+
+ if (!port || !(dev->hw_features & NETIF_F_HW_ESP) ||
+ !atomic_read(&port->xfrm_state_count))
+ return;
+
+ netdev_dbg(dev, "carrier %s, preserving ESP HW offload SAs\n",
+ netif_carrier_ok(dev) ? "up" : "down");
+}
+
+#if IS_ENABLED(CONFIG_NET_DSA)
+static void airoha_xfrm_dsa_attach_user(struct net_device *conduit,
+ struct net_device *user)
+{
+ netdev_features_t features = airoha_xfrm_dev_features(conduit);
+
+ if (conduit->xfrmdev_ops != &airoha_xfrmdev_ops ||
+ !airoha_xfrm_dsa_user_matches_port(user, conduit))
+ return;
+
+ if (!(features & NETIF_F_HW_ESP))
+ return;
+
+ if (user->xfrmdev_ops && user->xfrmdev_ops != &airoha_xfrmdev_ops) {
+ netdev_dbg(conduit,
+ "DSA user %s already has XFRM offload ops\n",
+ user->name);
+ return;
+ }
+
+ user->xfrmdev_ops = &airoha_xfrmdev_ops;
+ user->hw_features |= features;
+ user->hw_enc_features |= features;
+ user->gso_partial_features |= features & NETIF_F_GSO_ESP;
+ netdev_dbg(user, "ESP HW offload available via %s\n", conduit->name);
+}
+
+static void airoha_xfrm_dsa_detach_user(struct net_device *user)
+{
+ struct airoha_gdm_port *port;
+ bool active = false;
+ bool enabled;
+
+ if (user->xfrmdev_ops != &airoha_xfrmdev_ops ||
+ !dsa_user_dev_check(user))
+ return;
+
+ enabled = user->features & NETIF_F_HW_ESP;
+ port = airoha_xfrm_dsa_dev_port(user);
+ if (port)
+ active = atomic_read(&port->xfrm_state_count);
+
+ if (active) {
+ netdev_warn(user, "DSA detach with active ESP SAs, flushing\n");
+ airoha_xfrm_flush_dev(user);
+ }
+
+ user->wanted_features &= ~AIROHA_XFRM_FEATURES;
+ user->features &= ~AIROHA_XFRM_FEATURES;
+ user->hw_features &= ~AIROHA_XFRM_FEATURES;
+ user->hw_enc_features &= ~AIROHA_XFRM_FEATURES;
+ user->gso_partial_features &= ~NETIF_F_GSO_ESP;
+ user->xfrmdev_ops = NULL;
+
+ if (active || enabled)
+ netdev_features_change(user);
+}
+
+static void airoha_xfrm_dsa_feature_change(struct net_device *dev)
+{
+ struct airoha_gdm_port *port;
+
+ if (dev->xfrmdev_ops != &airoha_xfrmdev_ops ||
+ !dsa_user_dev_check(dev) || (dev->features & NETIF_F_HW_ESP))
+ return;
+
+ port = airoha_xfrm_dsa_dev_port(dev);
+ if (port && atomic_read(&port->xfrm_state_count)) {
+ netdev_warn(dev, "DSA feature lost ESP SAs, flushing\n");
+ airoha_xfrm_flush_dev(dev);
+ }
+}
+#endif
+
+static int airoha_xfrm_netdevice_event(struct notifier_block *nb,
+ unsigned long event, void *ptr)
+{
+ struct net_device *dev = netdev_notifier_info_to_dev(ptr);
+
+ switch (event) {
+ case NETDEV_CHANGE:
+ airoha_xfrm_link_change(dev);
+ break;
+#if IS_ENABLED(CONFIG_NET_DSA)
+ case NETDEV_CHANGEUPPER: {
+ struct netdev_notifier_changeupper_info *info = ptr;
+
+ if (info->linking)
+ airoha_xfrm_dsa_attach_user(dev, info->upper_dev);
+ else
+ airoha_xfrm_dsa_detach_user(info->upper_dev);
+ break;
+ }
+ case NETDEV_FEAT_CHANGE:
+ airoha_xfrm_dsa_feature_change(dev);
+ break;
+ case NETDEV_UNREGISTER:
+ airoha_xfrm_dsa_detach_user(dev);
+ break;
+#endif
+ default:
+ break;
+ }
+
+ return NOTIFY_DONE;
+}
+
+static struct notifier_block airoha_xfrm_netdev_notifier = {
+ .notifier_call = airoha_xfrm_netdevice_event,
+};
+
+static int airoha_xfrm_register_netdev_notifier(void)
+{
+ return register_netdevice_notifier(&airoha_xfrm_netdev_notifier);
+}
+
+static void airoha_xfrm_unregister_netdev_notifier(void)
+{
+ unregister_netdevice_notifier(&airoha_xfrm_netdev_notifier);
+}
+
+static void airoha_xfrm_drop_dev(struct net_device *dev, const char *reason)
+{
+ struct airoha_gdm_port *port = airoha_xfrm_dev_port(dev);
+ bool advertised = dev->hw_features & AIROHA_XFRM_FEATURES;
+ bool enabled = dev->features & NETIF_F_HW_ESP;
+ bool active = false;
+
+ if (port)
+ active = atomic_read(&port->xfrm_state_count);
+
+ if (active) {
+ netdev_warn(dev, "%s, flushing ESP HW offload SAs\n", reason);
+ airoha_xfrm_flush_dev(dev);
+ }
+
+ dev->wanted_features &= ~AIROHA_XFRM_FEATURES;
+ dev->features &= ~AIROHA_XFRM_FEATURES;
+ dev->hw_features &= ~AIROHA_XFRM_FEATURES;
+ dev->hw_enc_features &= ~AIROHA_XFRM_FEATURES;
+ dev->gso_partial_features &= ~NETIF_F_GSO_ESP;
+
+ if (active || enabled || advertised)
+ netdev_features_change(dev);
+}
+
+static void airoha_xfrm_drop_ipsec(struct eip93_ipsec *ipsec,
+ const char *reason)
+{
+ struct net_device *dev;
+ struct net *net;
+
+ rtnl_lock();
+ for_each_net(net) {
+ for_each_netdev(net, dev) {
+ struct airoha_gdm_port *port;
+
+ port = airoha_xfrm_dev_port(dev);
+ if (!port || port->xfrm_ipsec != ipsec)
+ continue;
+
+ airoha_xfrm_drop_dev(dev, reason);
+ }
+ }
+
+ for_each_net(net) {
+ for_each_netdev(net, dev) {
+ struct airoha_gdm_port *port;
+
+ if (dev->xfrmdev_ops != &airoha_xfrmdev_ops)
+ continue;
+
+ if (airoha_xfrm_dsa_dev_port(dev))
+ continue;
+
+ port = netdev_priv(dev);
+ if (dev == port->dev && port->xfrm_ipsec == ipsec) {
+ eip93_ipsec_put(port->xfrm_ipsec);
+ port->xfrm_ipsec = NULL;
+ }
+ }
+ }
+
+ for_each_net(net) {
+ for_each_netdev(net, dev) {
+ if (dev->xfrmdev_ops == &airoha_xfrmdev_ops &&
+ !(dev->hw_features & NETIF_F_HW_ESP))
+ dev->xfrmdev_ops = NULL;
+ }
+ }
+ rtnl_unlock();
+}
+
+static int airoha_xfrm_ipsec_event(struct notifier_block *nb,
+ unsigned long event, void *ptr)
+{
+ switch (event) {
+ case EIP93_IPSEC_EVENT_REMOVE:
+ airoha_xfrm_drop_ipsec(ptr, "EIP93 provider removed");
+ break;
+ case EIP93_IPSEC_EVENT_RESET:
+ airoha_xfrm_drop_ipsec(ptr, "EIP93 provider reset");
+ break;
+ case EIP93_IPSEC_EVENT_DMA_ERROR:
+ airoha_xfrm_drop_ipsec(ptr, "EIP93 DMA error");
+ break;
+ case EIP93_IPSEC_EVENT_CAPABILITY_LOSS:
+ airoha_xfrm_drop_ipsec(ptr, "EIP93 capability loss");
+ break;
+ default:
+ break;
+ }
+
+ return NOTIFY_DONE;
+}
+
+static struct notifier_block airoha_xfrm_ipsec_notifier = {
+ .notifier_call = airoha_xfrm_ipsec_event,
+};
+
+int airoha_xfrm_register_notifier(void)
+{
+ int err;
+
+ err = airoha_xfrm_register_netdev_notifier();
+ if (err)
+ return err;
+
+ err = eip93_ipsec_register_notifier(&airoha_xfrm_ipsec_notifier);
+ if (err)
+ airoha_xfrm_unregister_netdev_notifier();
+
+ return err;
+}
+
+void airoha_xfrm_unregister_notifier(void)
+{
+ eip93_ipsec_unregister_notifier(&airoha_xfrm_ipsec_notifier);
+ airoha_xfrm_unregister_netdev_notifier();
+}
+#else
+void airoha_xfrm_build_netdev(struct net_device *dev)
+{
+}
+
+void airoha_xfrm_teardown_netdev(struct net_device *dev)
+{
+}
+
+netdev_features_t airoha_xfrm_fix_features(struct net_device *dev,
+ netdev_features_t features)
+{
+ return features & ~(NETIF_F_HW_ESP_TX_CSUM | NETIF_F_GSO_ESP);
+}
+
+int airoha_xfrm_set_features(struct net_device *dev, netdev_features_t features)
+{
+ return 0;
+}
+
+bool airoha_xfrm_rx_skb(struct airoha_gdm_port *port, struct sk_buff *skb)
+{
+ return false;
+}
+
+int airoha_xfrm_encrypt_skb(struct airoha_gdm_port *port, struct sk_buff *skb)
+{
+ return 0;
+}
+
+int airoha_xfrm_register_notifier(void)
+{
+ return 0;
+}
+
+void airoha_xfrm_unregister_notifier(void)
+{
+}
+
+#endif
--
2.53.0
More information about the linux-arm-kernel
mailing list