// SPDX-License-Identifier: GPL-2.0-only /**************************************************************************** * Driver for Solarflare network controllers and boards * Copyright 2023, Advanced Micro Devices, Inc. * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 as published * by the Free Software Foundation, incorporated herein by reference. */ #include "tc_conntrack.h" #include "tc.h" #include "mae.h" static int efx_tc_flow_block(enum tc_setup_type type, void *type_data, void *cb_priv); static const struct rhashtable_params efx_tc_ct_zone_ht_params = { .key_len = offsetof(struct efx_tc_ct_zone, linkage), .key_offset = 0, .head_offset = offsetof(struct efx_tc_ct_zone, linkage), }; static const struct rhashtable_params efx_tc_ct_ht_params = { .key_len = offsetof(struct efx_tc_ct_entry, linkage), .key_offset = 0, .head_offset = offsetof(struct efx_tc_ct_entry, linkage), }; static void efx_tc_ct_zone_free(void *ptr, void *arg) { struct efx_tc_ct_zone *zone = ptr; struct efx_nic *efx = zone->efx; netif_err(efx, drv, efx->net_dev, "tc ct_zone %u still present at teardown, removing\n", zone->zone); nf_flow_table_offload_del_cb(zone->nf_ft, efx_tc_flow_block, zone); kfree(zone); } static void efx_tc_ct_free(void *ptr, void *arg) { struct efx_tc_ct_entry *conn = ptr; struct efx_nic *efx = arg; netif_err(efx, drv, efx->net_dev, "tc ct_entry %lx still present at teardown\n", conn->cookie); /* We can release the counter, but we can't remove the CT itself * from hardware because the table meta is already gone. */ efx_tc_flower_release_counter(efx, conn->cnt); kfree(conn); } int efx_tc_init_conntrack(struct efx_nic *efx) { int rc; rc = rhashtable_init(&efx->tc->ct_zone_ht, &efx_tc_ct_zone_ht_params); if (rc < 0) goto fail_ct_zone_ht; rc = rhashtable_init(&efx->tc->ct_ht, &efx_tc_ct_ht_params); if (rc < 0) goto fail_ct_ht; return 0; fail_ct_ht: rhashtable_destroy(&efx->tc->ct_zone_ht); fail_ct_zone_ht: return rc; } /* Only call this in init failure teardown. * Normal exit should fini instead as there may be entries in the table. */ void efx_tc_destroy_conntrack(struct efx_nic *efx) { rhashtable_destroy(&efx->tc->ct_ht); rhashtable_destroy(&efx->tc->ct_zone_ht); } void efx_tc_fini_conntrack(struct efx_nic *efx) { rhashtable_free_and_destroy(&efx->tc->ct_zone_ht, efx_tc_ct_zone_free, NULL); rhashtable_free_and_destroy(&efx->tc->ct_ht, efx_tc_ct_free, efx); } #define EFX_NF_TCP_FLAG(flg) cpu_to_be16(be32_to_cpu(TCP_FLAG_##flg) >> 16) static int efx_tc_ct_parse_match(struct efx_nic *efx, struct flow_rule *fr, struct efx_tc_ct_entry *conn) { struct flow_dissector *dissector = fr->match.dissector; unsigned char ipv = 0; bool tcp = false; if (flow_rule_match_key(fr, FLOW_DISSECTOR_KEY_CONTROL)) { struct flow_match_control fm; flow_rule_match_control(fr, &fm); if (IS_ALL_ONES(fm.mask->addr_type)) switch (fm.key->addr_type) { case FLOW_DISSECTOR_KEY_IPV4_ADDRS: ipv = 4; break; case FLOW_DISSECTOR_KEY_IPV6_ADDRS: ipv = 6; break; default: break; } } if (!ipv) { netif_dbg(efx, drv, efx->net_dev, "Conntrack missing ipv specification\n"); return -EOPNOTSUPP; } if (dissector->used_keys & ~(BIT_ULL(FLOW_DISSECTOR_KEY_CONTROL) | BIT_ULL(FLOW_DISSECTOR_KEY_BASIC) | BIT_ULL(FLOW_DISSECTOR_KEY_IPV4_ADDRS) | BIT_ULL(FLOW_DISSECTOR_KEY_IPV6_ADDRS) | BIT_ULL(FLOW_DISSECTOR_KEY_PORTS) | BIT_ULL(FLOW_DISSECTOR_KEY_TCP) | BIT_ULL(FLOW_DISSECTOR_KEY_META))) { netif_dbg(efx, drv, efx->net_dev, "Unsupported conntrack keys %#llx\n", dissector->used_keys); return -EOPNOTSUPP; } if (flow_rule_match_key(fr, FLOW_DISSECTOR_KEY_BASIC)) { struct flow_match_basic fm; flow_rule_match_basic(fr, &fm); if (!IS_ALL_ONES(fm.mask->n_proto)) { netif_dbg(efx, drv, efx->net_dev, "Conntrack eth_proto is not exact-match; mask %04x\n", ntohs(fm.mask->n_proto)); return -EOPNOTSUPP; } conn->eth_proto = fm.key->n_proto; if (conn->eth_proto != (ipv == 4 ? htons(ETH_P_IP) : htons(ETH_P_IPV6))) { netif_dbg(efx, drv, efx->net_dev, "Conntrack eth_proto is not IPv%u, is %04x\n", ipv, ntohs(conn->eth_proto)); return -EOPNOTSUPP; } if (!IS_ALL_ONES(fm.mask->ip_proto)) { netif_dbg(efx, drv, efx->net_dev, "Conntrack ip_proto is not exact-match; mask %02x\n", fm.mask->ip_proto); return -EOPNOTSUPP; } conn->ip_proto = fm.key->ip_proto; switch (conn->ip_proto) { case IPPROTO_TCP: tcp = true; break; case IPPROTO_UDP: break; default: netif_dbg(efx, drv, efx->net_dev, "Conntrack ip_proto not TCP or UDP, is %02x\n", conn->ip_proto); return -EOPNOTSUPP; } } else { netif_dbg(efx, drv, efx->net_dev, "Conntrack missing eth_proto, ip_proto\n"); return -EOPNOTSUPP; } if (ipv == 4 && flow_rule_match_key(fr, FLOW_DISSECTOR_KEY_IPV4_ADDRS)) { struct flow_match_ipv4_addrs fm; flow_rule_match_ipv4_addrs(fr, &fm); if (!IS_ALL_ONES(fm.mask->src)) { netif_dbg(efx, drv, efx->net_dev, "Conntrack ipv4.src is not exact-match; mask %08x\n", ntohl(fm.mask->src)); return -EOPNOTSUPP; } conn->src_ip = fm.key->src; if (!IS_ALL_ONES(fm.mask->dst)) { netif_dbg(efx, drv, efx->net_dev, "Conntrack ipv4.dst is not exact-match; mask %08x\n", ntohl(fm.mask->dst)); return -EOPNOTSUPP; } conn->dst_ip = fm.key->dst; } else if (ipv == 6 && flow_rule_match_key(fr, FLOW_DISSECTOR_KEY_IPV6_ADDRS)) { struct flow_match_ipv6_addrs fm; flow_rule_match_ipv6_addrs(fr, &fm); if (!efx_ipv6_addr_all_ones(&fm.mask->src)) { netif_dbg(efx, drv, efx->net_dev, "Conntrack ipv6.src is not exact-match; mask %pI6\n", &fm.mask->src); return -EOPNOTSUPP; } conn->src_ip6 = fm.key->src; if (!efx_ipv6_addr_all_ones(&fm.mask->dst)) { netif_dbg(efx, drv, efx->net_dev, "Conntrack ipv6.dst is not exact-match; mask %pI6\n", &fm.mask->dst); return -EOPNOTSUPP; } conn->dst_ip6 = fm.key->dst; } else { netif_dbg(efx, drv, efx->net_dev, "Conntrack missing IPv%u addrs\n", ipv); return -EOPNOTSUPP; } if (flow_rule_match_key(fr, FLOW_DISSECTOR_KEY_PORTS)) { struct flow_match_ports fm; flow_rule_match_ports(fr, &fm); if (!IS_ALL_ONES(fm.mask->src)) { netif_dbg(efx, drv, efx->net_dev, "Conntrack ports.src is not exact-match; mask %04x\n", ntohs(fm.mask->src)); return -EOPNOTSUPP; } conn->l4_sport = fm.key->src; if (!IS_ALL_ONES(fm.mask->dst)) { netif_dbg(efx, drv, efx->net_dev, "Conntrack ports.dst is not exact-match; mask %04x\n", ntohs(fm.mask->dst)); return -EOPNOTSUPP; } conn->l4_dport = fm.key->dst; } else { netif_dbg(efx, drv, efx->net_dev, "Conntrack missing L4 ports\n"); return -EOPNOTSUPP; } if (flow_rule_match_key(fr, FLOW_DISSECTOR_KEY_TCP)) { __be16 tcp_interesting_flags; struct flow_match_tcp fm; if (!tcp) { netif_dbg(efx, drv, efx->net_dev, "Conntrack matching on TCP keys but ipproto is not tcp\n"); return -EOPNOTSUPP; } flow_rule_match_tcp(fr, &fm); tcp_interesting_flags = EFX_NF_TCP_FLAG(SYN) | EFX_NF_TCP_FLAG(RST) | EFX_NF_TCP_FLAG(FIN); /* If any of the tcp_interesting_flags is set, we always * inhibit CT lookup in LHS (so SW can update CT table). */ if (fm.key->flags & tcp_interesting_flags) { netif_dbg(efx, drv, efx->net_dev, "Unsupported conntrack tcp.flags %04x/%04x\n", ntohs(fm.key->flags), ntohs(fm.mask->flags)); return -EOPNOTSUPP; } /* Other TCP flags cannot be filtered at CT */ if (fm.mask->flags & ~tcp_interesting_flags) { netif_dbg(efx, drv, efx->net_dev, "Unsupported conntrack tcp.flags %04x/%04x\n", ntohs(fm.key->flags), ntohs(fm.mask->flags)); return -EOPNOTSUPP; } } return 0; } /** * struct efx_tc_ct_mangler_state - tracks which fields have been pedited * * @ipv4: IP source or destination addr has been set * @tcpudp: TCP/UDP source or destination port has been set */ struct efx_tc_ct_mangler_state { u8 ipv4:1; u8 tcpudp:1; }; static int efx_tc_ct_mangle(struct efx_nic *efx, struct efx_tc_ct_entry *conn, const struct flow_action_entry *fa, struct efx_tc_ct_mangler_state *mung) { /* Is this the first mangle we've processed for this rule? */ bool first = !(mung->ipv4 || mung->tcpudp); bool dnat = false; switch (fa->mangle.htype) { case FLOW_ACT_MANGLE_HDR_TYPE_IP4: switch (fa->mangle.offset) { case offsetof(struct iphdr, daddr): dnat = true; fallthrough; case offsetof(struct iphdr, saddr): if (fa->mangle.mask) return -EOPNOTSUPP; conn->nat_ip = htonl(fa->mangle.val); mung->ipv4 = 1; break; default: return -EOPNOTSUPP; } break; case FLOW_ACT_MANGLE_HDR_TYPE_TCP: case FLOW_ACT_MANGLE_HDR_TYPE_UDP: /* Both struct tcphdr and struct udphdr start with * __be16 source; * __be16 dest; * so we can use the same code for both. */ switch (fa->mangle.offset) { case offsetof(struct tcphdr, dest): BUILD_BUG_ON(offsetof(struct tcphdr, dest) != offsetof(struct udphdr, dest)); dnat = true; fallthrough; case offsetof(struct tcphdr, source): BUILD_BUG_ON(offsetof(struct tcphdr, source) != offsetof(struct udphdr, source)); if (~fa->mangle.mask != 0xffff) return -EOPNOTSUPP; conn->l4_natport = htons(fa->mangle.val); mung->tcpudp = 1; break; default: return -EOPNOTSUPP; } break; default: return -EOPNOTSUPP; } /* first mangle tells us whether this is SNAT or DNAT; * subsequent mangles must match that */ if (first) conn->dnat = dnat; else if (conn->dnat != dnat) return -EOPNOTSUPP; return 0; } static int efx_tc_ct_replace(struct efx_tc_ct_zone *ct_zone, struct flow_cls_offload *tc) { struct flow_rule *fr = flow_cls_offload_flow_rule(tc); struct efx_tc_ct_mangler_state mung = {}; struct efx_tc_ct_entry *conn, *old; struct efx_nic *efx = ct_zone->efx; const struct flow_action_entry *fa; struct efx_tc_counter *cnt; int rc, i; if (WARN_ON(!efx->tc)) return -ENETDOWN; if (WARN_ON(!efx->tc->up)) return -ENETDOWN; conn = kzalloc(sizeof(*conn), GFP_USER); if (!conn) return -ENOMEM; conn->cookie = tc->cookie; old = rhashtable_lookup_get_insert_fast(&efx->tc->ct_ht, &conn->linkage, efx_tc_ct_ht_params); if (IS_ERR(old)) { rc = PTR_ERR(old); goto release; } else if (old) { netif_dbg(efx, drv, efx->net_dev, "Already offloaded conntrack (cookie %lx)\n", tc->cookie); rc = -EEXIST; goto release; } /* Parse match */ conn->zone = ct_zone; rc = efx_tc_ct_parse_match(efx, fr, conn); if (rc) goto release; /* Parse actions */ flow_action_for_each(i, fa, &fr->action) { switch (fa->id) { case FLOW_ACTION_CT_METADATA: conn->mark = fa->ct_metadata.mark; if (memchr_inv(fa->ct_metadata.labels, 0, sizeof(fa->ct_metadata.labels))) { netif_dbg(efx, drv, efx->net_dev, "Setting CT label not supported\n"); rc = -EOPNOTSUPP; goto release; } break; case FLOW_ACTION_MANGLE: if (conn->eth_proto != htons(ETH_P_IP)) { netif_dbg(efx, drv, efx->net_dev, "NAT only supported for IPv4\n"); rc = -EOPNOTSUPP; goto release; } rc = efx_tc_ct_mangle(efx, conn, fa, &mung); if (rc) goto release; break; default: netif_dbg(efx, drv, efx->net_dev, "Unhandled action %u for conntrack\n", fa->id); rc = -EOPNOTSUPP; goto release; } } /* fill in defaults for unmangled values */ if (!mung.ipv4) conn->nat_ip = conn->dnat ? conn->dst_ip : conn->src_ip; if (!mung.tcpudp) conn->l4_natport = conn->dnat ? conn->l4_dport : conn->l4_sport; cnt = efx_tc_flower_allocate_counter(efx, EFX_TC_COUNTER_TYPE_CT); if (IS_ERR(cnt)) { rc = PTR_ERR(cnt); goto release; } conn->cnt = cnt; rc = efx_mae_insert_ct(efx, conn); if (rc) { netif_dbg(efx, drv, efx->net_dev, "Failed to insert conntrack, %d\n", rc); goto release; } mutex_lock(&ct_zone->mutex); list_add_tail(&conn->list, &ct_zone->cts); mutex_unlock(&ct_zone->mutex); return 0; release: if (conn->cnt) efx_tc_flower_release_counter(efx, conn->cnt); if (!old) rhashtable_remove_fast(&efx->tc->ct_ht, &conn->linkage, efx_tc_ct_ht_params); kfree(conn); return rc; } /* Caller must follow with efx_tc_ct_remove_finish() after RCU grace period! */ static void efx_tc_ct_remove(struct efx_nic *efx, struct efx_tc_ct_entry *conn) { int rc; /* Remove it from HW */ rc = efx_mae_remove_ct(efx, conn); /* Delete it from SW */ rhashtable_remove_fast(&efx->tc->ct_ht, &conn->linkage, efx_tc_ct_ht_params); if (rc) { netif_err(efx, drv, efx->net_dev, "Failed to remove conntrack %lx from hw, rc %d\n", conn->cookie, rc); } else { netif_dbg(efx, drv, efx->net_dev, "Removed conntrack %lx\n", conn->cookie); } } static void efx_tc_ct_remove_finish(struct efx_nic *efx, struct efx_tc_ct_entry *conn) { /* Remove related CT counter. This is delayed after the conn object we * are working with has been successfully removed. This protects the * counter from being used-after-free inside efx_tc_ct_stats. */ efx_tc_flower_release_counter(efx, conn->cnt); kfree(conn); } static int efx_tc_ct_destroy(struct efx_tc_ct_zone *ct_zone, struct flow_cls_offload *tc) { struct efx_nic *efx = ct_zone->efx; struct efx_tc_ct_entry *conn; conn = rhashtable_lookup_fast(&efx->tc->ct_ht, &tc->cookie, efx_tc_ct_ht_params); if (!conn) { netif_warn(efx, drv, efx->net_dev, "Conntrack %lx not found to remove\n", tc->cookie); return -ENOENT; } mutex_lock(&ct_zone->mutex); list_del(&conn->list); efx_tc_ct_remove(efx, conn); mutex_unlock(&ct_zone->mutex); synchronize_rcu(); efx_tc_ct_remove_finish(efx, conn); return 0; } static int efx_tc_ct_stats(struct efx_tc_ct_zone *ct_zone, struct flow_cls_offload *tc) { struct efx_nic *efx = ct_zone->efx; struct efx_tc_ct_entry *conn; struct efx_tc_counter *cnt; rcu_read_lock(); conn = rhashtable_lookup_fast(&efx->tc->ct_ht, &tc->cookie, efx_tc_ct_ht_params); if (!conn) { netif_warn(efx, drv, efx->net_dev, "Conntrack %lx not found for stats\n", tc->cookie); rcu_read_unlock(); return -ENOENT; } cnt = conn->cnt; spin_lock_bh(&cnt->lock); /* Report only last use */ flow_stats_update(&tc->stats, 0, 0, 0, cnt->touched, FLOW_ACTION_HW_STATS_DELAYED); spin_unlock_bh(&cnt->lock); rcu_read_unlock(); return 0; } static int efx_tc_flow_block(enum tc_setup_type type, void *type_data, void *cb_priv) { struct flow_cls_offload *tcb = type_data; struct efx_tc_ct_zone *ct_zone = cb_priv; if (type != TC_SETUP_CLSFLOWER) return -EOPNOTSUPP; switch (tcb->command) { case FLOW_CLS_REPLACE: return efx_tc_ct_replace(ct_zone, tcb); case FLOW_CLS_DESTROY: return efx_tc_ct_destroy(ct_zone, tcb); case FLOW_CLS_STATS: return efx_tc_ct_stats(ct_zone, tcb); default: break; } return -EOPNOTSUPP; } struct efx_tc_ct_zone *efx_tc_ct_register_zone(struct efx_nic *efx, u16 zone, struct nf_flowtable *ct_ft) { struct efx_tc_ct_zone *ct_zone, *old; int rc; ct_zone = kzalloc(sizeof(*ct_zone), GFP_USER); if (!ct_zone) return ERR_PTR(-ENOMEM); ct_zone->zone = zone; old = rhashtable_lookup_get_insert_fast(&efx->tc->ct_zone_ht, &ct_zone->linkage, efx_tc_ct_zone_ht_params); if (old) { /* don't need our new entry */ kfree(ct_zone); if (IS_ERR(old)) /* oh dear, it's actually an error */ return ERR_CAST(old); if (!refcount_inc_not_zero(&old->ref)) return ERR_PTR(-EAGAIN); /* existing entry found */ WARN_ON_ONCE(old->nf_ft != ct_ft); netif_dbg(efx, drv, efx->net_dev, "Found existing ct_zone for %u\n", zone); return old; } ct_zone->nf_ft = ct_ft; ct_zone->efx = efx; INIT_LIST_HEAD(&ct_zone->cts); mutex_init(&ct_zone->mutex); rc = nf_flow_table_offload_add_cb(ct_ft, efx_tc_flow_block, ct_zone); netif_dbg(efx, drv, efx->net_dev, "Adding new ct_zone for %u, rc %d\n", zone, rc); if (rc < 0) goto fail; refcount_set(&ct_zone->ref, 1); return ct_zone; fail: rhashtable_remove_fast(&efx->tc->ct_zone_ht, &ct_zone->linkage, efx_tc_ct_zone_ht_params); kfree(ct_zone); return ERR_PTR(rc); } void efx_tc_ct_unregister_zone(struct efx_nic *efx, struct efx_tc_ct_zone *ct_zone) { struct efx_tc_ct_entry *conn, *next; if (!refcount_dec_and_test(&ct_zone->ref)) return; /* still in use */ nf_flow_table_offload_del_cb(ct_zone->nf_ft, efx_tc_flow_block, ct_zone); rhashtable_remove_fast(&efx->tc->ct_zone_ht, &ct_zone->linkage, efx_tc_ct_zone_ht_params); mutex_lock(&ct_zone->mutex); list_for_each_entry(conn, &ct_zone->cts, list) efx_tc_ct_remove(efx, conn); synchronize_rcu(); /* need to use _safe because efx_tc_ct_remove_finish() frees conn */ list_for_each_entry_safe(conn, next, &ct_zone->cts, list) efx_tc_ct_remove_finish(efx, conn); mutex_unlock(&ct_zone->mutex); mutex_destroy(&ct_zone->mutex); netif_dbg(efx, drv, efx->net_dev, "Removed ct_zone for %u\n", ct_zone->zone); kfree(ct_zone); }