From 9f420c44c78801ade25594e35d2d02231e271996 Mon Sep 17 00:00:00 2001 From: Nikhil Hegde Date: Thu, 3 Sep 2026 17:45:16 +0000 Subject: [PATCH 1/7] vppbld: add copp_punt_policer plugin for CoPP dataplane enforcement New VPP plugin classifying and rate-limiting control-plane traffic (ARP, LACP, LLDP, UDLD, TTL_ERROR) on the device-input feature arc. Conforming packets are delivered directly to each port's paired TAP interface. Enabled in docker-syncd-vpp's VPP startup config. Signed-off-by: Nikhil Hegde --- docker-syncd-vpp/conf/startup.conf.tmpl | 1 + rules/vpp.mk | 2 +- .../plugins/copp_punt_policer/CMakeLists.txt | 31 ++ vppbld/plugins/copp_punt_policer/FEATURE.yaml | 30 ++ .../copp_punt_policer/copp_punt_policer.api | 82 ++++ .../copp_punt_policer/copp_punt_policer.c | 319 +++++++++++++++ .../copp_punt_policer/copp_punt_policer.h | 61 +++ .../copp_punt_policer_node.c | 372 ++++++++++++++++++ 8 files changed, 897 insertions(+), 1 deletion(-) create mode 100644 vppbld/plugins/copp_punt_policer/CMakeLists.txt create mode 100644 vppbld/plugins/copp_punt_policer/FEATURE.yaml create mode 100644 vppbld/plugins/copp_punt_policer/copp_punt_policer.api create mode 100644 vppbld/plugins/copp_punt_policer/copp_punt_policer.c create mode 100644 vppbld/plugins/copp_punt_policer/copp_punt_policer.h create mode 100644 vppbld/plugins/copp_punt_policer/copp_punt_policer_node.c diff --git a/docker-syncd-vpp/conf/startup.conf.tmpl b/docker-syncd-vpp/conf/startup.conf.tmpl index 40a6a537..ee96c024 100644 --- a/docker-syncd-vpp/conf/startup.conf.tmpl +++ b/docker-syncd-vpp/conf/startup.conf.tmpl @@ -243,6 +243,7 @@ plugins { plugin ip_validate_plugin.so { enable } plugin sflow_plugin.so { enable } plugin sonic_ext_plugin.so { enable } + plugin copp_punt_policer_plugin.so { enable } ## Enable all plugins by default and then selectively disable specific plugins # plugin dpdk_plugin.so { disable } diff --git a/rules/vpp.mk b/rules/vpp.mk index 739c82c6..2c2dbd79 100644 --- a/rules/vpp.mk +++ b/rules/vpp.mk @@ -7,7 +7,7 @@ VPP_VERSION_BASE = 2606 # https://packages.buildkite.com/sonic-vpp/vpp; if the suffix isn't bumped, # downstream sonic-buildimage builds will silently pull stale debs that # pre-date the new patch series and end up with VPP/SAI CRC drift. -VPP_VERSION = $(VPP_VERSION_BASE)-0.6 +VPP_VERSION = $(VPP_VERSION_BASE)-0.7 VPP_VERSION_SONIC = $(VPP_VERSION)+b1sonic1 VPP_SRC_PATH = platform/vpp/vppbld diff --git a/vppbld/plugins/copp_punt_policer/CMakeLists.txt b/vppbld/plugins/copp_punt_policer/CMakeLists.txt new file mode 100644 index 00000000..e1474500 --- /dev/null +++ b/vppbld/plugins/copp_punt_policer/CMakeLists.txt @@ -0,0 +1,31 @@ +# Copyright (c) 2026 SONiC-VPP contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Linked against linux-cp's `lcp` library so the data-path node can look +# up the phy->host (TAP) interface pairing and punt conforming, +# CoPP-matched packets directly to the mapped TAP -- bypassing +# ethernet-input/linux-cp's own protocol-dispatch nodes entirely, so +# delivery does not depend on whichever downstream path (arp-input, +# linux-cp-punt-xc, ip4-punt-redirect, ...) would otherwise have +# handled it. +add_vpp_plugin(copp_punt_policer + SOURCES + copp_punt_policer.c + copp_punt_policer_node.c + + API_FILES + copp_punt_policer.api + + LINK_LIBRARIES + lcp +) diff --git a/vppbld/plugins/copp_punt_policer/FEATURE.yaml b/vppbld/plugins/copp_punt_policer/FEATURE.yaml new file mode 100644 index 00000000..be1df9ae --- /dev/null +++ b/vppbld/plugins/copp_punt_policer/FEATURE.yaml @@ -0,0 +1,30 @@ +--- +name: CoPP Punt Policer +maintainer: SONiC-VPP contributors +features: + - Applies per-ethertype rate policing to control-plane traffic on the + device-input feature arc, before ethernet-input's protocol dispatch + runs. This closes a gap left by VPP's existing classify-based + policer-classify feature, which only runs on l2-input (bridged L2 + traffic), ip4-unicast, and ip6-unicast -- arcs that ARP, LACP, LLDP, + and UDLD traffic never traverses on linux-cp-paired, L3-routed + ports (they are dispatched directly from ethernet-input to + arp-input/linux-cp-punt-xc and punted straight to the interface's + TAP). + - Reuses VPP's existing policer objects and metering primitive + (vnet_police_packet()) -- no new token-bucket implementation. + Policer objects are created/updated by external control-plane code + exactly as before (e.g. via policer_add); this plugin only adds a + second, additional consulting point for those same objects on + control-plane ethertypes that would otherwise never reach a + policer at all. + - Binding an ethertype to a policer is done via a small VPP binary + API (copp_punt_policer_bind) or the "copp punt policer bind" + debug CLI; the feature auto-enables on device-input for every + interface as it is created, so no per-interface enable step is + needed once at least one binding exists. + - Per-binding conform/exceed/violate packet counters are exposed via + copp_punt_policer_get_counters (API) or "show copp punt policer" + (CLI). +description: "Per-ethertype punt policing on device-input, for SONiC/VPP CoPP" +state: experimental diff --git a/vppbld/plugins/copp_punt_policer/copp_punt_policer.api b/vppbld/plugins/copp_punt_policer/copp_punt_policer.api new file mode 100644 index 00000000..69c3eb47 --- /dev/null +++ b/vppbld/plugins/copp_punt_policer/copp_punt_policer.api @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2026 SONiC-VPP contributors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +option version = "1.0.0"; + +/** \brief Bind (or unbind) an Ethernet ethertype to an existing VPP + policer object, enforced on the device-input arc for every + interface -- i.e. before ethernet-input's protocol dispatch runs, + so it catches control-plane traffic (ARP/LACP/LLDP/UDLD/etc.) that + never traverses l2-input or ip4/ip6-unicast (where VPP's existing + classify-based policer-classify feature lives). + + @param client_index - opaque cookie to identify the sender + @param context - sender context, to match reply w/ request + @param ethertype - the Ethernet ethertype to match (host byte order, + e.g. 0x0806 for ARP, 0x8809 for LACP, 0x88cc for LLDP, or + 0x0800 for match_ip4_ttl_expiring below) + @param policer_name - name of an existing VPP policer object + (created via policer_add, e.g. "copp-policer-0x"); + looked up once at bind time via vnet_policer_main's + policer_index_by_name hash + @param is_bind - 1 to add/replace the binding, 0 to remove it + @param match_ip4_ttl_expiring - when true (only meaningful with + ethertype 0x0800), additionally requires the IPv4 header's + TTL to be <= 1 for a packet to match this entry -- ordinary + IPv4 traffic (BGP, DHCP, ...) with ethertype 0x0800 keeps + passing through unclassified. This is how CoPP's TTL_ERROR + trap is implemented: reusing this plugin's proven + classify+police+direct-to-TAP mechanism instead of a new + VPP punt-reason/ICMP-generation path. +*/ +define copp_punt_policer_bind +{ + u32 client_index; + u32 context; + u16 ethertype; + string policer_name[64]; + bool is_bind; + bool match_ip4_ttl_expiring; +}; + +define copp_punt_policer_bind_reply +{ + u32 context; + i32 retval; +}; + +/** \brief Read back this plugin's own conform/exceed/violate packet + counters for one bound ethertype (independent of, and in addition + to, the underlying VPP policer object's own stats-segment + counters -- since this plugin's node is what actually applies the + verdict). + + @param ethertype - the ethertype to query counters for +*/ +define copp_punt_policer_get_counters +{ + u32 client_index; + u32 context; + u16 ethertype; +}; + +define copp_punt_policer_get_counters_reply +{ + u32 context; + i32 retval; + u64 conform_packets; + u64 exceed_packets; + u64 violate_packets; +}; diff --git a/vppbld/plugins/copp_punt_policer/copp_punt_policer.c b/vppbld/plugins/copp_punt_policer/copp_punt_policer.c new file mode 100644 index 00000000..6c112366 --- /dev/null +++ b/vppbld/plugins/copp_punt_policer/copp_punt_policer.c @@ -0,0 +1,319 @@ +/* + * Copyright (c) 2026 SONiC-VPP contributors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * copp_punt_policer: per-ethertype policing on the device-input arc. + * + * BACKGROUND (see sonic-net/sonic-buildimage#25801, SONiC-on-VPP CoPP HLD, + * item 4/8): VPP's existing classify-based policer feature + * (policer-classify) only runs on three feature arcs: l2-input (bridged + * L2 traffic), ip4-unicast, ip6-unicast. On linux-cp-paired, L3-routed + * ports (this project's topology), ARP/LACP/LLDP/UDLD traffic never + * traverses any of those three arcs -- ethernet-input dispatches it + * directly to arp-input/linux-cp-punt-xc, which punt straight to the + * TAP. BGP/DHCP already work because they do ride ip4/ip6-unicast. + * + * This plugin closes that gap by registering a small feature node on + * device-input, the one arc every packet on a port crosses before + * ethernet-input's protocol dispatch runs. It parses just the 14-byte + * Ethernet header (cheap -- no full packet classification), looks up + * a per-ethertype policer binding, and applies VPP's existing + * vnet_police_packet() token-bucket primitive against the existing SAI- + * created VPP policer object (same policer objects SwitchVppPolicer.cpp + * already creates correctly with the right CIR/CBS from copp_cfg.json). + * No new metering implementation, no VPP core patch -- this is a + * self-contained plugin, following the same out-of-tree pattern as this + * repo's existing ip_validate/tunterm_acl plugins + * (platform/vpp/vppbld/plugins/). + */ + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#define REPLY_MSG_ID_BASE sm->msg_id_base +#include + +VLIB_PLUGIN_REGISTER () = { + .version = COPP_PUNT_POLICER_PLUGIN_BUILD_VER, + .description = "CoPP punt policer (per-ethertype policing on device-input)", +}; + +copp_punt_policer_main_t copp_punt_policer_main; + +/* + * Find an existing entry for this ethertype, or -1 if none. Linear scan + * over a small, bounded array (COPP_PUNT_POLICER_MAX_ENTRIES) -- this is + * control-plane/config-time code, not the per-packet data path (see + * copp_punt_policer_node.c for that). + */ +static int +copp_punt_policer_find_entry (copp_punt_policer_main_t *cpm, u16 ethertype) +{ + for (u32 i = 0; i < cpm->n_entries; i++) + { + if (cpm->entries[i].in_use && cpm->entries[i].ethertype == ethertype) + return (int) i; + } + return -1; +} + +/* + * Bind (or unbind) an ethertype -> policer-name entry. On bind, the + * named policer is looked up now via vnet_policer_main's + * policer_index_by_name hash (populated by policer_add(), which + * SwitchVppPolicer.cpp already calls for every SAI POLICER object) -- + * if the name isn't found yet, the entry is still recorded with + * policer_index left unresolved (~0); the data-path node re-attempts + * the lookup lazily so binding order relative to policer_add() doesn't + * matter. + * + * match_ip4_ttl_expiring: when set (only meaningful with ethertype == + * 0x0800), the data-path node additionally requires the packet's IPv4 + * TTL to be <= 1 to match this entry -- this is how CoPP's TTL_ERROR + * trap reuses this plugin's classify+police+direct-to-TAP mechanism, + * without policing ordinary IPv4 traffic (BGP, DHCP, ...) that also + * carries ethertype 0x0800. + */ +int +copp_punt_policer_bind (u16 ethertype, const char *policer_name, + int is_bind, int match_ip4_ttl_expiring) +{ + copp_punt_policer_main_t *cpm = &copp_punt_policer_main; + int idx = copp_punt_policer_find_entry (cpm, ethertype); + + if (!is_bind) + { + if (idx < 0) + return 0; + clib_memset (&cpm->entries[idx], 0, sizeof (cpm->entries[idx])); + cpm->conform_packets[idx] = 0; + cpm->exceed_packets[idx] = 0; + cpm->violate_packets[idx] = 0; + return 0; + } + + if (idx < 0) + { + if (cpm->n_entries >= COPP_PUNT_POLICER_MAX_ENTRIES) + return VNET_API_ERROR_QUEUE_FULL; + idx = (int) cpm->n_entries++; + } + + clib_memset (&cpm->entries[idx], 0, sizeof (cpm->entries[idx])); + cpm->entries[idx].ethertype = ethertype; + snprintf ((char *) cpm->entries[idx].name, + sizeof (cpm->entries[idx].name), "%s", policer_name); + cpm->entries[idx].policer_index = ~0; + cpm->entries[idx].in_use = 1; + cpm->entries[idx].match_ip4_ttl_expiring = match_ip4_ttl_expiring ? 1 : 0; + cpm->conform_packets[idx] = 0; + cpm->exceed_packets[idx] = 0; + cpm->violate_packets[idx] = 0; + + return 0; +} + +static void +vl_api_copp_punt_policer_bind_t_handler ( + vl_api_copp_punt_policer_bind_t *mp) +{ + copp_punt_policer_main_t *sm = &copp_punt_policer_main; + vl_api_copp_punt_policer_bind_reply_t *rmp; + int rv; + char name[64]; + + snprintf (name, sizeof (name), "%s", mp->policer_name); + rv = copp_punt_policer_bind (ntohs (mp->ethertype), name, mp->is_bind, + mp->match_ip4_ttl_expiring); + + REPLY_MACRO (VL_API_COPP_PUNT_POLICER_BIND_REPLY); +} + +static void +vl_api_copp_punt_policer_get_counters_t_handler ( + vl_api_copp_punt_policer_get_counters_t *mp) +{ + copp_punt_policer_main_t *sm = &copp_punt_policer_main; + vl_api_copp_punt_policer_get_counters_reply_t *rmp; + int rv = 0; + u64 conform = 0, exceed = 0, violate = 0; + int idx = copp_punt_policer_find_entry (sm, ntohs (mp->ethertype)); + + if (idx < 0) + rv = VNET_API_ERROR_NO_SUCH_ENTRY; + else + { + conform = sm->conform_packets[idx]; + exceed = sm->exceed_packets[idx]; + violate = sm->violate_packets[idx]; + } + + REPLY_MACRO2 (VL_API_COPP_PUNT_POLICER_GET_COUNTERS_REPLY, + ({ + rmp->conform_packets = clib_host_to_net_u64 (conform); + rmp->exceed_packets = clib_host_to_net_u64 (exceed); + rmp->violate_packets = clib_host_to_net_u64 (violate); + })); +} + +/* API definitions */ +#include + +static clib_error_t * +copp_punt_policer_init (vlib_main_t *vm) +{ + copp_punt_policer_main_t *cpm = &copp_punt_policer_main; + + cpm->msg_id_base = setup_message_id_table (); + cpm->vlib_main = vm; + cpm->vnet_main = vnet_get_main (); + cpm->n_entries = 0; + + /* + * One-shot sanity event, unconditional (no `elog trace`/`event-logger + * trace` arming needed to see it). If it is present in `vppctl show + * event-logger` after boot, the event-logger ring is live end-to-end + * and any absence of per-packet copp-punt-policer events from the + * data-path node (see copp_punt_policer_node.c) is a real "no packets + * reached this node" finding, not a broken/disabled logger. + */ + ELOG_TYPE_DECLARE (e) = { + .format = "copp-punt-policer: plugin initialized, elog is live", + }; + elog_main_t *em = vlib_get_elog_main (); + ELOG (em, e, 0); + + return 0; +} + +VLIB_INIT_FUNCTION (copp_punt_policer_init); + +/* + * Auto-enable the feature on every interface as it is created, exactly + * like ip_validate does for ip4/ip6-unicast -- no explicit per-interface + * API call needed. Bindings (which ethertypes map to which policers) + * are configured separately via copp_punt_policer_bind above; this only + * wires the *feature* onto device-input for the interface so the node + * runs at all. + */ +static clib_error_t * +copp_punt_policer_sw_interface_add_del (vnet_main_t *vnm, u32 sw_if_index, + u32 is_add) +{ + vnet_feature_enable_disable ("device-input", "copp-punt-policer", + sw_if_index, is_add, 0, 0); + return 0; +} + +VNET_SW_INTERFACE_ADD_DEL_FUNCTION (copp_punt_policer_sw_interface_add_del); + +/* CLI: bind/unbind, for manual testing and for a scriptable non-API path */ +static clib_error_t * +copp_punt_policer_bind_command_fn (vlib_main_t *vm, + unformat_input_t *input, + vlib_cli_command_t *cmd) +{ + u32 ethertype = 0; + u8 *policer_name = 0; + int is_bind = 1; + int match_ip4_ttl_expiring = 0; + clib_error_t *error = 0; + + while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT) + { + if (unformat (input, "ethertype 0x%x", ðertype)) + ; + else if (unformat (input, "ethertype %d", ðertype)) + ; + else if (unformat (input, "policer %s", &policer_name)) + ; + else if (unformat (input, "match-ip4-ttl-expiring")) + match_ip4_ttl_expiring = 1; + else if (unformat (input, "del")) + is_bind = 0; + else + { + error = clib_error_return (0, "unknown input `%U'", + format_unformat_error, input); + goto done; + } + } + + if (ethertype == 0 || (is_bind && !policer_name)) + { + error = clib_error_return (0, "usage: copp punt policer bind " + "ethertype <0xNNNN> policer " + "[match-ip4-ttl-expiring] [del]"); + goto done; + } + + { + int rv = copp_punt_policer_bind ((u16) ethertype, + policer_name ? (char *) policer_name : "", + is_bind, match_ip4_ttl_expiring); + if (rv) + error = clib_error_return (0, "bind failed: rv %d", rv); + } + +done: + vec_free (policer_name); + return error; +} + +VLIB_CLI_COMMAND (copp_punt_policer_bind_command, static) = { + .path = "copp punt policer bind", + .short_help = "copp punt policer bind ethertype <0xNNNN> policer " + "[match-ip4-ttl-expiring] [del]", + .function = copp_punt_policer_bind_command_fn, +}; + +static clib_error_t * +copp_punt_policer_show_command_fn (vlib_main_t *vm, + unformat_input_t *input, + vlib_cli_command_t *cmd) +{ + copp_punt_policer_main_t *cpm = &copp_punt_policer_main; + + vlib_cli_output (vm, "%-8s %-40s %-12s %10s %10s %10s %s", + "ethtype", "policer-name", "vpp-idx", + "conform", "exceed", "violate", "match"); + for (u32 i = 0; i < cpm->n_entries; i++) + { + if (!cpm->entries[i].in_use) + continue; + vlib_cli_output (vm, "0x%04x %-40s %-12d %10llu %10llu %10llu %s", + cpm->entries[i].ethertype, cpm->entries[i].name, + (i32) cpm->entries[i].policer_index, + cpm->conform_packets[i], cpm->exceed_packets[i], + cpm->violate_packets[i], + cpm->entries[i].match_ip4_ttl_expiring ? + "ip4-ttl<=1" : "-"); + } + + return 0; +} + +VLIB_CLI_COMMAND (copp_punt_policer_show_command, static) = { + .path = "show copp punt policer", + .short_help = "show copp punt policer", + .function = copp_punt_policer_show_command_fn, +}; diff --git a/vppbld/plugins/copp_punt_policer/copp_punt_policer.h b/vppbld/plugins/copp_punt_policer/copp_punt_policer.h new file mode 100644 index 00000000..2c0f9d52 --- /dev/null +++ b/vppbld/plugins/copp_punt_policer/copp_punt_policer.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2026 SONiC-VPP contributors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef __included_copp_punt_policer_h__ +#define __included_copp_punt_policer_h__ + +#include +#include + +/* + * A single ethertype -> policer-name binding, keyed by the raw Ethernet + * ethertype field. + */ +#define COPP_PUNT_POLICER_MAX_ENTRIES 16 +#define COPP_PUNT_POLICER_NAME_LEN 64 + +typedef struct +{ + u16 ethertype; /* host byte order */ + u8 name[COPP_PUNT_POLICER_NAME_LEN]; + u32 policer_index; + u8 in_use; + u8 match_ip4_ttl_expiring; /* TTL_ERROR trap */ +} copp_punt_policer_entry_t; + +typedef struct +{ + /* API message ID base */ + u16 msg_id_base; + + /* ethertype -> policer binding table */ + copp_punt_policer_entry_t entries[COPP_PUNT_POLICER_MAX_ENTRIES]; + u32 n_entries; + + /* per-entry conform/exceed/violate packet counters */ + u64 conform_packets[COPP_PUNT_POLICER_MAX_ENTRIES]; + u64 exceed_packets[COPP_PUNT_POLICER_MAX_ENTRIES]; + u64 violate_packets[COPP_PUNT_POLICER_MAX_ENTRIES]; + + vlib_main_t *vlib_main; + vnet_main_t *vnet_main; +} copp_punt_policer_main_t; + +extern copp_punt_policer_main_t copp_punt_policer_main; + +extern vlib_node_registration_t copp_punt_policer_node; + +#define COPP_PUNT_POLICER_PLUGIN_BUILD_VER "1.0" + +#endif /* __included_copp_punt_policer_h__ */ diff --git a/vppbld/plugins/copp_punt_policer/copp_punt_policer_node.c b/vppbld/plugins/copp_punt_policer/copp_punt_policer_node.c new file mode 100644 index 00000000..cc2a7d51 --- /dev/null +++ b/vppbld/plugins/copp_punt_policer/copp_punt_policer_node.c @@ -0,0 +1,372 @@ +/* + * Copyright (c) 2026 SONiC-VPP contributors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Per-packet data path for the copp_punt_policer feature. Runs on + * device-input, before ethernet-input's protocol dispatch -- the buffer's + * current data pointer is the start of the raw Ethernet frame as + * received from the interface, so this reads the 14-byte header + * directly (no ethernet-input-provided L2 metadata to rely on yet). + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +ELOG_TYPE_DECLARE (copp_punt_policer_elog_seen) = { + .format = "copp-punt-policer: sw_if_index %d ethertype 0x%x matched %d " + "policer_index %d verdict %d", + .format_args = "i4i4i4i4i4", +}; + +typedef struct +{ + u32 sw_if_index; + u32 ethertype; + u32 matched; + u32 policer_index; + u32 verdict; +} __clib_packed copp_punt_policer_elog_data_t; + +static_always_inline void +copp_punt_policer_elog (vlib_main_t *vm, u32 sw_if_index, u16 ethertype, + int matched, u32 policer_index, u32 verdict) +{ + elog_main_t *em = vlib_get_elog_main (); + copp_punt_policer_elog_data_t *ed; + + ed = ELOG_DATA (em, copp_punt_policer_elog_seen); + ed->sw_if_index = sw_if_index; + ed->ethertype = ethertype; + ed->matched = matched >= 0; + ed->policer_index = policer_index; + ed->verdict = verdict; +} + +typedef struct +{ + u32 sw_if_index; + u32 next_index; + u16 ethertype; + u32 policer_index; + u32 verdict; /* policer_result_e */ +} copp_punt_policer_trace_t; + +static u8 * +format_copp_punt_policer_trace (u8 *s, va_list *args) +{ + CLIB_UNUSED (vlib_main_t * vm) = va_arg (*args, vlib_main_t *); + CLIB_UNUSED (vlib_node_t * node) = va_arg (*args, vlib_node_t *); + copp_punt_policer_trace_t *t = va_arg (*args, copp_punt_policer_trace_t *); + + s = format (s, + "COPP-PUNT-POLICER: sw_if_index %d next %d ethertype 0x%04x " + "policer_index %d verdict %d", + t->sw_if_index, t->next_index, t->ethertype, t->policer_index, + t->verdict); + return s; +} + +#define foreach_copp_punt_policer_error \ + _ (PASS, "packets passed (unmatched ethertype or conform)") \ + _ (DROP_EXCEED, "packets dropped (policer exceed/violate)") \ + _ (DROP_UNRESOLVED, "packets dropped (policer name not yet resolvable)") + +typedef enum +{ +#define _(sym, str) COPP_PUNT_POLICER_ERROR_##sym, + foreach_copp_punt_policer_error +#undef _ + COPP_PUNT_POLICER_N_ERROR, +} copp_punt_policer_error_t; + +static char *copp_punt_policer_error_strings[] = { +#define _(sym, string) string, + foreach_copp_punt_policer_error +#undef _ +}; + +typedef enum +{ + COPP_PUNT_POLICER_NEXT_DROP, + COPP_PUNT_POLICER_NEXT_INTERFACE_OUTPUT, + COPP_PUNT_POLICER_N_NEXT, +} copp_punt_policer_next_t; + +/* + * Resolve VPP policer_index for a bound entry by name. + * Looked up lazily so bind-before-policer-exists (or a later + * policer_update recreating the object under the same name) both work + * without requiring bind order to match creation order. + */ +static_always_inline u32 +copp_punt_policer_resolve_index (copp_punt_policer_entry_t *entry) +{ + vnet_policer_main_t *pm = &vnet_policer_main; + uword *p; + + if (PREDICT_TRUE (entry->policer_index != ~0)) + { + if (PREDICT_TRUE (pool_is_free_index (pm->policers, + entry->policer_index) == 0)) + return entry->policer_index; + } + + p = hash_get_mem (pm->policer_index_by_name, entry->name); + if (!p) + return ~0; + + entry->policer_index = (u32) p[0]; + return entry->policer_index; +} + +/* + * Process one packet: parse the Ethernet ethertype, find a bound entry, + * meter it, and pick the next node. Returns the error code recorded for + * counters. + */ +static_always_inline copp_punt_policer_error_t +copp_punt_policer_x1 (copp_punt_policer_main_t *cpm, vlib_buffer_t *b, + u16 *next, u16 *out_ethertype, u32 *out_policer_index, + u32 *out_verdict, int *out_matched_idx) +{ + ethernet_header_t *eth; + u16 ethertype; + u32 feat_next; + copp_punt_policer_entry_t *entry = 0; + int idx = -1; + index_t lipi; + lcp_itf_pair_t *lip; + + vnet_feature_next (&feat_next, b); + *next = (u16) feat_next; + *out_matched_idx = -1; + + if (PREDICT_FALSE (b->current_length < sizeof (ethernet_header_t))) + { + *out_ethertype = 0; + *out_policer_index = ~0; + *out_verdict = POLICE_CONFORM; + return COPP_PUNT_POLICER_ERROR_PASS; + } + + eth = vlib_buffer_get_current (b); + ethertype = clib_net_to_host_u16 (eth->type); + *out_ethertype = ethertype; + + for (u32 i = 0; i < cpm->n_entries; i++) + { + copp_punt_policer_entry_t *cand = &cpm->entries[i]; + + if (!cand->in_use || cand->ethertype != ethertype) + continue; + + if (cand->match_ip4_ttl_expiring) + { + ip4_header_t *ip4; + + if (b->current_length < + sizeof (ethernet_header_t) + sizeof (ip4_header_t)) + continue; + + ip4 = (ip4_header_t *) (eth + 1); + if (ip4->ttl > 1) + continue; + } + + entry = cand; + idx = (int) i; + break; + } + + if (!entry) + { + /* No policer for this ethertype -- pass through */ + *out_policer_index = ~0; + *out_verdict = POLICE_CONFORM; + return COPP_PUNT_POLICER_ERROR_PASS; + } + + *out_matched_idx = idx; + + { + u32 policer_index = copp_punt_policer_resolve_index (entry); + *out_policer_index = policer_index; + + if (PREDICT_FALSE (policer_index == ~0)) + { + /* Bound but the named policer doesn't exist in VPP -- + * drop rather than silently letting through unpoliced + */ + *out_verdict = POLICE_VIOLATE; + *next = COPP_PUNT_POLICER_NEXT_DROP; + return COPP_PUNT_POLICER_ERROR_DROP_UNRESOLVED; + } + + { + vnet_policer_main_t *pm = &vnet_policer_main; + policer_t *policer = pool_elt_at_index (pm->policers, policer_index); + /* + * VPP's own pps-mode policer config translation calibrates the + * underlying byte/kbps token bucket assuming every packet is + * exactly 256 bytes regardless of real frame size. + */ + policer_result_e verdict = vnet_police_packet ( + policer, 256, + POLICE_CONFORM, clib_cpu_time_now () >> POLICER_TICKS_PER_PERIOD_SHIFT); + + *out_verdict = verdict; + + if (PREDICT_FALSE (verdict != POLICE_CONFORM)) + { + *next = COPP_PUNT_POLICER_NEXT_DROP; + return COPP_PUNT_POLICER_ERROR_DROP_EXCEED; + } + } + } + + /* Conforming packet: punt it straight to the mapped TAP */ + lipi = lcp_itf_pair_find_by_phy (vnet_buffer (b)->sw_if_index[VLIB_RX]); + if (PREDICT_TRUE (lipi != INDEX_INVALID)) + { + lip = lcp_itf_pair_get (lipi); + if (lip) + { + vnet_buffer (b)->sw_if_index[VLIB_TX] = lip->lip_host_sw_if_index; + *next = COPP_PUNT_POLICER_NEXT_INTERFACE_OUTPUT; + } + } + + return COPP_PUNT_POLICER_ERROR_PASS; +} + +VLIB_NODE_FN (copp_punt_policer_node) +(vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *frame) +{ + copp_punt_policer_main_t *cpm = &copp_punt_policer_main; + u32 n_left_from, *from; + vlib_buffer_t *bufs[VLIB_FRAME_SIZE], **b; + u16 nexts[VLIB_FRAME_SIZE], *next; + u32 error_counts[COPP_PUNT_POLICER_N_ERROR] = { 0 }; + /* per-entry conform/exceed/violate deltas accumulated locally and + * flushed once at the end, to avoid a scattered-write per packet into + * cpm->{conform,exceed,violate}_packets for the common (unmatched) + * case */ + u64 conform_delta[COPP_PUNT_POLICER_MAX_ENTRIES] = { 0 }; + u64 exceed_delta[COPP_PUNT_POLICER_MAX_ENTRIES] = { 0 }; + u64 violate_delta[COPP_PUNT_POLICER_MAX_ENTRIES] = { 0 }; + + from = vlib_frame_vector_args (frame); + n_left_from = frame->n_vectors; + + vlib_get_buffers (vm, from, bufs, n_left_from); + b = bufs; + next = nexts; + + while (n_left_from) + { + u16 ethertype = 0; + u32 policer_index = ~0; + u32 verdict = POLICE_CONFORM; + int matched_idx = -1; + copp_punt_policer_error_t err; + + err = copp_punt_policer_x1 (cpm, b[0], &next[0], ðertype, + &policer_index, &verdict, &matched_idx); + error_counts[err]++; + + if (matched_idx >= 0) + copp_punt_policer_elog (vm, vnet_buffer (b[0])->sw_if_index[VLIB_RX], + ethertype, matched_idx, policer_index, + verdict); + + if (matched_idx >= 0) + { + switch ((policer_result_e) verdict) + { + case POLICE_CONFORM: + conform_delta[matched_idx]++; + break; + case POLICE_EXCEED: + exceed_delta[matched_idx]++; + break; + case POLICE_VIOLATE: + violate_delta[matched_idx]++; + break; + } + } + + if (PREDICT_FALSE ((node->flags & VLIB_NODE_FLAG_TRACE) && + (b[0]->flags & VLIB_BUFFER_IS_TRACED))) + { + copp_punt_policer_trace_t *t = + vlib_add_trace (vm, node, b[0], sizeof (*t)); + t->sw_if_index = vnet_buffer (b[0])->sw_if_index[VLIB_RX]; + t->next_index = next[0]; + t->ethertype = ethertype; + t->policer_index = policer_index; + t->verdict = verdict; + } + + b += 1; + next += 1; + n_left_from -= 1; + } + + vlib_buffer_enqueue_to_next (vm, node, from, nexts, frame->n_vectors); + + for (int i = 0; i < COPP_PUNT_POLICER_N_ERROR; i++) + { + if (error_counts[i]) + vlib_node_increment_counter (vm, copp_punt_policer_node.index, i, + error_counts[i]); + } + + for (u32 i = 0; i < cpm->n_entries; i++) + { + if (conform_delta[i]) + cpm->conform_packets[i] += conform_delta[i]; + if (exceed_delta[i]) + cpm->exceed_packets[i] += exceed_delta[i]; + if (violate_delta[i]) + cpm->violate_packets[i] += violate_delta[i]; + } + + return frame->n_vectors; +} + +VLIB_REGISTER_NODE (copp_punt_policer_node) = { + .name = "copp-punt-policer", + .vector_size = sizeof (u32), + .format_trace = format_copp_punt_policer_trace, + .type = VLIB_NODE_TYPE_INTERNAL, + .n_errors = ARRAY_LEN (copp_punt_policer_error_strings), + .error_strings = copp_punt_policer_error_strings, + .n_next_nodes = COPP_PUNT_POLICER_N_NEXT, + .next_nodes = { + [COPP_PUNT_POLICER_NEXT_DROP] = "error-drop", + [COPP_PUNT_POLICER_NEXT_INTERFACE_OUTPUT] = "interface-output", + }, +}; + +VNET_FEATURE_INIT (copp_punt_policer_feat, static) = { + .arc_name = "device-input", + .node_name = "copp-punt-policer", + .runs_before = VNET_FEATURES ("ethernet-input"), +}; From fb3966dfc70467be2b21544b9a805c0c243fdeaf Mon Sep 17 00:00:00 2001 From: Nikhil Hegde Date: Thu, 3 Sep 2026 20:48:28 +0000 Subject: [PATCH 2/7] Fix build against VPP's policer pluginification (68c2966f1) The pinned VPP commit (3f9e978d7) includes upstream's 'policer: pluginify policer' refactor, which moved policer support from src/vnet/policer/ into its own out-of-tree-style plugin at src/plugins/policer/. This broke the CI build: fatal error: 'vnet/policer/policer.h' file not found Fix: - Update includes from to (the new location, matching how src/plugins/unittest/policer_test.c already references it). - The policer plugin's main-struct global (vnet_policer_main) is no longer directly linkable from an external plugin -- it's only extern-visible from inside the policer plugin itself (POLICER_PLUGIN_INTERNAL). External plugins must resolve it via the exported policer_get_main() accessor (a vlib_get_plugin_symbol() runtime lookup), same pattern policer_test.c uses. Switch both call sites and add a defensive null check, since the lookup can now legitimately fail (missing/not-yet-loaded policer plugin) where a direct global reference never could. Verified via a standalone syntax-only compile of both changed .c files against a clean checkout of the exact pinned VPP commit (3f9e978d7) -- 0 errors, only the pre-existing -Waddress-of-packed-member warnings already suppressed by the project's real build flags. Signed-off-by: Nikhil Hegde --- .../copp_punt_policer/copp_punt_policer.api | 2 +- .../copp_punt_policer/copp_punt_policer.c | 4 ++-- .../copp_punt_policer/copp_punt_policer_node.c | 17 ++++++++++++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/vppbld/plugins/copp_punt_policer/copp_punt_policer.api b/vppbld/plugins/copp_punt_policer/copp_punt_policer.api index 69c3eb47..846a9412 100644 --- a/vppbld/plugins/copp_punt_policer/copp_punt_policer.api +++ b/vppbld/plugins/copp_punt_policer/copp_punt_policer.api @@ -29,7 +29,7 @@ option version = "1.0.0"; 0x0800 for match_ip4_ttl_expiring below) @param policer_name - name of an existing VPP policer object (created via policer_add, e.g. "copp-policer-0x"); - looked up once at bind time via vnet_policer_main's + looked up once at bind time via policer_main's policer_index_by_name hash @param is_bind - 1 to add/replace the binding, 0 to remove it @param match_ip4_ttl_expiring - when true (only meaningful with diff --git a/vppbld/plugins/copp_punt_policer/copp_punt_policer.c b/vppbld/plugins/copp_punt_policer/copp_punt_policer.c index 6c112366..80428723 100644 --- a/vppbld/plugins/copp_punt_policer/copp_punt_policer.c +++ b/vppbld/plugins/copp_punt_policer/copp_punt_policer.c @@ -39,7 +39,7 @@ #include #include -#include +#include #include #include @@ -78,7 +78,7 @@ copp_punt_policer_find_entry (copp_punt_policer_main_t *cpm, u16 ethertype) /* * Bind (or unbind) an ethertype -> policer-name entry. On bind, the - * named policer is looked up now via vnet_policer_main's + * named policer is looked up now via policer_main's * policer_index_by_name hash (populated by policer_add(), which * SwitchVppPolicer.cpp already calls for every SAI POLICER object) -- * if the name isn't found yet, the entry is still recorded with diff --git a/vppbld/plugins/copp_punt_policer/copp_punt_policer_node.c b/vppbld/plugins/copp_punt_policer/copp_punt_policer_node.c index cc2a7d51..e341bf36 100644 --- a/vppbld/plugins/copp_punt_policer/copp_punt_policer_node.c +++ b/vppbld/plugins/copp_punt_policer/copp_punt_policer_node.c @@ -23,7 +23,7 @@ #include #include #include -#include +#include #include #include #include @@ -118,9 +118,12 @@ typedef enum static_always_inline u32 copp_punt_policer_resolve_index (copp_punt_policer_entry_t *entry) { - vnet_policer_main_t *pm = &vnet_policer_main; + policer_main_t *pm = policer_get_main (); uword *p; + if (PREDICT_FALSE (pm == 0)) + return ~0; + if (PREDICT_TRUE (entry->policer_index != ~0)) { if (PREDICT_TRUE (pool_is_free_index (pm->policers, @@ -220,7 +223,15 @@ copp_punt_policer_x1 (copp_punt_policer_main_t *cpm, vlib_buffer_t *b, } { - vnet_policer_main_t *pm = &vnet_policer_main; + policer_main_t *pm = policer_get_main (); + + if (PREDICT_FALSE (pm == 0)) + { + *out_verdict = POLICE_VIOLATE; + *next = COPP_PUNT_POLICER_NEXT_DROP; + return COPP_PUNT_POLICER_ERROR_DROP_UNRESOLVED; + } + policer_t *policer = pool_elt_at_index (pm->policers, policer_index); /* * VPP's own pps-mode policer config translation calibrates the From 337f2515f2468b7849d6fdc370a73137a9d8328e Mon Sep 17 00:00:00 2001 From: Nikhil Hegde Date: Fri, 4 Sep 2026 00:13:04 +0000 Subject: [PATCH 3/7] Address review: enable policer_plugin.so, increment SAI stats counters - docker-syncd-vpp/conf/startup.conf.tmpl: enable policer_plugin.so. The pinned VPP version loads policer as its own plugin (no longer core vnet); without this, sonic-sairedis's policer_msg_id_base lookup returns ~0 and aborts syncd at connect. - copp_punt_policer_node.c: call policer_get_counters() + vlib_increment_combined_counter() after computing the verdict, so the VPP stats-segment counters (/net/policer/{conform,exceed,violate}) that SwitchVppPolicer.cpp's getPolicerStats() reads are actually populated. vnet_police_packet() alone only updates the token bucket and never touches these counters. - copp_punt_policer_node.c: gate the per-packet ELOG call on VLIB_NODE_FLAG_TRACE instead of firing unconditionally. - copp_punt_policer.c: document the lack of a vlib_worker_thread_barrier_sync() around the bind-table mutation -- the only caller in this environment is SONiC's syncd (single VAPI client, main thread only), so this is unlikely to be an issue. Signed-off-by: Nikhil Hegde --- docker-syncd-vpp/conf/startup.conf.tmpl | 1 + .../plugins/copp_punt_policer/copp_punt_policer.c | 4 ++++ .../copp_punt_policer/copp_punt_policer_node.c | 14 ++++++++++---- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/docker-syncd-vpp/conf/startup.conf.tmpl b/docker-syncd-vpp/conf/startup.conf.tmpl index ee96c024..f44d2d19 100644 --- a/docker-syncd-vpp/conf/startup.conf.tmpl +++ b/docker-syncd-vpp/conf/startup.conf.tmpl @@ -243,6 +243,7 @@ plugins { plugin ip_validate_plugin.so { enable } plugin sflow_plugin.so { enable } plugin sonic_ext_plugin.so { enable } + plugin policer_plugin.so { enable } plugin copp_punt_policer_plugin.so { enable } ## Enable all plugins by default and then selectively disable specific plugins diff --git a/vppbld/plugins/copp_punt_policer/copp_punt_policer.c b/vppbld/plugins/copp_punt_policer/copp_punt_policer.c index 80428723..3a63f4e9 100644 --- a/vppbld/plugins/copp_punt_policer/copp_punt_policer.c +++ b/vppbld/plugins/copp_punt_policer/copp_punt_policer.c @@ -92,6 +92,10 @@ copp_punt_policer_find_entry (copp_punt_policer_main_t *cpm, u16 ethertype) * trap reuses this plugin's classify+police+direct-to-TAP mechanism, * without policing ordinary IPv4 traffic (BGP, DHCP, ...) that also * carries ethertype 0x0800. + * + * Thread safety: mutates cpm->entries[]/n_entries with no lock, but the + * only caller in this environment is SONiC's syncd (single VAPI client, + * main thread only), so this is unlikely to be an issue in practice. */ int copp_punt_policer_bind (u16 ethertype, const char *policer_name, diff --git a/vppbld/plugins/copp_punt_policer/copp_punt_policer_node.c b/vppbld/plugins/copp_punt_policer/copp_punt_policer_node.c index e341bf36..e9d6b643 100644 --- a/vppbld/plugins/copp_punt_policer/copp_punt_policer_node.c +++ b/vppbld/plugins/copp_punt_policer/copp_punt_policer_node.c @@ -145,7 +145,7 @@ copp_punt_policer_resolve_index (copp_punt_policer_entry_t *entry) * counters. */ static_always_inline copp_punt_policer_error_t -copp_punt_policer_x1 (copp_punt_policer_main_t *cpm, vlib_buffer_t *b, +copp_punt_policer_x1 (vlib_main_t *vm, copp_punt_policer_main_t *cpm, vlib_buffer_t *b, u16 *next, u16 *out_ethertype, u32 *out_policer_index, u32 *out_verdict, int *out_matched_idx) { @@ -238,10 +238,16 @@ copp_punt_policer_x1 (copp_punt_policer_main_t *cpm, vlib_buffer_t *b, * underlying byte/kbps token bucket assuming every packet is * exactly 256 bytes regardless of real frame size. */ + u32 metered_len = 256; policer_result_e verdict = vnet_police_packet ( - policer, 256, + policer, metered_len, POLICE_CONFORM, clib_cpu_time_now () >> POLICER_TICKS_PER_PERIOD_SHIFT); + vlib_combined_counter_main_t *pc = policer_get_counters (); + if (PREDICT_TRUE (pc != 0)) + vlib_increment_combined_counter ( + &pc[verdict], vm->thread_index, policer_index, 1, metered_len); + *out_verdict = verdict; if (PREDICT_FALSE (verdict != POLICE_CONFORM)) @@ -298,11 +304,11 @@ VLIB_NODE_FN (copp_punt_policer_node) int matched_idx = -1; copp_punt_policer_error_t err; - err = copp_punt_policer_x1 (cpm, b[0], &next[0], ðertype, + err = copp_punt_policer_x1 (vm, cpm, b[0], &next[0], ðertype, &policer_index, &verdict, &matched_idx); error_counts[err]++; - if (matched_idx >= 0) + if (PREDICT_FALSE ((node->flags & VLIB_NODE_FLAG_TRACE) && matched_idx >= 0)) copp_punt_policer_elog (vm, vnet_buffer (b[0])->sw_if_index[VLIB_RX], ethertype, matched_idx, policer_index, verdict); From ad1c7c6eb2e03920bc3ff524802b1c7c0e684d45 Mon Sep 17 00:00:00 2001 From: Nikhil Hegde Date: Thu, 10 Sep 2026 18:04:14 +0000 Subject: [PATCH 4/7] vppbld: add copp_ip2me_policer plugin for IP2ME/SNMP/SSH CoPP Polices traffic destined to router-owned IPv4 addresses on the ip4-punt arc, ahead of ip4-punt-redirect -- a single global arc, so no per-interface binding is needed. Reuses VPP's existing policer objects. Signed-off-by: Nikhil Hegde --- .../plugins/copp_ip2me_policer/CMakeLists.txt | 21 ++ .../plugins/copp_ip2me_policer/FEATURE.yaml | 26 ++ .../copp_ip2me_policer/copp_ip2me_policer.api | 82 ++++ .../copp_ip2me_policer/copp_ip2me_policer.c | 345 +++++++++++++++++ .../copp_ip2me_policer/copp_ip2me_policer.h | 79 ++++ .../copp_ip2me_policer_node.c | 351 ++++++++++++++++++ 6 files changed, 904 insertions(+) create mode 100644 vppbld/plugins/copp_ip2me_policer/CMakeLists.txt create mode 100644 vppbld/plugins/copp_ip2me_policer/FEATURE.yaml create mode 100644 vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer.api create mode 100644 vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer.c create mode 100644 vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer.h create mode 100644 vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer_node.c diff --git a/vppbld/plugins/copp_ip2me_policer/CMakeLists.txt b/vppbld/plugins/copp_ip2me_policer/CMakeLists.txt new file mode 100644 index 00000000..5455958c --- /dev/null +++ b/vppbld/plugins/copp_ip2me_policer/CMakeLists.txt @@ -0,0 +1,21 @@ +# Copyright (c) 2026 SONiC-VPP contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +add_vpp_plugin(copp_ip2me_policer + SOURCES + copp_ip2me_policer.c + copp_ip2me_policer_node.c + + API_FILES + copp_ip2me_policer.api +) diff --git a/vppbld/plugins/copp_ip2me_policer/FEATURE.yaml b/vppbld/plugins/copp_ip2me_policer/FEATURE.yaml new file mode 100644 index 00000000..4d6d9a57 --- /dev/null +++ b/vppbld/plugins/copp_ip2me_policer/FEATURE.yaml @@ -0,0 +1,26 @@ +--- +name: CoPP IP2ME Policer +maintainer: SONiC-VPP contributors +features: + - Applies rate policing to IP2ME/SNMP/SSH traffic (destined to one of + the router's own IPv4 addresses that VPP's own dataplane does not + answer itself) on the ip4-punt feature arc, ahead of + ip4-punt-redirect. ip4-punt is only reached after ip4-lookup/ + ip4-local have already decided a packet is host-bound, and is a + single global arc (not per-interface), so no per-interface or + per-router-interface binding bookkeeping is needed -- unlike VPP's + existing classify-based policer-classify feature, which only meters + traffic on whichever interfaces it has been explicitly bound to. + - Reuses VPP's existing policer objects and metering primitive + (vnet_police_packet()) -- no new token-bucket implementation. + - IPv4 addresses to track are added/removed via + copp_ip2me_policer_addr_add_del (API) or the "copp ip2me policer + addr" debug CLI; the shared policer is bound via + copp_ip2me_policer_bind (API) or "copp ip2me policer bind". The + feature is enabled once, globally, at plugin init -- no + per-interface enable step, ever. + - conform/exceed/violate packet counters are exposed via + copp_ip2me_policer_get_counters (API) or "show copp ip2me policer" + (CLI). +description: "IP2ME/SNMP/SSH punt policing on ip4-punt, for SONiC/VPP CoPP" +state: experimental diff --git a/vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer.api b/vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer.api new file mode 100644 index 00000000..2ad0e021 --- /dev/null +++ b/vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer.api @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2026 SONiC-VPP contributors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +option version = "1.0.0"; + +/** \brief Add or remove one IPv4 address from the IP2ME set enforced on + the ip4-punt feature arc (device-agnostic -- addresses are checked + regardless of which interface the packet arrived on, since ip4-punt + only runs after routing has already decided the packet is destined + to one of the router's own addresses). + + @param client_index - opaque cookie to identify the sender + @param context - sender context, to match reply w/ request + @param addr - the IPv4 address (network byte order) to add/remove + @param is_add - 1 to add, 0 to remove +*/ +define copp_ip2me_policer_addr_add_del +{ + u32 client_index; + u32 context; + u32 addr; + bool is_add; +}; + +define copp_ip2me_policer_addr_add_del_reply +{ + u32 context; + i32 retval; +}; + +/** \brief Bind (or unbind) the shared IP2ME policer used to meter all + currently-registered IP2ME addresses (SSH/SNMP/IP2ME all map to the + same SAI ip2me trap and therefore the same policer). + + @param policer_name - name of an existing VPP policer object + (created via policer_add), looked up once via policer_main's + policer_index_by_name hash + @param is_bind - 1 to bind, 0 to unbind +*/ +define copp_ip2me_policer_bind +{ + u32 client_index; + u32 context; + string policer_name[64]; + bool is_bind; +}; + +define copp_ip2me_policer_bind_reply +{ + u32 context; + i32 retval; +}; + +/** \brief Read back this plugin's own conform/exceed/violate packet + counters. +*/ +define copp_ip2me_policer_get_counters +{ + u32 client_index; + u32 context; +}; + +define copp_ip2me_policer_get_counters_reply +{ + u32 context; + i32 retval; + u64 conform_packets; + u64 exceed_packets; + u64 violate_packets; +}; diff --git a/vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer.c b/vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer.c new file mode 100644 index 00000000..039973e7 --- /dev/null +++ b/vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer.c @@ -0,0 +1,345 @@ +/* + * Copyright (c) 2026 SONiC-VPP contributors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * copp_ip2me_policer: CoPP enforcement for IP2ME/SNMP/SSH traffic -- + * traffic destined to one of the router's own IPv4 addresses that VPP's + * dataplane does not answer itself (see sonic-net/sonic-buildimage#25801, + * SONiC-on-VPP CoPP HLD). + * + * BACKGROUND: VPP's built-in classify-based policer feature + * (ip4-policer-classify, on the ip4-unicast arc) only meters traffic on + * whichever interface it has been explicitly bound to. Since the + * classify table matches purely on destination IP, IP2ME traffic + * destined to router-interface A's address can legitimately arrive on + * router-interface B -- if B never had the feature bound, that traffic + * skips policing entirely and reaches VPP's ip4-punt-redirect mechanism + * completely unpoliced. Binding to every possible L3 ingress interface + * is the fix VPP's own classify feature requires, but it needs lazy, + * per-RIF VAPI calls (deferred to avoid syncd's SAI-call watchdog) and + * has proven fragile in practice. + * + * This plugin avoids the whole binding-scope problem by living on + * ip4-punt instead: a single, always-on, global feature arc that every + * packet reaching this point has already been routed through + * (ip4-lookup -> ip4-local -> ip4-punt), regardless of which interface + * it arrived on. No per-interface binding is needed -- ip4-punt is + * reached the same way no matter the ingress interface, once VPP's own + * dataplane has already concluded "nothing here handles this packet, it + * needs the host." That is exactly SAI's IP2ME semantics, so this + * plugin only needs to answer one question (is the destination address + * one we're tracking?) and apply the existing SAI-created policer + * object, then let the packet continue unchanged to ip4-punt-redirect + * (conform) or drop it (exceed/violate) -- it never needs to redirect + * to a TAP itself, unlike copp_punt_policer's device-input node, since + * ip4-punt-redirect already does that for every packet that reaches it. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#define REPLY_MSG_ID_BASE sm->msg_id_base +#include + +VLIB_PLUGIN_REGISTER () = { + .version = COPP_IP2ME_POLICER_PLUGIN_BUILD_VER, + .description = "CoPP IP2ME/SNMP/SSH policing on the ip4-punt arc", +}; + +copp_ip2me_policer_main_t copp_ip2me_policer_main; + +/* + * Find an existing entry for this address, or -1 if none. Linear scan + * over a small, bounded array (COPP_IP2ME_POLICER_MAX_ADDRS) -- this is + * control-plane/config-time code, not the per-packet data path (see + * copp_ip2me_policer_node.c for that). + */ +static int +copp_ip2me_policer_find_addr (copp_ip2me_policer_main_t *cpm, u32 addr) +{ + for (u32 i = 0; i < cpm->n_addrs; i++) + { + if (cpm->addrs[i].in_use && cpm->addrs[i].addr == addr) + return (int) i; + } + return -1; +} + +int +copp_ip2me_policer_addr_add_del (u32 addr, int is_add) +{ + copp_ip2me_policer_main_t *cpm = &copp_ip2me_policer_main; + int idx = copp_ip2me_policer_find_addr (cpm, addr); + + if (!is_add) + { + if (idx < 0) + return 0; + clib_memset (&cpm->addrs[idx], 0, sizeof (cpm->addrs[idx])); + return 0; + } + + if (idx >= 0) + return 0; /* already present */ + + if (cpm->n_addrs >= COPP_IP2ME_POLICER_MAX_ADDRS) + return VNET_API_ERROR_QUEUE_FULL; + + idx = (int) cpm->n_addrs++; + cpm->addrs[idx].addr = addr; + cpm->addrs[idx].in_use = 1; + + return 0; +} + +int +copp_ip2me_policer_bind (const char *policer_name, int is_bind) +{ + copp_ip2me_policer_main_t *cpm = &copp_ip2me_policer_main; + + if (!is_bind) + { + clib_memset (cpm->policer_name, 0, sizeof (cpm->policer_name)); + cpm->policer_index = ~0; + cpm->policer_bound = 0; + cpm->conform_packets = 0; + cpm->exceed_packets = 0; + cpm->violate_packets = 0; + return 0; + } + + snprintf ((char *) cpm->policer_name, sizeof (cpm->policer_name), "%s", + policer_name); + cpm->policer_index = ~0; + cpm->policer_bound = 1; + cpm->conform_packets = 0; + cpm->exceed_packets = 0; + cpm->violate_packets = 0; + + return 0; +} + +static void +vl_api_copp_ip2me_policer_addr_add_del_t_handler ( + vl_api_copp_ip2me_policer_addr_add_del_t *mp) +{ + copp_ip2me_policer_main_t *sm = &copp_ip2me_policer_main; + vl_api_copp_ip2me_policer_addr_add_del_reply_t *rmp; + int rv; + + rv = copp_ip2me_policer_addr_add_del (mp->addr, mp->is_add); + + REPLY_MACRO (VL_API_COPP_IP2ME_POLICER_ADDR_ADD_DEL_REPLY); +} + +static void +vl_api_copp_ip2me_policer_bind_t_handler ( + vl_api_copp_ip2me_policer_bind_t *mp) +{ + copp_ip2me_policer_main_t *sm = &copp_ip2me_policer_main; + vl_api_copp_ip2me_policer_bind_reply_t *rmp; + int rv; + char name[64]; + + snprintf (name, sizeof (name), "%s", mp->policer_name); + rv = copp_ip2me_policer_bind (name, mp->is_bind); + + REPLY_MACRO (VL_API_COPP_IP2ME_POLICER_BIND_REPLY); +} + +static void +vl_api_copp_ip2me_policer_get_counters_t_handler ( + vl_api_copp_ip2me_policer_get_counters_t *mp) +{ + copp_ip2me_policer_main_t *sm = &copp_ip2me_policer_main; + vl_api_copp_ip2me_policer_get_counters_reply_t *rmp; + int rv = 0; + + REPLY_MACRO2 (VL_API_COPP_IP2ME_POLICER_GET_COUNTERS_REPLY, + ({ + rmp->conform_packets = clib_host_to_net_u64 (sm->conform_packets); + rmp->exceed_packets = clib_host_to_net_u64 (sm->exceed_packets); + rmp->violate_packets = clib_host_to_net_u64 (sm->violate_packets); + })); +} + +/* API definitions */ +#include + +static clib_error_t * +copp_ip2me_policer_init (vlib_main_t *vm) +{ + copp_ip2me_policer_main_t *cpm = &copp_ip2me_policer_main; + + cpm->msg_id_base = setup_message_id_table (); + cpm->vlib_main = vm; + cpm->vnet_main = vnet_get_main (); + cpm->n_addrs = 0; + cpm->policer_index = ~0; + cpm->policer_bound = 0; + + /* + * ip4-punt is a global feature arc, not per-interface -- enable this + * feature on it ONCE at init, unconditionally, the same way + * ip4-punt-redirect itself is always enabled. No per-interface + * enable/disable call is needed or ever made (see + * copp_punt_policer_sw_interface_add_del in the sibling + * copp_punt_policer plugin for contrast -- that one has to do this + * per-interface because device-input is a per-interface arc; ip4-punt + * is not). + */ + vnet_feature_enable_disable ("ip4-punt", "copp-ip2me-policer", 0, 1, 0, 0); + + return 0; +} + +VLIB_INIT_FUNCTION (copp_ip2me_policer_init); + +/* CLI: bind/unbind policer, for manual testing and a scriptable non-API path */ +static clib_error_t * +copp_ip2me_policer_bind_command_fn (vlib_main_t *vm, + unformat_input_t *input, + vlib_cli_command_t *cmd) +{ + u8 *policer_name = 0; + int is_bind = 1; + clib_error_t *error = 0; + + while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT) + { + if (unformat (input, "policer %s", &policer_name)) + ; + else if (unformat (input, "del")) + is_bind = 0; + else + { + error = clib_error_return (0, "unknown input `%U'", + format_unformat_error, input); + goto done; + } + } + + if (is_bind && !policer_name) + { + error = clib_error_return (0, "usage: copp ip2me policer bind " + "policer [del]"); + goto done; + } + + { + int rv = copp_ip2me_policer_bind ( + policer_name ? (char *) policer_name : "", is_bind); + if (rv) + error = clib_error_return (0, "bind failed: rv %d", rv); + } + +done: + vec_free (policer_name); + return error; +} + +VLIB_CLI_COMMAND (copp_ip2me_policer_bind_command, static) = { + .path = "copp ip2me policer bind", + .short_help = "copp ip2me policer bind policer [del]", + .function = copp_ip2me_policer_bind_command_fn, +}; + +/* CLI: add/remove an IP2ME address, for manual testing */ +static clib_error_t * +copp_ip2me_policer_addr_command_fn (vlib_main_t *vm, + unformat_input_t *input, + vlib_cli_command_t *cmd) +{ + ip4_address_t addr; + int have_addr = 0; + int is_add = 1; + clib_error_t *error = 0; + + while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT) + { + if (unformat (input, "%U", unformat_ip4_address, &addr)) + have_addr = 1; + else if (unformat (input, "del")) + is_add = 0; + else + { + error = clib_error_return (0, "unknown input `%U'", + format_unformat_error, input); + goto done; + } + } + + if (!have_addr) + { + error = clib_error_return (0, "usage: copp ip2me policer addr " + " [del]"); + goto done; + } + + { + int rv = copp_ip2me_policer_addr_add_del (addr.as_u32, is_add); + if (rv) + error = clib_error_return (0, "addr add/del failed: rv %d", rv); + } + +done: + return error; +} + +VLIB_CLI_COMMAND (copp_ip2me_policer_addr_command, static) = { + .path = "copp ip2me policer addr", + .short_help = "copp ip2me policer addr [del]", + .function = copp_ip2me_policer_addr_command_fn, +}; + +static clib_error_t * +copp_ip2me_policer_show_command_fn (vlib_main_t *vm, + unformat_input_t *input, + vlib_cli_command_t *cmd) +{ + copp_ip2me_policer_main_t *cpm = &copp_ip2me_policer_main; + + vlib_cli_output (vm, "policer: %s (index %d) bound=%d", + cpm->policer_bound ? (char *) cpm->policer_name : "-", + (i32) cpm->policer_index, cpm->policer_bound); + vlib_cli_output (vm, "conform %llu exceed %llu violate %llu", + cpm->conform_packets, cpm->exceed_packets, + cpm->violate_packets); + vlib_cli_output (vm, "%-8s addresses:", "count"); + vlib_cli_output (vm, "%u", cpm->n_addrs); + for (u32 i = 0; i < cpm->n_addrs; i++) + { + if (!cpm->addrs[i].in_use) + continue; + vlib_cli_output (vm, " %U", format_ip4_address, &cpm->addrs[i].addr); + } + + return 0; +} + +VLIB_CLI_COMMAND (copp_ip2me_policer_show_command, static) = { + .path = "show copp ip2me policer", + .short_help = "show copp ip2me policer", + .function = copp_ip2me_policer_show_command_fn, +}; diff --git a/vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer.h b/vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer.h new file mode 100644 index 00000000..9cb698bc --- /dev/null +++ b/vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer.h @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2026 SONiC-VPP contributors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef __included_copp_ip2me_policer_h__ +#define __included_copp_ip2me_policer_h__ + +#include + +/* + * IPv4 destination addresses considered "IP2ME" -- traffic destined to + * one of the router's own addresses that VPP's own dataplane doesn't + * answer itself (i.e. anything that reaches ip4-punt at all: SSH, SNMP, + * unregistered ports, ...). Enforced on the ip4-punt feature arc, which + * VPP only reaches AFTER ip4-lookup/ip4-local have already decided this + * packet is host-bound -- unlike device-input (too early: dest-IP alone + * doesn't mean CPU-bound, e.g. ping-to-router is answered in-dataplane) + * or VPP's built-in ip4-policer-classify feature (which only matches on + * interfaces it's explicitly bound to, requiring per-RIF bookkeeping + * that this plugin avoids entirely by living on a global, always-on + * arc instead). + */ +#define COPP_IP2ME_POLICER_MAX_ADDRS 256 +#define COPP_IP2ME_POLICER_NAME_LEN 64 + +typedef struct +{ + u32 addr; /* network byte order, matches ip4_address_t.as_u32 */ + u8 in_use; +} copp_ip2me_policer_addr_t; + +typedef struct +{ + /* API message ID base */ + u16 msg_id_base; + + /* IPv4 addresses currently classified as IP2ME (router's own + * addresses reachable via ip4-punt). Small, bounded, linear-scanned + * array -- looked up once per packet in the data path, so kept + * simple; COPP_IP2ME_POLICER_MAX_ADDRS comfortably covers every + * router-interface address on this testbed's largest topology. */ + copp_ip2me_policer_addr_t addrs[COPP_IP2ME_POLICER_MAX_ADDRS]; + u32 n_addrs; + + /* Single policer binding -- the SAI ip2me trap's policer (shared by + * IP2ME/SNMP/SSH; see PROTOCOL_TO_TRAP_ID in sonic-mgmt's + * copp_tests.py -- all three trap to the same SAI_HOSTIF_TRAP_TYPE_ + * IP2ME). Looked up lazily by name the same way copp_punt_policer + * does, so bind order relative to policer_add() doesn't matter. */ + u8 policer_name[COPP_IP2ME_POLICER_NAME_LEN]; + u32 policer_index; + u8 policer_bound; + + /* conform/exceed/violate packet counters */ + u64 conform_packets; + u64 exceed_packets; + u64 violate_packets; + + vlib_main_t *vlib_main; + vnet_main_t *vnet_main; +} copp_ip2me_policer_main_t; + +extern copp_ip2me_policer_main_t copp_ip2me_policer_main; + +extern vlib_node_registration_t copp_ip2me_policer_node; + +#define COPP_IP2ME_POLICER_PLUGIN_BUILD_VER "1.0" + +#endif /* __included_copp_ip2me_policer_h__ */ diff --git a/vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer_node.c b/vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer_node.c new file mode 100644 index 00000000..58d0b48c --- /dev/null +++ b/vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer_node.c @@ -0,0 +1,351 @@ +/* + * Copyright (c) 2026 SONiC-VPP contributors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Per-packet data path for the copp_ip2me_policer feature. Runs on the + * ip4-punt arc, AFTER ip4-lookup/ip4-local have already decided this + * packet is destined to a local address and nothing in VPP's own + * dataplane handles it (that decision is what routes it to ip4-punt in + * the first place -- see ip4_local_set_next_and_error() in + * src/vnet/ip/ip4_forward.c). ip4-local never advances the buffer past + * the IP header, so vlib_buffer_get_current() here still points at the + * start of the IPv4 header -- no offset math needed, unlike + * device-input-based classification which runs before the Ethernet + * header has been stripped. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +ELOG_TYPE_DECLARE (copp_ip2me_policer_elog_seen) = { + .format = "copp-ip2me-policer: sw_if_index %d matched %d policer_index %d " + "verdict %d", + .format_args = "i4i4i4i4", +}; + +typedef struct +{ + u32 sw_if_index; + u32 matched; + u32 policer_index; + u32 verdict; +} __clib_packed copp_ip2me_policer_elog_data_t; + +static_always_inline void +copp_ip2me_policer_elog (vlib_main_t *vm, u32 sw_if_index, int matched, + u32 policer_index, u32 verdict) +{ + elog_main_t *em = vlib_get_elog_main (); + copp_ip2me_policer_elog_data_t *ed; + + ed = ELOG_DATA (em, copp_ip2me_policer_elog_seen); + ed->sw_if_index = sw_if_index; + ed->matched = matched; + ed->policer_index = policer_index; + ed->verdict = verdict; +} + +typedef struct +{ + u32 sw_if_index; + u32 next_index; + u32 dst_addr; + u32 policer_index; + u32 verdict; /* policer_result_e */ +} copp_ip2me_policer_trace_t; + +static u8 * +format_copp_ip2me_policer_trace (u8 *s, va_list *args) +{ + CLIB_UNUSED (vlib_main_t * vm) = va_arg (*args, vlib_main_t *); + CLIB_UNUSED (vlib_node_t * node) = va_arg (*args, vlib_node_t *); + copp_ip2me_policer_trace_t *t = va_arg (*args, copp_ip2me_policer_trace_t *); + + s = format (s, + "COPP-IP2ME-POLICER: sw_if_index %d next %d dst %U " + "policer_index %d verdict %d", + t->sw_if_index, t->next_index, format_ip4_address, + &t->dst_addr, t->policer_index, t->verdict); + return s; +} + +#define foreach_copp_ip2me_policer_error \ + _ (PASS, "packets passed (unmatched address or conform)") \ + _ (DROP_EXCEED, "packets dropped (policer exceed/violate)") \ + _ (DROP_UNRESOLVED, "packets dropped (policer name not yet resolvable)") + +typedef enum +{ +#define _(sym, str) COPP_IP2ME_POLICER_ERROR_##sym, + foreach_copp_ip2me_policer_error +#undef _ + COPP_IP2ME_POLICER_N_ERROR, +} copp_ip2me_policer_error_t; + +static char *copp_ip2me_policer_error_strings[] = { +#define _(sym, string) string, + foreach_copp_ip2me_policer_error +#undef _ +}; + +/* + * Resolve VPP policer_index by name, lazily -- same pattern as + * copp_punt_policer_node.c, so bind order relative to policer_add() + * doesn't matter and a later policer_update() recreating the object + * under the same name is picked up automatically. + */ +static_always_inline u32 +copp_ip2me_policer_resolve_index (copp_ip2me_policer_main_t *cpm) +{ + policer_main_t *pm = policer_get_main (); + uword *p; + + if (PREDICT_FALSE (pm == 0)) + return ~0; + + if (PREDICT_TRUE (cpm->policer_index != ~0)) + { + if (PREDICT_TRUE (pool_is_free_index (pm->policers, + cpm->policer_index) == 0)) + return cpm->policer_index; + } + + p = hash_get_mem (pm->policer_index_by_name, cpm->policer_name); + if (!p) + return ~0; + + cpm->policer_index = (u32) p[0]; + return cpm->policer_index; +} + +static_always_inline int +copp_ip2me_policer_addr_match (copp_ip2me_policer_main_t *cpm, u32 dst_addr) +{ + for (u32 i = 0; i < cpm->n_addrs; i++) + { + if (cpm->addrs[i].in_use && cpm->addrs[i].addr == dst_addr) + return 1; + } + return 0; +} + +/* + * Process one packet: check the destination IPv4 address against the + * IP2ME set, meter matches with the shared policer, and pick the next + * node. A conforming/unmatched packet CONTINUES on the ip4-punt arc + * (i.e. reaches ip4-punt-redirect next, unmodified) -- this node never + * redirects to a TAP itself, unlike copp_punt_policer's device-input + * node, since ip4-punt-redirect already does that for every packet + * that reaches it. + */ +static_always_inline copp_ip2me_policer_error_t +copp_ip2me_policer_x1 (vlib_main_t *vm, copp_ip2me_policer_main_t *cpm, + vlib_buffer_t *b, u16 *next, u32 *out_dst_addr, + u32 *out_policer_index, u32 *out_verdict, + int *out_matched) +{ + ip4_header_t *ip4; + u32 feat_next; + u32 dst_addr; + + vnet_feature_next (&feat_next, b); + *next = (u16) feat_next; + *out_matched = 0; + + if (PREDICT_FALSE (!cpm->policer_bound || + b->current_length < sizeof (ip4_header_t))) + { + *out_dst_addr = 0; + *out_policer_index = ~0; + *out_verdict = POLICE_CONFORM; + return COPP_IP2ME_POLICER_ERROR_PASS; + } + + ip4 = vlib_buffer_get_current (b); + dst_addr = ip4->dst_address.as_u32; + *out_dst_addr = dst_addr; + + if (!copp_ip2me_policer_addr_match (cpm, dst_addr)) + { + /* Not an address we're tracking -- pass through unaffected */ + *out_policer_index = ~0; + *out_verdict = POLICE_CONFORM; + return COPP_IP2ME_POLICER_ERROR_PASS; + } + + *out_matched = 1; + + { + u32 policer_index = copp_ip2me_policer_resolve_index (cpm); + *out_policer_index = policer_index; + + if (PREDICT_FALSE (policer_index == ~0)) + { + /* Bound but the named policer doesn't exist in VPP yet -- + * drop rather than silently letting through unpoliced */ + *out_verdict = POLICE_VIOLATE; + *next = ~0; /* set by caller to NEXT_DROP */ + return COPP_IP2ME_POLICER_ERROR_DROP_UNRESOLVED; + } + + { + policer_main_t *pm = policer_get_main (); + + if (PREDICT_FALSE (pm == 0)) + { + *out_verdict = POLICE_VIOLATE; + *next = ~0; + return COPP_IP2ME_POLICER_ERROR_DROP_UNRESOLVED; + } + + policer_t *policer = pool_elt_at_index (pm->policers, policer_index); + /* Same 256-byte reference length convention copp_punt_policer + * uses, matching VPP's own pps-mode policer calibration. */ + u32 metered_len = 256; + policer_result_e verdict = vnet_police_packet ( + policer, metered_len, POLICE_CONFORM, + clib_cpu_time_now () >> POLICER_TICKS_PER_PERIOD_SHIFT); + + vlib_combined_counter_main_t *pc = policer_get_counters (); + if (PREDICT_TRUE (pc != 0)) + vlib_increment_combined_counter ( + &pc[verdict], vm->thread_index, policer_index, 1, metered_len); + + *out_verdict = verdict; + + if (PREDICT_FALSE (verdict != POLICE_CONFORM)) + { + *next = ~0; + return COPP_IP2ME_POLICER_ERROR_DROP_EXCEED; + } + } + } + + /* Conform: leave *next as the feature-arc's own "continue" next index + * (already set via vnet_feature_next() above) -- i.e. proceed to + * ip4-punt-redirect exactly as if this feature were never enabled. */ + return COPP_IP2ME_POLICER_ERROR_PASS; +} + +VLIB_NODE_FN (copp_ip2me_policer_node) +(vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *frame) +{ + copp_ip2me_policer_main_t *cpm = &copp_ip2me_policer_main; + u32 n_left_from, *from; + vlib_buffer_t *bufs[VLIB_FRAME_SIZE], **b; + u16 nexts[VLIB_FRAME_SIZE], *next; + u32 error_counts[COPP_IP2ME_POLICER_N_ERROR] = { 0 }; + u64 conform_delta = 0, exceed_delta = 0, violate_delta = 0; + + from = vlib_frame_vector_args (frame); + n_left_from = frame->n_vectors; + + vlib_get_buffers (vm, from, bufs, n_left_from); + b = bufs; + next = nexts; + + while (n_left_from) + { + u32 dst_addr = 0; + u32 policer_index = ~0; + u32 verdict = POLICE_CONFORM; + int matched = 0; + copp_ip2me_policer_error_t err; + + err = copp_ip2me_policer_x1 (vm, cpm, b[0], &next[0], &dst_addr, + &policer_index, &verdict, &matched); + error_counts[err]++; + + if (next[0] == (u16) ~0) + next[0] = 0; /* COPP_IP2ME_POLICER_NEXT_DROP, see below */ + + if (PREDICT_FALSE ((node->flags & VLIB_NODE_FLAG_TRACE) && matched)) + copp_ip2me_policer_elog (vm, vnet_buffer (b[0])->sw_if_index[VLIB_RX], + matched, policer_index, verdict); + + if (matched) + { + switch ((policer_result_e) verdict) + { + case POLICE_CONFORM: + conform_delta++; + break; + case POLICE_EXCEED: + exceed_delta++; + break; + case POLICE_VIOLATE: + violate_delta++; + break; + } + } + + if (PREDICT_FALSE ((node->flags & VLIB_NODE_FLAG_TRACE) && + (b[0]->flags & VLIB_BUFFER_IS_TRACED))) + { + copp_ip2me_policer_trace_t *t = + vlib_add_trace (vm, node, b[0], sizeof (*t)); + t->sw_if_index = vnet_buffer (b[0])->sw_if_index[VLIB_RX]; + t->next_index = next[0]; + t->dst_addr = dst_addr; + t->policer_index = policer_index; + t->verdict = verdict; + } + + b += 1; + next += 1; + n_left_from -= 1; + } + + vlib_buffer_enqueue_to_next (vm, node, from, nexts, frame->n_vectors); + + for (int i = 0; i < COPP_IP2ME_POLICER_N_ERROR; i++) + { + if (error_counts[i]) + vlib_node_increment_counter (vm, copp_ip2me_policer_node.index, i, + error_counts[i]); + } + + if (conform_delta) + cpm->conform_packets += conform_delta; + if (exceed_delta) + cpm->exceed_packets += exceed_delta; + if (violate_delta) + cpm->violate_packets += violate_delta; + + return frame->n_vectors; +} + +VLIB_REGISTER_NODE (copp_ip2me_policer_node) = { + .name = "copp-ip2me-policer", + .vector_size = sizeof (u32), + .format_trace = format_copp_ip2me_policer_trace, + .type = VLIB_NODE_TYPE_INTERNAL, + .n_errors = ARRAY_LEN (copp_ip2me_policer_error_strings), + .error_strings = copp_ip2me_policer_error_strings, + .n_next_nodes = 1, + .next_nodes = { + [0] = "ip4-drop", + }, +}; + +VNET_FEATURE_INIT (copp_ip2me_policer_feat, static) = { + .arc_name = "ip4-punt", + .node_name = "copp-ip2me-policer", + .runs_before = VNET_FEATURES ("ip4-punt-redirect"), +}; From f4c1b3f26d7131640b220326ce671766896b1d27 Mon Sep 17 00:00:00 2001 From: Nikhil Hegde Date: Fri, 18 Sep 2026 20:00:28 +0000 Subject: [PATCH 5/7] vppbld: fold CoPP policing into sonic_ext; add UDLD dataplane trap Retires the standalone copp_punt_policer and copp_ip2me_policer plugins, folding both into the shared sonic_ext plugin as copp_ifout_node.c (ARP/LACP/LLDP/UDLD/TTL_ERROR on the interface-output arc) and copp_ip2me_node.c (IP2ME/SNMP/SSH/BGP/BGPV6 on the ip4-punt arc, now with independently-keyed per-trap-group policer slots so removing one SAI trap's binding never disturbs another's -- see copp_ip2me_node.c). Adds copp_udld_node.c: UDLD is not Ethernet-II framed (802.3 length field, not an EtherType), so it cannot be matched by copp-ifout's ethertype/length-byte read; this node reaches UDLD via VPP's real LLC-null/LLC+SNAP+Cisco-UDLD-OUI dispatch and pre-resolves the copp-ifout entry via a buffer opaque field instead. Drops the now-stale copp_punt_policer_plugin.so enable line from startup.conf.tmpl (superseded by sonic_ext_plugin.so, already enabled). Signed-off-by: Nikhil Hegde --- docker-syncd-vpp/conf/startup.conf.tmpl | 1 - .../plugins/copp_ip2me_policer/CMakeLists.txt | 21 - .../plugins/copp_ip2me_policer/FEATURE.yaml | 26 - .../copp_ip2me_policer/copp_ip2me_policer.api | 82 --- .../copp_ip2me_policer/copp_ip2me_policer.c | 345 ----------- .../copp_ip2me_policer/copp_ip2me_policer.h | 79 --- .../copp_ip2me_policer_node.c | 351 ----------- .../plugins/copp_punt_policer/CMakeLists.txt | 31 - vppbld/plugins/copp_punt_policer/FEATURE.yaml | 30 - .../copp_punt_policer/copp_punt_policer.api | 82 --- .../copp_punt_policer/copp_punt_policer.c | 323 ----------- .../copp_punt_policer/copp_punt_policer.h | 61 -- .../copp_punt_policer_node.c | 389 ------------- vppbld/plugins/sonic_ext/CMakeLists.txt | 3 + vppbld/plugins/sonic_ext/FEATURE.yaml | 6 + vppbld/plugins/sonic_ext/capture_node.c | 7 + vppbld/plugins/sonic_ext/cli.c | 141 +++++ vppbld/plugins/sonic_ext/copp_ifout_node.c | 457 +++++++++++++++ vppbld/plugins/sonic_ext/copp_ip2me_node.c | 545 ++++++++++++++++++ vppbld/plugins/sonic_ext/copp_udld_node.c | 394 +++++++++++++ vppbld/plugins/sonic_ext/sonic_ext.api | 152 +++++ vppbld/plugins/sonic_ext/sonic_ext.c | 58 ++ vppbld/plugins/sonic_ext/sonic_ext.h | 145 +++++ vppbld/plugins/sonic_ext/sonic_ext_api.c | 125 ++++ 24 files changed, 2033 insertions(+), 1821 deletions(-) delete mode 100644 vppbld/plugins/copp_ip2me_policer/CMakeLists.txt delete mode 100644 vppbld/plugins/copp_ip2me_policer/FEATURE.yaml delete mode 100644 vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer.api delete mode 100644 vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer.c delete mode 100644 vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer.h delete mode 100644 vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer_node.c delete mode 100644 vppbld/plugins/copp_punt_policer/CMakeLists.txt delete mode 100644 vppbld/plugins/copp_punt_policer/FEATURE.yaml delete mode 100644 vppbld/plugins/copp_punt_policer/copp_punt_policer.api delete mode 100644 vppbld/plugins/copp_punt_policer/copp_punt_policer.c delete mode 100644 vppbld/plugins/copp_punt_policer/copp_punt_policer.h delete mode 100644 vppbld/plugins/copp_punt_policer/copp_punt_policer_node.c create mode 100644 vppbld/plugins/sonic_ext/copp_ifout_node.c create mode 100644 vppbld/plugins/sonic_ext/copp_ip2me_node.c create mode 100644 vppbld/plugins/sonic_ext/copp_udld_node.c diff --git a/docker-syncd-vpp/conf/startup.conf.tmpl b/docker-syncd-vpp/conf/startup.conf.tmpl index f44d2d19..a2143bcc 100644 --- a/docker-syncd-vpp/conf/startup.conf.tmpl +++ b/docker-syncd-vpp/conf/startup.conf.tmpl @@ -244,7 +244,6 @@ plugins { plugin sflow_plugin.so { enable } plugin sonic_ext_plugin.so { enable } plugin policer_plugin.so { enable } - plugin copp_punt_policer_plugin.so { enable } ## Enable all plugins by default and then selectively disable specific plugins # plugin dpdk_plugin.so { disable } diff --git a/vppbld/plugins/copp_ip2me_policer/CMakeLists.txt b/vppbld/plugins/copp_ip2me_policer/CMakeLists.txt deleted file mode 100644 index 5455958c..00000000 --- a/vppbld/plugins/copp_ip2me_policer/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (c) 2026 SONiC-VPP contributors -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at: -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -add_vpp_plugin(copp_ip2me_policer - SOURCES - copp_ip2me_policer.c - copp_ip2me_policer_node.c - - API_FILES - copp_ip2me_policer.api -) diff --git a/vppbld/plugins/copp_ip2me_policer/FEATURE.yaml b/vppbld/plugins/copp_ip2me_policer/FEATURE.yaml deleted file mode 100644 index 4d6d9a57..00000000 --- a/vppbld/plugins/copp_ip2me_policer/FEATURE.yaml +++ /dev/null @@ -1,26 +0,0 @@ ---- -name: CoPP IP2ME Policer -maintainer: SONiC-VPP contributors -features: - - Applies rate policing to IP2ME/SNMP/SSH traffic (destined to one of - the router's own IPv4 addresses that VPP's own dataplane does not - answer itself) on the ip4-punt feature arc, ahead of - ip4-punt-redirect. ip4-punt is only reached after ip4-lookup/ - ip4-local have already decided a packet is host-bound, and is a - single global arc (not per-interface), so no per-interface or - per-router-interface binding bookkeeping is needed -- unlike VPP's - existing classify-based policer-classify feature, which only meters - traffic on whichever interfaces it has been explicitly bound to. - - Reuses VPP's existing policer objects and metering primitive - (vnet_police_packet()) -- no new token-bucket implementation. - - IPv4 addresses to track are added/removed via - copp_ip2me_policer_addr_add_del (API) or the "copp ip2me policer - addr" debug CLI; the shared policer is bound via - copp_ip2me_policer_bind (API) or "copp ip2me policer bind". The - feature is enabled once, globally, at plugin init -- no - per-interface enable step, ever. - - conform/exceed/violate packet counters are exposed via - copp_ip2me_policer_get_counters (API) or "show copp ip2me policer" - (CLI). -description: "IP2ME/SNMP/SSH punt policing on ip4-punt, for SONiC/VPP CoPP" -state: experimental diff --git a/vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer.api b/vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer.api deleted file mode 100644 index 2ad0e021..00000000 --- a/vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer.api +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) 2026 SONiC-VPP contributors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -option version = "1.0.0"; - -/** \brief Add or remove one IPv4 address from the IP2ME set enforced on - the ip4-punt feature arc (device-agnostic -- addresses are checked - regardless of which interface the packet arrived on, since ip4-punt - only runs after routing has already decided the packet is destined - to one of the router's own addresses). - - @param client_index - opaque cookie to identify the sender - @param context - sender context, to match reply w/ request - @param addr - the IPv4 address (network byte order) to add/remove - @param is_add - 1 to add, 0 to remove -*/ -define copp_ip2me_policer_addr_add_del -{ - u32 client_index; - u32 context; - u32 addr; - bool is_add; -}; - -define copp_ip2me_policer_addr_add_del_reply -{ - u32 context; - i32 retval; -}; - -/** \brief Bind (or unbind) the shared IP2ME policer used to meter all - currently-registered IP2ME addresses (SSH/SNMP/IP2ME all map to the - same SAI ip2me trap and therefore the same policer). - - @param policer_name - name of an existing VPP policer object - (created via policer_add), looked up once via policer_main's - policer_index_by_name hash - @param is_bind - 1 to bind, 0 to unbind -*/ -define copp_ip2me_policer_bind -{ - u32 client_index; - u32 context; - string policer_name[64]; - bool is_bind; -}; - -define copp_ip2me_policer_bind_reply -{ - u32 context; - i32 retval; -}; - -/** \brief Read back this plugin's own conform/exceed/violate packet - counters. -*/ -define copp_ip2me_policer_get_counters -{ - u32 client_index; - u32 context; -}; - -define copp_ip2me_policer_get_counters_reply -{ - u32 context; - i32 retval; - u64 conform_packets; - u64 exceed_packets; - u64 violate_packets; -}; diff --git a/vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer.c b/vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer.c deleted file mode 100644 index 039973e7..00000000 --- a/vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer.c +++ /dev/null @@ -1,345 +0,0 @@ -/* - * Copyright (c) 2026 SONiC-VPP contributors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * copp_ip2me_policer: CoPP enforcement for IP2ME/SNMP/SSH traffic -- - * traffic destined to one of the router's own IPv4 addresses that VPP's - * dataplane does not answer itself (see sonic-net/sonic-buildimage#25801, - * SONiC-on-VPP CoPP HLD). - * - * BACKGROUND: VPP's built-in classify-based policer feature - * (ip4-policer-classify, on the ip4-unicast arc) only meters traffic on - * whichever interface it has been explicitly bound to. Since the - * classify table matches purely on destination IP, IP2ME traffic - * destined to router-interface A's address can legitimately arrive on - * router-interface B -- if B never had the feature bound, that traffic - * skips policing entirely and reaches VPP's ip4-punt-redirect mechanism - * completely unpoliced. Binding to every possible L3 ingress interface - * is the fix VPP's own classify feature requires, but it needs lazy, - * per-RIF VAPI calls (deferred to avoid syncd's SAI-call watchdog) and - * has proven fragile in practice. - * - * This plugin avoids the whole binding-scope problem by living on - * ip4-punt instead: a single, always-on, global feature arc that every - * packet reaching this point has already been routed through - * (ip4-lookup -> ip4-local -> ip4-punt), regardless of which interface - * it arrived on. No per-interface binding is needed -- ip4-punt is - * reached the same way no matter the ingress interface, once VPP's own - * dataplane has already concluded "nothing here handles this packet, it - * needs the host." That is exactly SAI's IP2ME semantics, so this - * plugin only needs to answer one question (is the destination address - * one we're tracking?) and apply the existing SAI-created policer - * object, then let the packet continue unchanged to ip4-punt-redirect - * (conform) or drop it (exceed/violate) -- it never needs to redirect - * to a TAP itself, unlike copp_punt_policer's device-input node, since - * ip4-punt-redirect already does that for every packet that reaches it. - */ - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include -#include - -#define REPLY_MSG_ID_BASE sm->msg_id_base -#include - -VLIB_PLUGIN_REGISTER () = { - .version = COPP_IP2ME_POLICER_PLUGIN_BUILD_VER, - .description = "CoPP IP2ME/SNMP/SSH policing on the ip4-punt arc", -}; - -copp_ip2me_policer_main_t copp_ip2me_policer_main; - -/* - * Find an existing entry for this address, or -1 if none. Linear scan - * over a small, bounded array (COPP_IP2ME_POLICER_MAX_ADDRS) -- this is - * control-plane/config-time code, not the per-packet data path (see - * copp_ip2me_policer_node.c for that). - */ -static int -copp_ip2me_policer_find_addr (copp_ip2me_policer_main_t *cpm, u32 addr) -{ - for (u32 i = 0; i < cpm->n_addrs; i++) - { - if (cpm->addrs[i].in_use && cpm->addrs[i].addr == addr) - return (int) i; - } - return -1; -} - -int -copp_ip2me_policer_addr_add_del (u32 addr, int is_add) -{ - copp_ip2me_policer_main_t *cpm = &copp_ip2me_policer_main; - int idx = copp_ip2me_policer_find_addr (cpm, addr); - - if (!is_add) - { - if (idx < 0) - return 0; - clib_memset (&cpm->addrs[idx], 0, sizeof (cpm->addrs[idx])); - return 0; - } - - if (idx >= 0) - return 0; /* already present */ - - if (cpm->n_addrs >= COPP_IP2ME_POLICER_MAX_ADDRS) - return VNET_API_ERROR_QUEUE_FULL; - - idx = (int) cpm->n_addrs++; - cpm->addrs[idx].addr = addr; - cpm->addrs[idx].in_use = 1; - - return 0; -} - -int -copp_ip2me_policer_bind (const char *policer_name, int is_bind) -{ - copp_ip2me_policer_main_t *cpm = &copp_ip2me_policer_main; - - if (!is_bind) - { - clib_memset (cpm->policer_name, 0, sizeof (cpm->policer_name)); - cpm->policer_index = ~0; - cpm->policer_bound = 0; - cpm->conform_packets = 0; - cpm->exceed_packets = 0; - cpm->violate_packets = 0; - return 0; - } - - snprintf ((char *) cpm->policer_name, sizeof (cpm->policer_name), "%s", - policer_name); - cpm->policer_index = ~0; - cpm->policer_bound = 1; - cpm->conform_packets = 0; - cpm->exceed_packets = 0; - cpm->violate_packets = 0; - - return 0; -} - -static void -vl_api_copp_ip2me_policer_addr_add_del_t_handler ( - vl_api_copp_ip2me_policer_addr_add_del_t *mp) -{ - copp_ip2me_policer_main_t *sm = &copp_ip2me_policer_main; - vl_api_copp_ip2me_policer_addr_add_del_reply_t *rmp; - int rv; - - rv = copp_ip2me_policer_addr_add_del (mp->addr, mp->is_add); - - REPLY_MACRO (VL_API_COPP_IP2ME_POLICER_ADDR_ADD_DEL_REPLY); -} - -static void -vl_api_copp_ip2me_policer_bind_t_handler ( - vl_api_copp_ip2me_policer_bind_t *mp) -{ - copp_ip2me_policer_main_t *sm = &copp_ip2me_policer_main; - vl_api_copp_ip2me_policer_bind_reply_t *rmp; - int rv; - char name[64]; - - snprintf (name, sizeof (name), "%s", mp->policer_name); - rv = copp_ip2me_policer_bind (name, mp->is_bind); - - REPLY_MACRO (VL_API_COPP_IP2ME_POLICER_BIND_REPLY); -} - -static void -vl_api_copp_ip2me_policer_get_counters_t_handler ( - vl_api_copp_ip2me_policer_get_counters_t *mp) -{ - copp_ip2me_policer_main_t *sm = &copp_ip2me_policer_main; - vl_api_copp_ip2me_policer_get_counters_reply_t *rmp; - int rv = 0; - - REPLY_MACRO2 (VL_API_COPP_IP2ME_POLICER_GET_COUNTERS_REPLY, - ({ - rmp->conform_packets = clib_host_to_net_u64 (sm->conform_packets); - rmp->exceed_packets = clib_host_to_net_u64 (sm->exceed_packets); - rmp->violate_packets = clib_host_to_net_u64 (sm->violate_packets); - })); -} - -/* API definitions */ -#include - -static clib_error_t * -copp_ip2me_policer_init (vlib_main_t *vm) -{ - copp_ip2me_policer_main_t *cpm = &copp_ip2me_policer_main; - - cpm->msg_id_base = setup_message_id_table (); - cpm->vlib_main = vm; - cpm->vnet_main = vnet_get_main (); - cpm->n_addrs = 0; - cpm->policer_index = ~0; - cpm->policer_bound = 0; - - /* - * ip4-punt is a global feature arc, not per-interface -- enable this - * feature on it ONCE at init, unconditionally, the same way - * ip4-punt-redirect itself is always enabled. No per-interface - * enable/disable call is needed or ever made (see - * copp_punt_policer_sw_interface_add_del in the sibling - * copp_punt_policer plugin for contrast -- that one has to do this - * per-interface because device-input is a per-interface arc; ip4-punt - * is not). - */ - vnet_feature_enable_disable ("ip4-punt", "copp-ip2me-policer", 0, 1, 0, 0); - - return 0; -} - -VLIB_INIT_FUNCTION (copp_ip2me_policer_init); - -/* CLI: bind/unbind policer, for manual testing and a scriptable non-API path */ -static clib_error_t * -copp_ip2me_policer_bind_command_fn (vlib_main_t *vm, - unformat_input_t *input, - vlib_cli_command_t *cmd) -{ - u8 *policer_name = 0; - int is_bind = 1; - clib_error_t *error = 0; - - while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT) - { - if (unformat (input, "policer %s", &policer_name)) - ; - else if (unformat (input, "del")) - is_bind = 0; - else - { - error = clib_error_return (0, "unknown input `%U'", - format_unformat_error, input); - goto done; - } - } - - if (is_bind && !policer_name) - { - error = clib_error_return (0, "usage: copp ip2me policer bind " - "policer [del]"); - goto done; - } - - { - int rv = copp_ip2me_policer_bind ( - policer_name ? (char *) policer_name : "", is_bind); - if (rv) - error = clib_error_return (0, "bind failed: rv %d", rv); - } - -done: - vec_free (policer_name); - return error; -} - -VLIB_CLI_COMMAND (copp_ip2me_policer_bind_command, static) = { - .path = "copp ip2me policer bind", - .short_help = "copp ip2me policer bind policer [del]", - .function = copp_ip2me_policer_bind_command_fn, -}; - -/* CLI: add/remove an IP2ME address, for manual testing */ -static clib_error_t * -copp_ip2me_policer_addr_command_fn (vlib_main_t *vm, - unformat_input_t *input, - vlib_cli_command_t *cmd) -{ - ip4_address_t addr; - int have_addr = 0; - int is_add = 1; - clib_error_t *error = 0; - - while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT) - { - if (unformat (input, "%U", unformat_ip4_address, &addr)) - have_addr = 1; - else if (unformat (input, "del")) - is_add = 0; - else - { - error = clib_error_return (0, "unknown input `%U'", - format_unformat_error, input); - goto done; - } - } - - if (!have_addr) - { - error = clib_error_return (0, "usage: copp ip2me policer addr " - " [del]"); - goto done; - } - - { - int rv = copp_ip2me_policer_addr_add_del (addr.as_u32, is_add); - if (rv) - error = clib_error_return (0, "addr add/del failed: rv %d", rv); - } - -done: - return error; -} - -VLIB_CLI_COMMAND (copp_ip2me_policer_addr_command, static) = { - .path = "copp ip2me policer addr", - .short_help = "copp ip2me policer addr [del]", - .function = copp_ip2me_policer_addr_command_fn, -}; - -static clib_error_t * -copp_ip2me_policer_show_command_fn (vlib_main_t *vm, - unformat_input_t *input, - vlib_cli_command_t *cmd) -{ - copp_ip2me_policer_main_t *cpm = &copp_ip2me_policer_main; - - vlib_cli_output (vm, "policer: %s (index %d) bound=%d", - cpm->policer_bound ? (char *) cpm->policer_name : "-", - (i32) cpm->policer_index, cpm->policer_bound); - vlib_cli_output (vm, "conform %llu exceed %llu violate %llu", - cpm->conform_packets, cpm->exceed_packets, - cpm->violate_packets); - vlib_cli_output (vm, "%-8s addresses:", "count"); - vlib_cli_output (vm, "%u", cpm->n_addrs); - for (u32 i = 0; i < cpm->n_addrs; i++) - { - if (!cpm->addrs[i].in_use) - continue; - vlib_cli_output (vm, " %U", format_ip4_address, &cpm->addrs[i].addr); - } - - return 0; -} - -VLIB_CLI_COMMAND (copp_ip2me_policer_show_command, static) = { - .path = "show copp ip2me policer", - .short_help = "show copp ip2me policer", - .function = copp_ip2me_policer_show_command_fn, -}; diff --git a/vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer.h b/vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer.h deleted file mode 100644 index 9cb698bc..00000000 --- a/vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer.h +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) 2026 SONiC-VPP contributors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#ifndef __included_copp_ip2me_policer_h__ -#define __included_copp_ip2me_policer_h__ - -#include - -/* - * IPv4 destination addresses considered "IP2ME" -- traffic destined to - * one of the router's own addresses that VPP's own dataplane doesn't - * answer itself (i.e. anything that reaches ip4-punt at all: SSH, SNMP, - * unregistered ports, ...). Enforced on the ip4-punt feature arc, which - * VPP only reaches AFTER ip4-lookup/ip4-local have already decided this - * packet is host-bound -- unlike device-input (too early: dest-IP alone - * doesn't mean CPU-bound, e.g. ping-to-router is answered in-dataplane) - * or VPP's built-in ip4-policer-classify feature (which only matches on - * interfaces it's explicitly bound to, requiring per-RIF bookkeeping - * that this plugin avoids entirely by living on a global, always-on - * arc instead). - */ -#define COPP_IP2ME_POLICER_MAX_ADDRS 256 -#define COPP_IP2ME_POLICER_NAME_LEN 64 - -typedef struct -{ - u32 addr; /* network byte order, matches ip4_address_t.as_u32 */ - u8 in_use; -} copp_ip2me_policer_addr_t; - -typedef struct -{ - /* API message ID base */ - u16 msg_id_base; - - /* IPv4 addresses currently classified as IP2ME (router's own - * addresses reachable via ip4-punt). Small, bounded, linear-scanned - * array -- looked up once per packet in the data path, so kept - * simple; COPP_IP2ME_POLICER_MAX_ADDRS comfortably covers every - * router-interface address on this testbed's largest topology. */ - copp_ip2me_policer_addr_t addrs[COPP_IP2ME_POLICER_MAX_ADDRS]; - u32 n_addrs; - - /* Single policer binding -- the SAI ip2me trap's policer (shared by - * IP2ME/SNMP/SSH; see PROTOCOL_TO_TRAP_ID in sonic-mgmt's - * copp_tests.py -- all three trap to the same SAI_HOSTIF_TRAP_TYPE_ - * IP2ME). Looked up lazily by name the same way copp_punt_policer - * does, so bind order relative to policer_add() doesn't matter. */ - u8 policer_name[COPP_IP2ME_POLICER_NAME_LEN]; - u32 policer_index; - u8 policer_bound; - - /* conform/exceed/violate packet counters */ - u64 conform_packets; - u64 exceed_packets; - u64 violate_packets; - - vlib_main_t *vlib_main; - vnet_main_t *vnet_main; -} copp_ip2me_policer_main_t; - -extern copp_ip2me_policer_main_t copp_ip2me_policer_main; - -extern vlib_node_registration_t copp_ip2me_policer_node; - -#define COPP_IP2ME_POLICER_PLUGIN_BUILD_VER "1.0" - -#endif /* __included_copp_ip2me_policer_h__ */ diff --git a/vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer_node.c b/vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer_node.c deleted file mode 100644 index 58d0b48c..00000000 --- a/vppbld/plugins/copp_ip2me_policer/copp_ip2me_policer_node.c +++ /dev/null @@ -1,351 +0,0 @@ -/* - * Copyright (c) 2026 SONiC-VPP contributors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Per-packet data path for the copp_ip2me_policer feature. Runs on the - * ip4-punt arc, AFTER ip4-lookup/ip4-local have already decided this - * packet is destined to a local address and nothing in VPP's own - * dataplane handles it (that decision is what routes it to ip4-punt in - * the first place -- see ip4_local_set_next_and_error() in - * src/vnet/ip/ip4_forward.c). ip4-local never advances the buffer past - * the IP header, so vlib_buffer_get_current() here still points at the - * start of the IPv4 header -- no offset math needed, unlike - * device-input-based classification which runs before the Ethernet - * header has been stripped. - */ - -#include -#include -#include -#include -#include -#include -#include -#include - -ELOG_TYPE_DECLARE (copp_ip2me_policer_elog_seen) = { - .format = "copp-ip2me-policer: sw_if_index %d matched %d policer_index %d " - "verdict %d", - .format_args = "i4i4i4i4", -}; - -typedef struct -{ - u32 sw_if_index; - u32 matched; - u32 policer_index; - u32 verdict; -} __clib_packed copp_ip2me_policer_elog_data_t; - -static_always_inline void -copp_ip2me_policer_elog (vlib_main_t *vm, u32 sw_if_index, int matched, - u32 policer_index, u32 verdict) -{ - elog_main_t *em = vlib_get_elog_main (); - copp_ip2me_policer_elog_data_t *ed; - - ed = ELOG_DATA (em, copp_ip2me_policer_elog_seen); - ed->sw_if_index = sw_if_index; - ed->matched = matched; - ed->policer_index = policer_index; - ed->verdict = verdict; -} - -typedef struct -{ - u32 sw_if_index; - u32 next_index; - u32 dst_addr; - u32 policer_index; - u32 verdict; /* policer_result_e */ -} copp_ip2me_policer_trace_t; - -static u8 * -format_copp_ip2me_policer_trace (u8 *s, va_list *args) -{ - CLIB_UNUSED (vlib_main_t * vm) = va_arg (*args, vlib_main_t *); - CLIB_UNUSED (vlib_node_t * node) = va_arg (*args, vlib_node_t *); - copp_ip2me_policer_trace_t *t = va_arg (*args, copp_ip2me_policer_trace_t *); - - s = format (s, - "COPP-IP2ME-POLICER: sw_if_index %d next %d dst %U " - "policer_index %d verdict %d", - t->sw_if_index, t->next_index, format_ip4_address, - &t->dst_addr, t->policer_index, t->verdict); - return s; -} - -#define foreach_copp_ip2me_policer_error \ - _ (PASS, "packets passed (unmatched address or conform)") \ - _ (DROP_EXCEED, "packets dropped (policer exceed/violate)") \ - _ (DROP_UNRESOLVED, "packets dropped (policer name not yet resolvable)") - -typedef enum -{ -#define _(sym, str) COPP_IP2ME_POLICER_ERROR_##sym, - foreach_copp_ip2me_policer_error -#undef _ - COPP_IP2ME_POLICER_N_ERROR, -} copp_ip2me_policer_error_t; - -static char *copp_ip2me_policer_error_strings[] = { -#define _(sym, string) string, - foreach_copp_ip2me_policer_error -#undef _ -}; - -/* - * Resolve VPP policer_index by name, lazily -- same pattern as - * copp_punt_policer_node.c, so bind order relative to policer_add() - * doesn't matter and a later policer_update() recreating the object - * under the same name is picked up automatically. - */ -static_always_inline u32 -copp_ip2me_policer_resolve_index (copp_ip2me_policer_main_t *cpm) -{ - policer_main_t *pm = policer_get_main (); - uword *p; - - if (PREDICT_FALSE (pm == 0)) - return ~0; - - if (PREDICT_TRUE (cpm->policer_index != ~0)) - { - if (PREDICT_TRUE (pool_is_free_index (pm->policers, - cpm->policer_index) == 0)) - return cpm->policer_index; - } - - p = hash_get_mem (pm->policer_index_by_name, cpm->policer_name); - if (!p) - return ~0; - - cpm->policer_index = (u32) p[0]; - return cpm->policer_index; -} - -static_always_inline int -copp_ip2me_policer_addr_match (copp_ip2me_policer_main_t *cpm, u32 dst_addr) -{ - for (u32 i = 0; i < cpm->n_addrs; i++) - { - if (cpm->addrs[i].in_use && cpm->addrs[i].addr == dst_addr) - return 1; - } - return 0; -} - -/* - * Process one packet: check the destination IPv4 address against the - * IP2ME set, meter matches with the shared policer, and pick the next - * node. A conforming/unmatched packet CONTINUES on the ip4-punt arc - * (i.e. reaches ip4-punt-redirect next, unmodified) -- this node never - * redirects to a TAP itself, unlike copp_punt_policer's device-input - * node, since ip4-punt-redirect already does that for every packet - * that reaches it. - */ -static_always_inline copp_ip2me_policer_error_t -copp_ip2me_policer_x1 (vlib_main_t *vm, copp_ip2me_policer_main_t *cpm, - vlib_buffer_t *b, u16 *next, u32 *out_dst_addr, - u32 *out_policer_index, u32 *out_verdict, - int *out_matched) -{ - ip4_header_t *ip4; - u32 feat_next; - u32 dst_addr; - - vnet_feature_next (&feat_next, b); - *next = (u16) feat_next; - *out_matched = 0; - - if (PREDICT_FALSE (!cpm->policer_bound || - b->current_length < sizeof (ip4_header_t))) - { - *out_dst_addr = 0; - *out_policer_index = ~0; - *out_verdict = POLICE_CONFORM; - return COPP_IP2ME_POLICER_ERROR_PASS; - } - - ip4 = vlib_buffer_get_current (b); - dst_addr = ip4->dst_address.as_u32; - *out_dst_addr = dst_addr; - - if (!copp_ip2me_policer_addr_match (cpm, dst_addr)) - { - /* Not an address we're tracking -- pass through unaffected */ - *out_policer_index = ~0; - *out_verdict = POLICE_CONFORM; - return COPP_IP2ME_POLICER_ERROR_PASS; - } - - *out_matched = 1; - - { - u32 policer_index = copp_ip2me_policer_resolve_index (cpm); - *out_policer_index = policer_index; - - if (PREDICT_FALSE (policer_index == ~0)) - { - /* Bound but the named policer doesn't exist in VPP yet -- - * drop rather than silently letting through unpoliced */ - *out_verdict = POLICE_VIOLATE; - *next = ~0; /* set by caller to NEXT_DROP */ - return COPP_IP2ME_POLICER_ERROR_DROP_UNRESOLVED; - } - - { - policer_main_t *pm = policer_get_main (); - - if (PREDICT_FALSE (pm == 0)) - { - *out_verdict = POLICE_VIOLATE; - *next = ~0; - return COPP_IP2ME_POLICER_ERROR_DROP_UNRESOLVED; - } - - policer_t *policer = pool_elt_at_index (pm->policers, policer_index); - /* Same 256-byte reference length convention copp_punt_policer - * uses, matching VPP's own pps-mode policer calibration. */ - u32 metered_len = 256; - policer_result_e verdict = vnet_police_packet ( - policer, metered_len, POLICE_CONFORM, - clib_cpu_time_now () >> POLICER_TICKS_PER_PERIOD_SHIFT); - - vlib_combined_counter_main_t *pc = policer_get_counters (); - if (PREDICT_TRUE (pc != 0)) - vlib_increment_combined_counter ( - &pc[verdict], vm->thread_index, policer_index, 1, metered_len); - - *out_verdict = verdict; - - if (PREDICT_FALSE (verdict != POLICE_CONFORM)) - { - *next = ~0; - return COPP_IP2ME_POLICER_ERROR_DROP_EXCEED; - } - } - } - - /* Conform: leave *next as the feature-arc's own "continue" next index - * (already set via vnet_feature_next() above) -- i.e. proceed to - * ip4-punt-redirect exactly as if this feature were never enabled. */ - return COPP_IP2ME_POLICER_ERROR_PASS; -} - -VLIB_NODE_FN (copp_ip2me_policer_node) -(vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *frame) -{ - copp_ip2me_policer_main_t *cpm = &copp_ip2me_policer_main; - u32 n_left_from, *from; - vlib_buffer_t *bufs[VLIB_FRAME_SIZE], **b; - u16 nexts[VLIB_FRAME_SIZE], *next; - u32 error_counts[COPP_IP2ME_POLICER_N_ERROR] = { 0 }; - u64 conform_delta = 0, exceed_delta = 0, violate_delta = 0; - - from = vlib_frame_vector_args (frame); - n_left_from = frame->n_vectors; - - vlib_get_buffers (vm, from, bufs, n_left_from); - b = bufs; - next = nexts; - - while (n_left_from) - { - u32 dst_addr = 0; - u32 policer_index = ~0; - u32 verdict = POLICE_CONFORM; - int matched = 0; - copp_ip2me_policer_error_t err; - - err = copp_ip2me_policer_x1 (vm, cpm, b[0], &next[0], &dst_addr, - &policer_index, &verdict, &matched); - error_counts[err]++; - - if (next[0] == (u16) ~0) - next[0] = 0; /* COPP_IP2ME_POLICER_NEXT_DROP, see below */ - - if (PREDICT_FALSE ((node->flags & VLIB_NODE_FLAG_TRACE) && matched)) - copp_ip2me_policer_elog (vm, vnet_buffer (b[0])->sw_if_index[VLIB_RX], - matched, policer_index, verdict); - - if (matched) - { - switch ((policer_result_e) verdict) - { - case POLICE_CONFORM: - conform_delta++; - break; - case POLICE_EXCEED: - exceed_delta++; - break; - case POLICE_VIOLATE: - violate_delta++; - break; - } - } - - if (PREDICT_FALSE ((node->flags & VLIB_NODE_FLAG_TRACE) && - (b[0]->flags & VLIB_BUFFER_IS_TRACED))) - { - copp_ip2me_policer_trace_t *t = - vlib_add_trace (vm, node, b[0], sizeof (*t)); - t->sw_if_index = vnet_buffer (b[0])->sw_if_index[VLIB_RX]; - t->next_index = next[0]; - t->dst_addr = dst_addr; - t->policer_index = policer_index; - t->verdict = verdict; - } - - b += 1; - next += 1; - n_left_from -= 1; - } - - vlib_buffer_enqueue_to_next (vm, node, from, nexts, frame->n_vectors); - - for (int i = 0; i < COPP_IP2ME_POLICER_N_ERROR; i++) - { - if (error_counts[i]) - vlib_node_increment_counter (vm, copp_ip2me_policer_node.index, i, - error_counts[i]); - } - - if (conform_delta) - cpm->conform_packets += conform_delta; - if (exceed_delta) - cpm->exceed_packets += exceed_delta; - if (violate_delta) - cpm->violate_packets += violate_delta; - - return frame->n_vectors; -} - -VLIB_REGISTER_NODE (copp_ip2me_policer_node) = { - .name = "copp-ip2me-policer", - .vector_size = sizeof (u32), - .format_trace = format_copp_ip2me_policer_trace, - .type = VLIB_NODE_TYPE_INTERNAL, - .n_errors = ARRAY_LEN (copp_ip2me_policer_error_strings), - .error_strings = copp_ip2me_policer_error_strings, - .n_next_nodes = 1, - .next_nodes = { - [0] = "ip4-drop", - }, -}; - -VNET_FEATURE_INIT (copp_ip2me_policer_feat, static) = { - .arc_name = "ip4-punt", - .node_name = "copp-ip2me-policer", - .runs_before = VNET_FEATURES ("ip4-punt-redirect"), -}; diff --git a/vppbld/plugins/copp_punt_policer/CMakeLists.txt b/vppbld/plugins/copp_punt_policer/CMakeLists.txt deleted file mode 100644 index e1474500..00000000 --- a/vppbld/plugins/copp_punt_policer/CMakeLists.txt +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) 2026 SONiC-VPP contributors -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at: -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Linked against linux-cp's `lcp` library so the data-path node can look -# up the phy->host (TAP) interface pairing and punt conforming, -# CoPP-matched packets directly to the mapped TAP -- bypassing -# ethernet-input/linux-cp's own protocol-dispatch nodes entirely, so -# delivery does not depend on whichever downstream path (arp-input, -# linux-cp-punt-xc, ip4-punt-redirect, ...) would otherwise have -# handled it. -add_vpp_plugin(copp_punt_policer - SOURCES - copp_punt_policer.c - copp_punt_policer_node.c - - API_FILES - copp_punt_policer.api - - LINK_LIBRARIES - lcp -) diff --git a/vppbld/plugins/copp_punt_policer/FEATURE.yaml b/vppbld/plugins/copp_punt_policer/FEATURE.yaml deleted file mode 100644 index be1df9ae..00000000 --- a/vppbld/plugins/copp_punt_policer/FEATURE.yaml +++ /dev/null @@ -1,30 +0,0 @@ ---- -name: CoPP Punt Policer -maintainer: SONiC-VPP contributors -features: - - Applies per-ethertype rate policing to control-plane traffic on the - device-input feature arc, before ethernet-input's protocol dispatch - runs. This closes a gap left by VPP's existing classify-based - policer-classify feature, which only runs on l2-input (bridged L2 - traffic), ip4-unicast, and ip6-unicast -- arcs that ARP, LACP, LLDP, - and UDLD traffic never traverses on linux-cp-paired, L3-routed - ports (they are dispatched directly from ethernet-input to - arp-input/linux-cp-punt-xc and punted straight to the interface's - TAP). - - Reuses VPP's existing policer objects and metering primitive - (vnet_police_packet()) -- no new token-bucket implementation. - Policer objects are created/updated by external control-plane code - exactly as before (e.g. via policer_add); this plugin only adds a - second, additional consulting point for those same objects on - control-plane ethertypes that would otherwise never reach a - policer at all. - - Binding an ethertype to a policer is done via a small VPP binary - API (copp_punt_policer_bind) or the "copp punt policer bind" - debug CLI; the feature auto-enables on device-input for every - interface as it is created, so no per-interface enable step is - needed once at least one binding exists. - - Per-binding conform/exceed/violate packet counters are exposed via - copp_punt_policer_get_counters (API) or "show copp punt policer" - (CLI). -description: "Per-ethertype punt policing on device-input, for SONiC/VPP CoPP" -state: experimental diff --git a/vppbld/plugins/copp_punt_policer/copp_punt_policer.api b/vppbld/plugins/copp_punt_policer/copp_punt_policer.api deleted file mode 100644 index 846a9412..00000000 --- a/vppbld/plugins/copp_punt_policer/copp_punt_policer.api +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) 2026 SONiC-VPP contributors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -option version = "1.0.0"; - -/** \brief Bind (or unbind) an Ethernet ethertype to an existing VPP - policer object, enforced on the device-input arc for every - interface -- i.e. before ethernet-input's protocol dispatch runs, - so it catches control-plane traffic (ARP/LACP/LLDP/UDLD/etc.) that - never traverses l2-input or ip4/ip6-unicast (where VPP's existing - classify-based policer-classify feature lives). - - @param client_index - opaque cookie to identify the sender - @param context - sender context, to match reply w/ request - @param ethertype - the Ethernet ethertype to match (host byte order, - e.g. 0x0806 for ARP, 0x8809 for LACP, 0x88cc for LLDP, or - 0x0800 for match_ip4_ttl_expiring below) - @param policer_name - name of an existing VPP policer object - (created via policer_add, e.g. "copp-policer-0x"); - looked up once at bind time via policer_main's - policer_index_by_name hash - @param is_bind - 1 to add/replace the binding, 0 to remove it - @param match_ip4_ttl_expiring - when true (only meaningful with - ethertype 0x0800), additionally requires the IPv4 header's - TTL to be <= 1 for a packet to match this entry -- ordinary - IPv4 traffic (BGP, DHCP, ...) with ethertype 0x0800 keeps - passing through unclassified. This is how CoPP's TTL_ERROR - trap is implemented: reusing this plugin's proven - classify+police+direct-to-TAP mechanism instead of a new - VPP punt-reason/ICMP-generation path. -*/ -define copp_punt_policer_bind -{ - u32 client_index; - u32 context; - u16 ethertype; - string policer_name[64]; - bool is_bind; - bool match_ip4_ttl_expiring; -}; - -define copp_punt_policer_bind_reply -{ - u32 context; - i32 retval; -}; - -/** \brief Read back this plugin's own conform/exceed/violate packet - counters for one bound ethertype (independent of, and in addition - to, the underlying VPP policer object's own stats-segment - counters -- since this plugin's node is what actually applies the - verdict). - - @param ethertype - the ethertype to query counters for -*/ -define copp_punt_policer_get_counters -{ - u32 client_index; - u32 context; - u16 ethertype; -}; - -define copp_punt_policer_get_counters_reply -{ - u32 context; - i32 retval; - u64 conform_packets; - u64 exceed_packets; - u64 violate_packets; -}; diff --git a/vppbld/plugins/copp_punt_policer/copp_punt_policer.c b/vppbld/plugins/copp_punt_policer/copp_punt_policer.c deleted file mode 100644 index 3a63f4e9..00000000 --- a/vppbld/plugins/copp_punt_policer/copp_punt_policer.c +++ /dev/null @@ -1,323 +0,0 @@ -/* - * Copyright (c) 2026 SONiC-VPP contributors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * copp_punt_policer: per-ethertype policing on the device-input arc. - * - * BACKGROUND (see sonic-net/sonic-buildimage#25801, SONiC-on-VPP CoPP HLD, - * item 4/8): VPP's existing classify-based policer feature - * (policer-classify) only runs on three feature arcs: l2-input (bridged - * L2 traffic), ip4-unicast, ip6-unicast. On linux-cp-paired, L3-routed - * ports (this project's topology), ARP/LACP/LLDP/UDLD traffic never - * traverses any of those three arcs -- ethernet-input dispatches it - * directly to arp-input/linux-cp-punt-xc, which punt straight to the - * TAP. BGP/DHCP already work because they do ride ip4/ip6-unicast. - * - * This plugin closes that gap by registering a small feature node on - * device-input, the one arc every packet on a port crosses before - * ethernet-input's protocol dispatch runs. It parses just the 14-byte - * Ethernet header (cheap -- no full packet classification), looks up - * a per-ethertype policer binding, and applies VPP's existing - * vnet_police_packet() token-bucket primitive against the existing SAI- - * created VPP policer object (same policer objects SwitchVppPolicer.cpp - * already creates correctly with the right CIR/CBS from copp_cfg.json). - * No new metering implementation, no VPP core patch -- this is a - * self-contained plugin, following the same out-of-tree pattern as this - * repo's existing ip_validate/tunterm_acl plugins - * (platform/vpp/vppbld/plugins/). - */ - -#include -#include -#include -#include -#include - -#include -#include - -#include -#include - -#define REPLY_MSG_ID_BASE sm->msg_id_base -#include - -VLIB_PLUGIN_REGISTER () = { - .version = COPP_PUNT_POLICER_PLUGIN_BUILD_VER, - .description = "CoPP punt policer (per-ethertype policing on device-input)", -}; - -copp_punt_policer_main_t copp_punt_policer_main; - -/* - * Find an existing entry for this ethertype, or -1 if none. Linear scan - * over a small, bounded array (COPP_PUNT_POLICER_MAX_ENTRIES) -- this is - * control-plane/config-time code, not the per-packet data path (see - * copp_punt_policer_node.c for that). - */ -static int -copp_punt_policer_find_entry (copp_punt_policer_main_t *cpm, u16 ethertype) -{ - for (u32 i = 0; i < cpm->n_entries; i++) - { - if (cpm->entries[i].in_use && cpm->entries[i].ethertype == ethertype) - return (int) i; - } - return -1; -} - -/* - * Bind (or unbind) an ethertype -> policer-name entry. On bind, the - * named policer is looked up now via policer_main's - * policer_index_by_name hash (populated by policer_add(), which - * SwitchVppPolicer.cpp already calls for every SAI POLICER object) -- - * if the name isn't found yet, the entry is still recorded with - * policer_index left unresolved (~0); the data-path node re-attempts - * the lookup lazily so binding order relative to policer_add() doesn't - * matter. - * - * match_ip4_ttl_expiring: when set (only meaningful with ethertype == - * 0x0800), the data-path node additionally requires the packet's IPv4 - * TTL to be <= 1 to match this entry -- this is how CoPP's TTL_ERROR - * trap reuses this plugin's classify+police+direct-to-TAP mechanism, - * without policing ordinary IPv4 traffic (BGP, DHCP, ...) that also - * carries ethertype 0x0800. - * - * Thread safety: mutates cpm->entries[]/n_entries with no lock, but the - * only caller in this environment is SONiC's syncd (single VAPI client, - * main thread only), so this is unlikely to be an issue in practice. - */ -int -copp_punt_policer_bind (u16 ethertype, const char *policer_name, - int is_bind, int match_ip4_ttl_expiring) -{ - copp_punt_policer_main_t *cpm = &copp_punt_policer_main; - int idx = copp_punt_policer_find_entry (cpm, ethertype); - - if (!is_bind) - { - if (idx < 0) - return 0; - clib_memset (&cpm->entries[idx], 0, sizeof (cpm->entries[idx])); - cpm->conform_packets[idx] = 0; - cpm->exceed_packets[idx] = 0; - cpm->violate_packets[idx] = 0; - return 0; - } - - if (idx < 0) - { - if (cpm->n_entries >= COPP_PUNT_POLICER_MAX_ENTRIES) - return VNET_API_ERROR_QUEUE_FULL; - idx = (int) cpm->n_entries++; - } - - clib_memset (&cpm->entries[idx], 0, sizeof (cpm->entries[idx])); - cpm->entries[idx].ethertype = ethertype; - snprintf ((char *) cpm->entries[idx].name, - sizeof (cpm->entries[idx].name), "%s", policer_name); - cpm->entries[idx].policer_index = ~0; - cpm->entries[idx].in_use = 1; - cpm->entries[idx].match_ip4_ttl_expiring = match_ip4_ttl_expiring ? 1 : 0; - cpm->conform_packets[idx] = 0; - cpm->exceed_packets[idx] = 0; - cpm->violate_packets[idx] = 0; - - return 0; -} - -static void -vl_api_copp_punt_policer_bind_t_handler ( - vl_api_copp_punt_policer_bind_t *mp) -{ - copp_punt_policer_main_t *sm = &copp_punt_policer_main; - vl_api_copp_punt_policer_bind_reply_t *rmp; - int rv; - char name[64]; - - snprintf (name, sizeof (name), "%s", mp->policer_name); - rv = copp_punt_policer_bind (ntohs (mp->ethertype), name, mp->is_bind, - mp->match_ip4_ttl_expiring); - - REPLY_MACRO (VL_API_COPP_PUNT_POLICER_BIND_REPLY); -} - -static void -vl_api_copp_punt_policer_get_counters_t_handler ( - vl_api_copp_punt_policer_get_counters_t *mp) -{ - copp_punt_policer_main_t *sm = &copp_punt_policer_main; - vl_api_copp_punt_policer_get_counters_reply_t *rmp; - int rv = 0; - u64 conform = 0, exceed = 0, violate = 0; - int idx = copp_punt_policer_find_entry (sm, ntohs (mp->ethertype)); - - if (idx < 0) - rv = VNET_API_ERROR_NO_SUCH_ENTRY; - else - { - conform = sm->conform_packets[idx]; - exceed = sm->exceed_packets[idx]; - violate = sm->violate_packets[idx]; - } - - REPLY_MACRO2 (VL_API_COPP_PUNT_POLICER_GET_COUNTERS_REPLY, - ({ - rmp->conform_packets = clib_host_to_net_u64 (conform); - rmp->exceed_packets = clib_host_to_net_u64 (exceed); - rmp->violate_packets = clib_host_to_net_u64 (violate); - })); -} - -/* API definitions */ -#include - -static clib_error_t * -copp_punt_policer_init (vlib_main_t *vm) -{ - copp_punt_policer_main_t *cpm = &copp_punt_policer_main; - - cpm->msg_id_base = setup_message_id_table (); - cpm->vlib_main = vm; - cpm->vnet_main = vnet_get_main (); - cpm->n_entries = 0; - - /* - * One-shot sanity event, unconditional (no `elog trace`/`event-logger - * trace` arming needed to see it). If it is present in `vppctl show - * event-logger` after boot, the event-logger ring is live end-to-end - * and any absence of per-packet copp-punt-policer events from the - * data-path node (see copp_punt_policer_node.c) is a real "no packets - * reached this node" finding, not a broken/disabled logger. - */ - ELOG_TYPE_DECLARE (e) = { - .format = "copp-punt-policer: plugin initialized, elog is live", - }; - elog_main_t *em = vlib_get_elog_main (); - ELOG (em, e, 0); - - return 0; -} - -VLIB_INIT_FUNCTION (copp_punt_policer_init); - -/* - * Auto-enable the feature on every interface as it is created, exactly - * like ip_validate does for ip4/ip6-unicast -- no explicit per-interface - * API call needed. Bindings (which ethertypes map to which policers) - * are configured separately via copp_punt_policer_bind above; this only - * wires the *feature* onto device-input for the interface so the node - * runs at all. - */ -static clib_error_t * -copp_punt_policer_sw_interface_add_del (vnet_main_t *vnm, u32 sw_if_index, - u32 is_add) -{ - vnet_feature_enable_disable ("device-input", "copp-punt-policer", - sw_if_index, is_add, 0, 0); - return 0; -} - -VNET_SW_INTERFACE_ADD_DEL_FUNCTION (copp_punt_policer_sw_interface_add_del); - -/* CLI: bind/unbind, for manual testing and for a scriptable non-API path */ -static clib_error_t * -copp_punt_policer_bind_command_fn (vlib_main_t *vm, - unformat_input_t *input, - vlib_cli_command_t *cmd) -{ - u32 ethertype = 0; - u8 *policer_name = 0; - int is_bind = 1; - int match_ip4_ttl_expiring = 0; - clib_error_t *error = 0; - - while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT) - { - if (unformat (input, "ethertype 0x%x", ðertype)) - ; - else if (unformat (input, "ethertype %d", ðertype)) - ; - else if (unformat (input, "policer %s", &policer_name)) - ; - else if (unformat (input, "match-ip4-ttl-expiring")) - match_ip4_ttl_expiring = 1; - else if (unformat (input, "del")) - is_bind = 0; - else - { - error = clib_error_return (0, "unknown input `%U'", - format_unformat_error, input); - goto done; - } - } - - if (ethertype == 0 || (is_bind && !policer_name)) - { - error = clib_error_return (0, "usage: copp punt policer bind " - "ethertype <0xNNNN> policer " - "[match-ip4-ttl-expiring] [del]"); - goto done; - } - - { - int rv = copp_punt_policer_bind ((u16) ethertype, - policer_name ? (char *) policer_name : "", - is_bind, match_ip4_ttl_expiring); - if (rv) - error = clib_error_return (0, "bind failed: rv %d", rv); - } - -done: - vec_free (policer_name); - return error; -} - -VLIB_CLI_COMMAND (copp_punt_policer_bind_command, static) = { - .path = "copp punt policer bind", - .short_help = "copp punt policer bind ethertype <0xNNNN> policer " - "[match-ip4-ttl-expiring] [del]", - .function = copp_punt_policer_bind_command_fn, -}; - -static clib_error_t * -copp_punt_policer_show_command_fn (vlib_main_t *vm, - unformat_input_t *input, - vlib_cli_command_t *cmd) -{ - copp_punt_policer_main_t *cpm = &copp_punt_policer_main; - - vlib_cli_output (vm, "%-8s %-40s %-12s %10s %10s %10s %s", - "ethtype", "policer-name", "vpp-idx", - "conform", "exceed", "violate", "match"); - for (u32 i = 0; i < cpm->n_entries; i++) - { - if (!cpm->entries[i].in_use) - continue; - vlib_cli_output (vm, "0x%04x %-40s %-12d %10llu %10llu %10llu %s", - cpm->entries[i].ethertype, cpm->entries[i].name, - (i32) cpm->entries[i].policer_index, - cpm->conform_packets[i], cpm->exceed_packets[i], - cpm->violate_packets[i], - cpm->entries[i].match_ip4_ttl_expiring ? - "ip4-ttl<=1" : "-"); - } - - return 0; -} - -VLIB_CLI_COMMAND (copp_punt_policer_show_command, static) = { - .path = "show copp punt policer", - .short_help = "show copp punt policer", - .function = copp_punt_policer_show_command_fn, -}; diff --git a/vppbld/plugins/copp_punt_policer/copp_punt_policer.h b/vppbld/plugins/copp_punt_policer/copp_punt_policer.h deleted file mode 100644 index 2c0f9d52..00000000 --- a/vppbld/plugins/copp_punt_policer/copp_punt_policer.h +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2026 SONiC-VPP contributors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#ifndef __included_copp_punt_policer_h__ -#define __included_copp_punt_policer_h__ - -#include -#include - -/* - * A single ethertype -> policer-name binding, keyed by the raw Ethernet - * ethertype field. - */ -#define COPP_PUNT_POLICER_MAX_ENTRIES 16 -#define COPP_PUNT_POLICER_NAME_LEN 64 - -typedef struct -{ - u16 ethertype; /* host byte order */ - u8 name[COPP_PUNT_POLICER_NAME_LEN]; - u32 policer_index; - u8 in_use; - u8 match_ip4_ttl_expiring; /* TTL_ERROR trap */ -} copp_punt_policer_entry_t; - -typedef struct -{ - /* API message ID base */ - u16 msg_id_base; - - /* ethertype -> policer binding table */ - copp_punt_policer_entry_t entries[COPP_PUNT_POLICER_MAX_ENTRIES]; - u32 n_entries; - - /* per-entry conform/exceed/violate packet counters */ - u64 conform_packets[COPP_PUNT_POLICER_MAX_ENTRIES]; - u64 exceed_packets[COPP_PUNT_POLICER_MAX_ENTRIES]; - u64 violate_packets[COPP_PUNT_POLICER_MAX_ENTRIES]; - - vlib_main_t *vlib_main; - vnet_main_t *vnet_main; -} copp_punt_policer_main_t; - -extern copp_punt_policer_main_t copp_punt_policer_main; - -extern vlib_node_registration_t copp_punt_policer_node; - -#define COPP_PUNT_POLICER_PLUGIN_BUILD_VER "1.0" - -#endif /* __included_copp_punt_policer_h__ */ diff --git a/vppbld/plugins/copp_punt_policer/copp_punt_policer_node.c b/vppbld/plugins/copp_punt_policer/copp_punt_policer_node.c deleted file mode 100644 index e9d6b643..00000000 --- a/vppbld/plugins/copp_punt_policer/copp_punt_policer_node.c +++ /dev/null @@ -1,389 +0,0 @@ -/* - * Copyright (c) 2026 SONiC-VPP contributors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Per-packet data path for the copp_punt_policer feature. Runs on - * device-input, before ethernet-input's protocol dispatch -- the buffer's - * current data pointer is the start of the raw Ethernet frame as - * received from the interface, so this reads the 14-byte header - * directly (no ethernet-input-provided L2 metadata to rely on yet). - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -ELOG_TYPE_DECLARE (copp_punt_policer_elog_seen) = { - .format = "copp-punt-policer: sw_if_index %d ethertype 0x%x matched %d " - "policer_index %d verdict %d", - .format_args = "i4i4i4i4i4", -}; - -typedef struct -{ - u32 sw_if_index; - u32 ethertype; - u32 matched; - u32 policer_index; - u32 verdict; -} __clib_packed copp_punt_policer_elog_data_t; - -static_always_inline void -copp_punt_policer_elog (vlib_main_t *vm, u32 sw_if_index, u16 ethertype, - int matched, u32 policer_index, u32 verdict) -{ - elog_main_t *em = vlib_get_elog_main (); - copp_punt_policer_elog_data_t *ed; - - ed = ELOG_DATA (em, copp_punt_policer_elog_seen); - ed->sw_if_index = sw_if_index; - ed->ethertype = ethertype; - ed->matched = matched >= 0; - ed->policer_index = policer_index; - ed->verdict = verdict; -} - -typedef struct -{ - u32 sw_if_index; - u32 next_index; - u16 ethertype; - u32 policer_index; - u32 verdict; /* policer_result_e */ -} copp_punt_policer_trace_t; - -static u8 * -format_copp_punt_policer_trace (u8 *s, va_list *args) -{ - CLIB_UNUSED (vlib_main_t * vm) = va_arg (*args, vlib_main_t *); - CLIB_UNUSED (vlib_node_t * node) = va_arg (*args, vlib_node_t *); - copp_punt_policer_trace_t *t = va_arg (*args, copp_punt_policer_trace_t *); - - s = format (s, - "COPP-PUNT-POLICER: sw_if_index %d next %d ethertype 0x%04x " - "policer_index %d verdict %d", - t->sw_if_index, t->next_index, t->ethertype, t->policer_index, - t->verdict); - return s; -} - -#define foreach_copp_punt_policer_error \ - _ (PASS, "packets passed (unmatched ethertype or conform)") \ - _ (DROP_EXCEED, "packets dropped (policer exceed/violate)") \ - _ (DROP_UNRESOLVED, "packets dropped (policer name not yet resolvable)") - -typedef enum -{ -#define _(sym, str) COPP_PUNT_POLICER_ERROR_##sym, - foreach_copp_punt_policer_error -#undef _ - COPP_PUNT_POLICER_N_ERROR, -} copp_punt_policer_error_t; - -static char *copp_punt_policer_error_strings[] = { -#define _(sym, string) string, - foreach_copp_punt_policer_error -#undef _ -}; - -typedef enum -{ - COPP_PUNT_POLICER_NEXT_DROP, - COPP_PUNT_POLICER_NEXT_INTERFACE_OUTPUT, - COPP_PUNT_POLICER_N_NEXT, -} copp_punt_policer_next_t; - -/* - * Resolve VPP policer_index for a bound entry by name. - * Looked up lazily so bind-before-policer-exists (or a later - * policer_update recreating the object under the same name) both work - * without requiring bind order to match creation order. - */ -static_always_inline u32 -copp_punt_policer_resolve_index (copp_punt_policer_entry_t *entry) -{ - policer_main_t *pm = policer_get_main (); - uword *p; - - if (PREDICT_FALSE (pm == 0)) - return ~0; - - if (PREDICT_TRUE (entry->policer_index != ~0)) - { - if (PREDICT_TRUE (pool_is_free_index (pm->policers, - entry->policer_index) == 0)) - return entry->policer_index; - } - - p = hash_get_mem (pm->policer_index_by_name, entry->name); - if (!p) - return ~0; - - entry->policer_index = (u32) p[0]; - return entry->policer_index; -} - -/* - * Process one packet: parse the Ethernet ethertype, find a bound entry, - * meter it, and pick the next node. Returns the error code recorded for - * counters. - */ -static_always_inline copp_punt_policer_error_t -copp_punt_policer_x1 (vlib_main_t *vm, copp_punt_policer_main_t *cpm, vlib_buffer_t *b, - u16 *next, u16 *out_ethertype, u32 *out_policer_index, - u32 *out_verdict, int *out_matched_idx) -{ - ethernet_header_t *eth; - u16 ethertype; - u32 feat_next; - copp_punt_policer_entry_t *entry = 0; - int idx = -1; - index_t lipi; - lcp_itf_pair_t *lip; - - vnet_feature_next (&feat_next, b); - *next = (u16) feat_next; - *out_matched_idx = -1; - - if (PREDICT_FALSE (b->current_length < sizeof (ethernet_header_t))) - { - *out_ethertype = 0; - *out_policer_index = ~0; - *out_verdict = POLICE_CONFORM; - return COPP_PUNT_POLICER_ERROR_PASS; - } - - eth = vlib_buffer_get_current (b); - ethertype = clib_net_to_host_u16 (eth->type); - *out_ethertype = ethertype; - - for (u32 i = 0; i < cpm->n_entries; i++) - { - copp_punt_policer_entry_t *cand = &cpm->entries[i]; - - if (!cand->in_use || cand->ethertype != ethertype) - continue; - - if (cand->match_ip4_ttl_expiring) - { - ip4_header_t *ip4; - - if (b->current_length < - sizeof (ethernet_header_t) + sizeof (ip4_header_t)) - continue; - - ip4 = (ip4_header_t *) (eth + 1); - if (ip4->ttl > 1) - continue; - } - - entry = cand; - idx = (int) i; - break; - } - - if (!entry) - { - /* No policer for this ethertype -- pass through */ - *out_policer_index = ~0; - *out_verdict = POLICE_CONFORM; - return COPP_PUNT_POLICER_ERROR_PASS; - } - - *out_matched_idx = idx; - - { - u32 policer_index = copp_punt_policer_resolve_index (entry); - *out_policer_index = policer_index; - - if (PREDICT_FALSE (policer_index == ~0)) - { - /* Bound but the named policer doesn't exist in VPP -- - * drop rather than silently letting through unpoliced - */ - *out_verdict = POLICE_VIOLATE; - *next = COPP_PUNT_POLICER_NEXT_DROP; - return COPP_PUNT_POLICER_ERROR_DROP_UNRESOLVED; - } - - { - policer_main_t *pm = policer_get_main (); - - if (PREDICT_FALSE (pm == 0)) - { - *out_verdict = POLICE_VIOLATE; - *next = COPP_PUNT_POLICER_NEXT_DROP; - return COPP_PUNT_POLICER_ERROR_DROP_UNRESOLVED; - } - - policer_t *policer = pool_elt_at_index (pm->policers, policer_index); - /* - * VPP's own pps-mode policer config translation calibrates the - * underlying byte/kbps token bucket assuming every packet is - * exactly 256 bytes regardless of real frame size. - */ - u32 metered_len = 256; - policer_result_e verdict = vnet_police_packet ( - policer, metered_len, - POLICE_CONFORM, clib_cpu_time_now () >> POLICER_TICKS_PER_PERIOD_SHIFT); - - vlib_combined_counter_main_t *pc = policer_get_counters (); - if (PREDICT_TRUE (pc != 0)) - vlib_increment_combined_counter ( - &pc[verdict], vm->thread_index, policer_index, 1, metered_len); - - *out_verdict = verdict; - - if (PREDICT_FALSE (verdict != POLICE_CONFORM)) - { - *next = COPP_PUNT_POLICER_NEXT_DROP; - return COPP_PUNT_POLICER_ERROR_DROP_EXCEED; - } - } - } - - /* Conforming packet: punt it straight to the mapped TAP */ - lipi = lcp_itf_pair_find_by_phy (vnet_buffer (b)->sw_if_index[VLIB_RX]); - if (PREDICT_TRUE (lipi != INDEX_INVALID)) - { - lip = lcp_itf_pair_get (lipi); - if (lip) - { - vnet_buffer (b)->sw_if_index[VLIB_TX] = lip->lip_host_sw_if_index; - *next = COPP_PUNT_POLICER_NEXT_INTERFACE_OUTPUT; - } - } - - return COPP_PUNT_POLICER_ERROR_PASS; -} - -VLIB_NODE_FN (copp_punt_policer_node) -(vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *frame) -{ - copp_punt_policer_main_t *cpm = &copp_punt_policer_main; - u32 n_left_from, *from; - vlib_buffer_t *bufs[VLIB_FRAME_SIZE], **b; - u16 nexts[VLIB_FRAME_SIZE], *next; - u32 error_counts[COPP_PUNT_POLICER_N_ERROR] = { 0 }; - /* per-entry conform/exceed/violate deltas accumulated locally and - * flushed once at the end, to avoid a scattered-write per packet into - * cpm->{conform,exceed,violate}_packets for the common (unmatched) - * case */ - u64 conform_delta[COPP_PUNT_POLICER_MAX_ENTRIES] = { 0 }; - u64 exceed_delta[COPP_PUNT_POLICER_MAX_ENTRIES] = { 0 }; - u64 violate_delta[COPP_PUNT_POLICER_MAX_ENTRIES] = { 0 }; - - from = vlib_frame_vector_args (frame); - n_left_from = frame->n_vectors; - - vlib_get_buffers (vm, from, bufs, n_left_from); - b = bufs; - next = nexts; - - while (n_left_from) - { - u16 ethertype = 0; - u32 policer_index = ~0; - u32 verdict = POLICE_CONFORM; - int matched_idx = -1; - copp_punt_policer_error_t err; - - err = copp_punt_policer_x1 (vm, cpm, b[0], &next[0], ðertype, - &policer_index, &verdict, &matched_idx); - error_counts[err]++; - - if (PREDICT_FALSE ((node->flags & VLIB_NODE_FLAG_TRACE) && matched_idx >= 0)) - copp_punt_policer_elog (vm, vnet_buffer (b[0])->sw_if_index[VLIB_RX], - ethertype, matched_idx, policer_index, - verdict); - - if (matched_idx >= 0) - { - switch ((policer_result_e) verdict) - { - case POLICE_CONFORM: - conform_delta[matched_idx]++; - break; - case POLICE_EXCEED: - exceed_delta[matched_idx]++; - break; - case POLICE_VIOLATE: - violate_delta[matched_idx]++; - break; - } - } - - if (PREDICT_FALSE ((node->flags & VLIB_NODE_FLAG_TRACE) && - (b[0]->flags & VLIB_BUFFER_IS_TRACED))) - { - copp_punt_policer_trace_t *t = - vlib_add_trace (vm, node, b[0], sizeof (*t)); - t->sw_if_index = vnet_buffer (b[0])->sw_if_index[VLIB_RX]; - t->next_index = next[0]; - t->ethertype = ethertype; - t->policer_index = policer_index; - t->verdict = verdict; - } - - b += 1; - next += 1; - n_left_from -= 1; - } - - vlib_buffer_enqueue_to_next (vm, node, from, nexts, frame->n_vectors); - - for (int i = 0; i < COPP_PUNT_POLICER_N_ERROR; i++) - { - if (error_counts[i]) - vlib_node_increment_counter (vm, copp_punt_policer_node.index, i, - error_counts[i]); - } - - for (u32 i = 0; i < cpm->n_entries; i++) - { - if (conform_delta[i]) - cpm->conform_packets[i] += conform_delta[i]; - if (exceed_delta[i]) - cpm->exceed_packets[i] += exceed_delta[i]; - if (violate_delta[i]) - cpm->violate_packets[i] += violate_delta[i]; - } - - return frame->n_vectors; -} - -VLIB_REGISTER_NODE (copp_punt_policer_node) = { - .name = "copp-punt-policer", - .vector_size = sizeof (u32), - .format_trace = format_copp_punt_policer_trace, - .type = VLIB_NODE_TYPE_INTERNAL, - .n_errors = ARRAY_LEN (copp_punt_policer_error_strings), - .error_strings = copp_punt_policer_error_strings, - .n_next_nodes = COPP_PUNT_POLICER_N_NEXT, - .next_nodes = { - [COPP_PUNT_POLICER_NEXT_DROP] = "error-drop", - [COPP_PUNT_POLICER_NEXT_INTERFACE_OUTPUT] = "interface-output", - }, -}; - -VNET_FEATURE_INIT (copp_punt_policer_feat, static) = { - .arc_name = "device-input", - .node_name = "copp-punt-policer", - .runs_before = VNET_FEATURES ("ethernet-input"), -}; diff --git a/vppbld/plugins/sonic_ext/CMakeLists.txt b/vppbld/plugins/sonic_ext/CMakeLists.txt index 76191b72..87c1635e 100644 --- a/vppbld/plugins/sonic_ext/CMakeLists.txt +++ b/vppbld/plugins/sonic_ext/CMakeLists.txt @@ -23,6 +23,9 @@ add_vpp_plugin(sonic_ext l2_vlan_filter_node.c ip2me_node.c drop_member_stats_node.c + copp_ifout_node.c + copp_ip2me_node.c + copp_udld_node.c cli.c API_FILES diff --git a/vppbld/plugins/sonic_ext/FEATURE.yaml b/vppbld/plugins/sonic_ext/FEATURE.yaml index ccf34808..6f423003 100644 --- a/vppbld/plugins/sonic_ext/FEATURE.yaml +++ b/vppbld/plugins/sonic_ext/FEATURE.yaml @@ -3,6 +3,12 @@ maintainer: SONiC-VPP contributors features: - punt-via-member: redirect punted unicast/ARP over aggregated interface (BVI, Bond) to the original member tap - host-xc: bypass ethernet-input for packets injected from the linux-cp host tap + - copp-ifout: per-ethertype CoPP rate policing (ARP, LACP, LLDP, UDLD, + TTL_ERROR) on the interface-output arc of every linux-cp host TAP -- + only meters traffic already decided to be punted, never ordinary + forwarded traffic + - copp-ip2me: CoPP rate policing for IP2ME/SNMP/SSH traffic on the + global ip4-punt arc, ahead of ip4-punt-redirect description: "VPP extensions for SONiC features" state: experimental properties: [API, CLI] diff --git a/vppbld/plugins/sonic_ext/capture_node.c b/vppbld/plugins/sonic_ext/capture_node.c index 18d6e050..019ebf5b 100644 --- a/vppbld/plugins/sonic_ext/capture_node.c +++ b/vppbld/plugins/sonic_ext/capture_node.c @@ -112,6 +112,13 @@ VLIB_NODE_FN (sonic_ext_capture_node) } seb->magic = SONIC_EXT_BUFFER_MAGIC; + + /* No pre-resolved copp-ifout entry yet -- sonic-ext-copp-udld sets + * this to a real index for the one class of traffic (UDLD) that + * cannot be matched by sonic-ext-copp-ifout's own ethertype/length + * byte read. Every other packet leaves this as ~0 and copp-ifout + * falls back to its normal byte-match path unchanged. */ + seb->copp_ifout_entry_idx = ~0; n_captured++; if (PREDICT_FALSE ((node->flags & VLIB_NODE_FLAG_TRACE) && diff --git a/vppbld/plugins/sonic_ext/cli.c b/vppbld/plugins/sonic_ext/cli.c index 4f89a272..8490c7b6 100644 --- a/vppbld/plugins/sonic_ext/cli.c +++ b/vppbld/plugins/sonic_ext/cli.c @@ -16,6 +16,7 @@ #include #include +#include static clib_error_t * sonic_ext_punt_via_member_command_fn (vlib_main_t *vm, @@ -147,6 +148,146 @@ VLIB_CLI_COMMAND (sonic_ext_ip2me_command, static) = { .function = sonic_ext_ip2me_command_fn, }; +static clib_error_t * +sonic_ext_copp_ifout_bind_command_fn (vlib_main_t *vm, + unformat_input_t *input, + vlib_cli_command_t *cmd) +{ + u32 ethertype = 0; + u8 *policer_name = 0; + int is_bind = 1; + int match_ip4_ttl_expiring = 0; + clib_error_t *error = 0; + + while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT) + { + if (unformat (input, "ethertype 0x%x", ðertype)) + ; + else if (unformat (input, "ethertype %d", ðertype)) + ; + else if (unformat (input, "policer %s", &policer_name)) + ; + else if (unformat (input, "match-ip4-ttl-expiring")) + match_ip4_ttl_expiring = 1; + else if (unformat (input, "del")) + is_bind = 0; + else + { + error = clib_error_return (0, "unknown input `%U'", + format_unformat_error, input); + goto done; + } + } + + if (ethertype == 0 || (is_bind && !policer_name)) + { + error = clib_error_return (0, "usage: sonic-ext copp-ifout bind " + "ethertype <0xNNNN> policer " + "[match-ip4-ttl-expiring] [del]"); + goto done; + } + + { + int rv = sonic_ext_copp_ifout_bind ( + (u16) ethertype, policer_name ? (char *) policer_name : "", is_bind, + match_ip4_ttl_expiring); + if (rv) + error = clib_error_return (0, "bind failed: rv %d", rv); + } + +done: + vec_free (policer_name); + return error; +} + +VLIB_CLI_COMMAND (sonic_ext_copp_ifout_bind_command, static) = { + .path = "sonic-ext copp-ifout bind", + .short_help = "sonic-ext copp-ifout bind ethertype <0xNNNN> policer " + "[match-ip4-ttl-expiring] [del]", + .function = sonic_ext_copp_ifout_bind_command_fn, +}; + +static clib_error_t * +show_sonic_ext_copp_ifout_command_fn (vlib_main_t *vm, + unformat_input_t *input, + vlib_cli_command_t *cmd) +{ + sonic_ext_main_t *sem = &sonic_ext_main; + + vlib_cli_output (vm, "%-8s %-40s %-12s %10s %10s %10s %s", + "ethtype", "policer-name", "vpp-idx", + "conform", "exceed", "violate", "match"); + for (u32 i = 0; i < sem->copp_ifout_n_entries; i++) + { + if (!sem->copp_ifout_entries[i].in_use) + continue; + vlib_cli_output (vm, "0x%04x %-40s %-12d %10llu %10llu %10llu %s", + sem->copp_ifout_entries[i].ethertype, + sem->copp_ifout_entries[i].name, + (i32) sem->copp_ifout_entries[i].policer_index, + sem->copp_ifout_conform_packets[i], + sem->copp_ifout_exceed_packets[i], + sem->copp_ifout_violate_packets[i], + sem->copp_ifout_entries[i].match_ip4_ttl_expiring ? + "ip4-ttl<=1" : "-"); + } + + return 0; +} + +VLIB_CLI_COMMAND (show_sonic_ext_copp_ifout_command, static) = { + .path = "show sonic-ext copp-ifout", + .short_help = "show sonic-ext copp-ifout", + .function = show_sonic_ext_copp_ifout_command_fn, +}; + +static clib_error_t * +show_sonic_ext_copp_ip2me_command_fn (vlib_main_t *vm, unformat_input_t *input, + vlib_cli_command_t *cmd) +{ + sonic_ext_main_t *sem = &sonic_ext_main; + + vlib_cli_output (vm, "%-30s %-8s %-7s %-9s %10s %10s %10s", "policer", + "vpp-idx", "kind", "match", "conform", "exceed", "violate"); + for (u32 i = 0; i < sem->copp_ip2me_n_policers; i++) + { + sonic_ext_copp_ip2me_policer_t *pol = &sem->copp_ip2me_policers[i]; + + if (!pol->in_use) + continue; + + if (pol->match_kind == SONIC_EXT_COPP_IP2ME_MATCH_ADDR) + vlib_cli_output (vm, "%-30s %-8d %-7s %-9s %10llu %10llu %10llu", + pol->name, (i32) pol->policer_index, "addr", "-", + pol->conform_packets, pol->exceed_packets, + pol->violate_packets); + else + vlib_cli_output (vm, "%-30s %-8d %-7s tcp/%-5d %10llu %10llu %10llu", + pol->name, (i32) pol->policer_index, "tcp-dport", + pol->match_tcp_dport, pol->conform_packets, + pol->exceed_packets, pol->violate_packets); + } + + vlib_cli_output (vm, "%-8s addresses (legacy shared IP2ME/SNMP/SSH slot " + "only):", "count"); + vlib_cli_output (vm, "%u", sem->copp_ip2me_n_addrs); + for (u32 i = 0; i < sem->copp_ip2me_n_addrs; i++) + { + if (!sem->copp_ip2me_addrs[i].in_use) + continue; + vlib_cli_output (vm, " %U", format_ip4_address, + &sem->copp_ip2me_addrs[i].addr); + } + + return 0; +} + +VLIB_CLI_COMMAND (show_sonic_ext_copp_ip2me_command, static) = { + .path = "show sonic-ext copp-ip2me", + .short_help = "show sonic-ext copp-ip2me", + .function = show_sonic_ext_copp_ip2me_command_fn, +}; + static clib_error_t * show_sonic_ext_command_fn (vlib_main_t *vm, unformat_input_t *input, vlib_cli_command_t *cmd) diff --git a/vppbld/plugins/sonic_ext/copp_ifout_node.c b/vppbld/plugins/sonic_ext/copp_ifout_node.c new file mode 100644 index 00000000..219b1fe6 --- /dev/null +++ b/vppbld/plugins/sonic_ext/copp_ifout_node.c @@ -0,0 +1,457 @@ +/* + * Copyright (c) 2026 SONiC-VPP contributors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * sonic-ext-copp-ifout + * + * CoPP per-ethertype rate policing for ARP/LACP/LLDP/UDLD/TTL_ERROR, + * enforced on the `interface-output` arc of each linux-cp-paired TAP + * -- NOT on `device-input` (see history below). + * + * BACKGROUND (sonic-net/sonic-buildimage#25801, SONiC-on-VPP CoPP HLD): + * VPP's existing classify-based policer feature (policer-classify) + * only runs on l2-input / ip4-unicast / ip6-unicast. On linux-cp- + * paired, L3-routed ports, ARP/LACP/LLDP/UDLD traffic never traverses + * any of those arcs -- ethernet-input dispatches it directly to + * arp-input / linux-cp-punt-xc, which punt straight to the TAP with + * no policer consulted at all. + * + * REVISION HISTORY: the first implementation of this policer + * (`copp_punt_policer`, a standalone plugin) registered its + * classify+police node on `device-input`, running unconditionally on + * every packet on every physical interface. Review feedback + * (sonic-net/SONiC#2539, yue-fred-gao) correctly flagged that this + * pays a per-packet tax (measured ~50ns/pkt) on the ~100% of ordinary + * forwarded traffic that never matches, and asked whether the policer + * could instead run on the punt path itself. Confirmed by reading + * linux-cp/lcp_node.c: linux-cp-punt / linux-cp-punt-xc (the ARP/ + * LACP/LLDP/UDLD/TTL_ERROR path) and lcp_arp_phy_node all set + * VLIB_TX = the phy's paired TAP and dispatch straight to + * `interface-output`, rewinding the buffer back to an intact, + * unmodified Ethernet frame first -- so by the time ANY of these + * protocols reaches interface-output on the TAP, the frame layout is + * exactly what device-input classification was already parsing. This + * node moved the same classify+meter logic there: it now only ever + * sees traffic VPP has ALREADY decided is CPU-bound, not the 100% of + * ordinary transit traffic device-input classification paid a tax on + * regardless of match. + * + * Also per reviewer feedback (yue-fred-gao, sonic-net/SONiC#2539, + * 2026-09-14), this was folded into the existing sonic_ext plugin + * (rather than a new standalone plugin) to avoid growing the plugin + * count for closely related SONiC-on-VPP dataplane features. + * + * NOT covered here: BGPV6 / IPv6 ND. Those already worked via VPP's + * own ip6-unicast classify-policer arc before this project. + * + * Delivery: a conforming/unmatched packet simply continues the + * interface-output arc unchanged (falls through to TX) -- linux-cp + * already set VLIB_TX before this node runs, so there is no manual + * TAP-redirect step. Exceed/violate go to error-drop. + * + * Per-TAP feature binding is driven by the LCP pair add/del callback + * (see sonic_ext.c's sonic_ext_lcp_pair_add_cb/_del_cb), the same + * mechanism sonic-ext-aggr-tap-redirect already uses -- this survives + * `config reload` (unlike a manual CLI bind, which does not), since + * the callback re-fires for every LCP pair recreated during reload. + */ + +#include + +#include +#include +#include +#include +#include +#include + +typedef struct +{ + u32 sw_if_index; + u32 next_index; + u16 ethertype; + u32 policer_index; + u32 verdict; +} sonic_ext_copp_ifout_trace_t; + +static u8 * +format_sonic_ext_copp_ifout_trace (u8 *s, va_list *args) +{ + CLIB_UNUSED (vlib_main_t * vm) = va_arg (*args, vlib_main_t *); + CLIB_UNUSED (vlib_node_t * node) = va_arg (*args, vlib_node_t *); + sonic_ext_copp_ifout_trace_t *t = + va_arg (*args, sonic_ext_copp_ifout_trace_t *); + + s = format (s, + "SONIC-EXT-COPP-IFOUT: sw_if_index %d next %d ethertype " + "0x%04x policer_index %d verdict %d", + t->sw_if_index, t->next_index, t->ethertype, t->policer_index, + t->verdict); + return s; +} + +#define foreach_sonic_ext_copp_ifout_error \ + _ (PASS, "packets passed (unmatched ethertype or conform)") \ + _ (DROP_EXCEED, "packets dropped (policer exceed/violate)") \ + _ (DROP_UNRESOLVED, "packets dropped (policer name not yet resolvable)") + +typedef enum +{ +#define _(sym, str) SONIC_EXT_COPP_IFOUT_ERROR_##sym, + foreach_sonic_ext_copp_ifout_error +#undef _ + SONIC_EXT_COPP_IFOUT_N_ERROR, +} sonic_ext_copp_ifout_error_t; + +static char *sonic_ext_copp_ifout_error_strings[] = { +#define _(sym, string) string, + foreach_sonic_ext_copp_ifout_error +#undef _ +}; + +typedef enum +{ + SONIC_EXT_COPP_IFOUT_NEXT_DROP, + SONIC_EXT_COPP_IFOUT_N_NEXT, +} sonic_ext_copp_ifout_next_t; + +static_always_inline u32 +sonic_ext_copp_ifout_resolve_index (sonic_ext_copp_ifout_entry_t *entry) +{ + policer_main_t *pm = policer_get_main (); + uword *p; + + if (PREDICT_FALSE (pm == 0)) + return ~0; + + if (PREDICT_TRUE (entry->policer_index != ~0)) + { + if (PREDICT_TRUE (pool_is_free_index (pm->policers, + entry->policer_index) == 0)) + return entry->policer_index; + } + + p = hash_get_mem (pm->policer_index_by_name, entry->name); + if (!p) + return ~0; + + entry->policer_index = (u32) p[0]; + return entry->policer_index; +} + +static_always_inline sonic_ext_copp_ifout_error_t +sonic_ext_copp_ifout_x1 (vlib_main_t *vm, sonic_ext_main_t *sem, + vlib_buffer_t *b, u16 *next, u16 *out_ethertype, + u32 *out_policer_index, u32 *out_verdict, + int *out_matched_idx) +{ + ethernet_header_t *eth; + u16 ethertype; + u32 feat_next; + sonic_ext_copp_ifout_entry_t *entry = 0; + int idx = -1; + + vnet_feature_next (&feat_next, b); + *next = (u16) feat_next; + *out_matched_idx = -1; + + if (PREDICT_FALSE (b->current_length < sizeof (ethernet_header_t))) + { + *out_ethertype = 0; + *out_policer_index = ~0; + *out_verdict = POLICE_CONFORM; + return SONIC_EXT_COPP_IFOUT_ERROR_PASS; + } + + eth = vlib_buffer_get_current (b); + ethertype = clib_net_to_host_u16 (eth->type); + *out_ethertype = ethertype; + + /* Pre-resolved match wins over the byte-match loop below. Set only by + * sonic-ext-copp-udld, which reaches this packet via VPP's real LLC-null / + * LLC+SNAP+Cisco-UDLD-OUI dispatch -- genuine protocol identification. + * UDLD's wire bytes at this offset are an 802.3 *length* field, not an + * EtherType, and that length varies with the frame's actual TLV payload, + * so it cannot be matched here the way ARP/LACP/LLDP/TTL_ERROR's real + * EtherTypes are. See sonic_ext_buffer_opaque_t.copp_ifout_entry_idx. */ + { + sonic_ext_buffer_opaque_t *seb = sonic_ext_buffer (b); + + if (seb->magic == SONIC_EXT_BUFFER_MAGIC && + seb->copp_ifout_entry_idx != (u32) ~0) + { + entry = &sem->copp_ifout_entries[seb->copp_ifout_entry_idx]; + idx = (int) seb->copp_ifout_entry_idx; + seb->copp_ifout_entry_idx = ~0; /* one-shot: do not leak into reuse */ + } + } + + if (!entry) + for (u32 i = 0; i < sem->copp_ifout_n_entries; i++) + { + sonic_ext_copp_ifout_entry_t *cand = &sem->copp_ifout_entries[i]; + + if (!cand->in_use || cand->ethertype != ethertype) + continue; + + if (cand->match_ip4_ttl_expiring) + { + ip4_header_t *ip4; + + if (b->current_length < + sizeof (ethernet_header_t) + sizeof (ip4_header_t)) + continue; + + ip4 = (ip4_header_t *) (eth + 1); + if (ip4->ttl > 1) + continue; + } + + entry = cand; + idx = (int) i; + break; + } + + if (!entry) + { + *out_policer_index = ~0; + *out_verdict = POLICE_CONFORM; + return SONIC_EXT_COPP_IFOUT_ERROR_PASS; + } + + *out_matched_idx = idx; + + { + u32 policer_index = sonic_ext_copp_ifout_resolve_index (entry); + *out_policer_index = policer_index; + + if (PREDICT_FALSE (policer_index == ~0)) + { + *out_verdict = POLICE_VIOLATE; + *next = SONIC_EXT_COPP_IFOUT_NEXT_DROP; + return SONIC_EXT_COPP_IFOUT_ERROR_DROP_UNRESOLVED; + } + + { + policer_main_t *pm = policer_get_main (); + + if (PREDICT_FALSE (pm == 0)) + { + *out_verdict = POLICE_VIOLATE; + *next = SONIC_EXT_COPP_IFOUT_NEXT_DROP; + return SONIC_EXT_COPP_IFOUT_ERROR_DROP_UNRESOLVED; + } + + policer_t *policer = pool_elt_at_index (pm->policers, policer_index); + u32 metered_len = 256; + policer_result_e verdict = vnet_police_packet ( + policer, metered_len, POLICE_CONFORM, + clib_cpu_time_now () >> POLICER_TICKS_PER_PERIOD_SHIFT); + + vlib_combined_counter_main_t *pc = policer_get_counters (); + if (PREDICT_TRUE (pc != 0)) + vlib_increment_combined_counter (&pc[verdict], vm->thread_index, + policer_index, 1, metered_len); + + *out_verdict = verdict; + + if (PREDICT_FALSE (verdict != POLICE_CONFORM)) + { + *next = SONIC_EXT_COPP_IFOUT_NEXT_DROP; + return SONIC_EXT_COPP_IFOUT_ERROR_DROP_EXCEED; + } + } + } + + /* Conform: leave *next as the feature-arc's own "continue" next + * index (already set via vnet_feature_next() above) -- linux-cp + * already pointed VLIB_TX at the right TAP before this node ran. */ + return SONIC_EXT_COPP_IFOUT_ERROR_PASS; +} + +VLIB_NODE_FN (sonic_ext_copp_ifout_node) +(vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *frame) +{ + sonic_ext_main_t *sem = &sonic_ext_main; + u32 n_left_from, *from; + vlib_buffer_t *bufs[VLIB_FRAME_SIZE], **b; + u16 nexts[VLIB_FRAME_SIZE], *next; + u32 error_counts[SONIC_EXT_COPP_IFOUT_N_ERROR] = { 0 }; + u64 conform_delta[SONIC_EXT_COPP_IFOUT_MAX_ENTRIES] = { 0 }; + u64 exceed_delta[SONIC_EXT_COPP_IFOUT_MAX_ENTRIES] = { 0 }; + u64 violate_delta[SONIC_EXT_COPP_IFOUT_MAX_ENTRIES] = { 0 }; + + from = vlib_frame_vector_args (frame); + n_left_from = frame->n_vectors; + + vlib_get_buffers (vm, from, bufs, n_left_from); + b = bufs; + next = nexts; + + while (n_left_from) + { + u16 ethertype = 0; + u32 policer_index = ~0; + u32 verdict = POLICE_CONFORM; + int matched_idx = -1; + sonic_ext_copp_ifout_error_t err; + + err = sonic_ext_copp_ifout_x1 (vm, sem, b[0], &next[0], ðertype, + &policer_index, &verdict, &matched_idx); + error_counts[err]++; + + if (matched_idx >= 0) + { + switch ((policer_result_e) verdict) + { + case POLICE_CONFORM: + conform_delta[matched_idx]++; + break; + case POLICE_EXCEED: + exceed_delta[matched_idx]++; + break; + case POLICE_VIOLATE: + violate_delta[matched_idx]++; + break; + } + } + + if (PREDICT_FALSE ((node->flags & VLIB_NODE_FLAG_TRACE) && + (b[0]->flags & VLIB_BUFFER_IS_TRACED))) + { + sonic_ext_copp_ifout_trace_t *t = + vlib_add_trace (vm, node, b[0], sizeof (*t)); + t->sw_if_index = vnet_buffer (b[0])->sw_if_index[VLIB_TX]; + t->next_index = next[0]; + t->ethertype = ethertype; + t->policer_index = policer_index; + t->verdict = verdict; + } + + b += 1; + next += 1; + n_left_from -= 1; + } + + vlib_buffer_enqueue_to_next (vm, node, from, nexts, frame->n_vectors); + + for (int i = 0; i < SONIC_EXT_COPP_IFOUT_N_ERROR; i++) + { + if (error_counts[i]) + vlib_node_increment_counter (vm, sonic_ext_copp_ifout_node.index, i, + error_counts[i]); + } + + for (u32 i = 0; i < sem->copp_ifout_n_entries; i++) + { + if (conform_delta[i]) + sem->copp_ifout_conform_packets[i] += conform_delta[i]; + if (exceed_delta[i]) + sem->copp_ifout_exceed_packets[i] += exceed_delta[i]; + if (violate_delta[i]) + sem->copp_ifout_violate_packets[i] += violate_delta[i]; + } + + return frame->n_vectors; +} + +VLIB_REGISTER_NODE (sonic_ext_copp_ifout_node) = { + .name = "sonic-ext-copp-ifout", + .vector_size = sizeof (u32), + .format_trace = format_sonic_ext_copp_ifout_trace, + .type = VLIB_NODE_TYPE_INTERNAL, + .n_errors = ARRAY_LEN (sonic_ext_copp_ifout_error_strings), + .error_strings = sonic_ext_copp_ifout_error_strings, + .n_next_nodes = SONIC_EXT_COPP_IFOUT_N_NEXT, + .next_nodes = { + [SONIC_EXT_COPP_IFOUT_NEXT_DROP] = "error-drop", + }, +}; + +/* + * Feature binding is per-TAP (the LCP host tap of every real phy -- + * not aggregate/BVI/bond taps, which have no CoPP-punted ARP/LACP/ + * LLDP/UDLD/TTL_ERROR traffic of their own; those protocols are + * always punted to the *member* phy's own tap, never the aggregate's). + * Driven from the LCP pair add/del callback in sonic_ext.c, exactly + * like sonic-ext-aggr-tap-redirect and sonic-ext-host-xc already are + * -- this is what makes the binding survive `config reload` (a plain + * per-run manual CLI bind would not: LCP pairs, and hence their + * taps, are recreated on every reload, but the callback re-fires for + * each one as it comes back). + */ +void +sonic_ext_copp_ifout_enable_disable (u32 sw_if_index, int enable) +{ + vnet_feature_enable_disable ("interface-output", "sonic-ext-copp-ifout", + sw_if_index, enable, 0, 0); +} + +VNET_FEATURE_INIT (sonic_ext_copp_ifout_feat, static) = { + .arc_name = "interface-output", + .node_name = "sonic-ext-copp-ifout", +}; + +int +sonic_ext_copp_ifout_find_entry (sonic_ext_main_t *sem, u16 ethertype) +{ + for (u32 i = 0; i < sem->copp_ifout_n_entries; i++) + { + if (sem->copp_ifout_entries[i].in_use && + sem->copp_ifout_entries[i].ethertype == ethertype) + return (int) i; + } + return -1; +} + +int +sonic_ext_copp_ifout_bind (u16 ethertype, const char *policer_name, + int is_bind, int match_ip4_ttl_expiring) +{ + sonic_ext_main_t *sem = &sonic_ext_main; + int idx = sonic_ext_copp_ifout_find_entry (sem, ethertype); + + if (!is_bind) + { + if (idx < 0) + return 0; + clib_memset (&sem->copp_ifout_entries[idx], 0, + sizeof (sem->copp_ifout_entries[idx])); + sem->copp_ifout_conform_packets[idx] = 0; + sem->copp_ifout_exceed_packets[idx] = 0; + sem->copp_ifout_violate_packets[idx] = 0; + return 0; + } + + if (idx < 0) + { + if (sem->copp_ifout_n_entries >= SONIC_EXT_COPP_IFOUT_MAX_ENTRIES) + return VNET_API_ERROR_QUEUE_FULL; + idx = (int) sem->copp_ifout_n_entries++; + } + + clib_memset (&sem->copp_ifout_entries[idx], 0, + sizeof (sem->copp_ifout_entries[idx])); + sem->copp_ifout_entries[idx].ethertype = ethertype; + snprintf ((char *) sem->copp_ifout_entries[idx].name, + sizeof (sem->copp_ifout_entries[idx].name), "%s", policer_name); + sem->copp_ifout_entries[idx].policer_index = ~0; + sem->copp_ifout_entries[idx].in_use = 1; + sem->copp_ifout_entries[idx].match_ip4_ttl_expiring = + match_ip4_ttl_expiring ? 1 : 0; + sem->copp_ifout_conform_packets[idx] = 0; + sem->copp_ifout_exceed_packets[idx] = 0; + sem->copp_ifout_violate_packets[idx] = 0; + + return 0; +} diff --git a/vppbld/plugins/sonic_ext/copp_ip2me_node.c b/vppbld/plugins/sonic_ext/copp_ip2me_node.c new file mode 100644 index 00000000..4e2a19ac --- /dev/null +++ b/vppbld/plugins/sonic_ext/copp_ip2me_node.c @@ -0,0 +1,545 @@ +/* + * Copyright (c) 2026 SONiC-VPP contributors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * sonic-ext-copp-ip2me: CoPP enforcement for IP2ME/SNMP/SSH traffic -- + * traffic destined to one of the router's own IPv4 addresses that VPP's + * dataplane does not answer itself (see sonic-net/sonic-buildimage#25801, + * SONiC-on-VPP CoPP HLD). Folded into sonic_ext (formerly the standalone + * copp_ip2me_policer plugin) alongside sonic-ext-copp-ifout, per the + * same reviewer feedback (yue-fred-gao, sonic-net/SONiC#2539) to avoid + * growing the plugin count for closely related SONiC-on-VPP dataplane + * features. + * + * BACKGROUND: VPP's built-in classify-based policer feature + * (ip4-policer-classify, on the ip4-unicast arc) only meters traffic on + * whichever interface it has been explicitly bound to. Since the + * classify table matches purely on destination IP, IP2ME traffic + * destined to router-interface A's address can legitimately arrive on + * router-interface B -- if B never had the feature bound, that traffic + * skips policing entirely and reaches VPP's ip4-punt-redirect mechanism + * completely unpoliced. Binding to every possible L3 ingress interface + * is the fix VPP's own classify feature requires, but it needs lazy, + * per-RIF VAPI calls (deferred to avoid syncd's SAI-call watchdog) and + * has proven fragile in practice. + * + * This node avoids the whole binding-scope problem by living on + * ip4-punt instead: a single, always-on, global feature arc that every + * packet reaching this point has already been routed through + * (ip4-lookup -> ip4-local -> ip4-punt), regardless of which interface + * it arrived on. No per-interface binding is needed -- ip4-punt is + * reached the same way no matter the ingress interface, once VPP's own + * dataplane has already concluded "nothing here handles this packet, it + * needs the host." That is exactly SAI's IP2ME semantics, so this + * node only needs to answer one question (is the destination address + * one we're tracking?) and apply the existing SAI-created policer + * object, then let the packet continue unchanged to ip4-punt-redirect + * (conform) or drop it (exceed/violate) -- it never needs to redirect + * to a TAP itself, unlike sonic-ext-copp-ifout's interface-output node, + * since ip4-punt-redirect already does that for every packet that + * reaches it. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include + +typedef struct +{ + u32 sw_if_index; + u32 next_index; + u32 dst_addr; + u16 dst_port; + u32 policer_index; + u32 verdict; /* policer_result_e */ +} sonic_ext_copp_ip2me_trace_t; + +static u8 * +format_sonic_ext_copp_ip2me_trace (u8 *s, va_list *args) +{ + CLIB_UNUSED (vlib_main_t * vm) = va_arg (*args, vlib_main_t *); + CLIB_UNUSED (vlib_node_t * node) = va_arg (*args, vlib_node_t *); + sonic_ext_copp_ip2me_trace_t *t = + va_arg (*args, sonic_ext_copp_ip2me_trace_t *); + + s = format (s, + "SONIC-EXT-COPP-IP2ME: sw_if_index %d next %d dst %U " + "dst_port %d policer_index %d verdict %d", + t->sw_if_index, t->next_index, format_ip4_address, + &t->dst_addr, t->dst_port, t->policer_index, t->verdict); + return s; +} + +#define foreach_sonic_ext_copp_ip2me_error \ + _ (PASS, "packets passed (unmatched address or conform)") \ + _ (DROP_EXCEED, "packets dropped (policer exceed/violate)") \ + _ (DROP_UNRESOLVED, "packets dropped (policer name not yet resolvable)") + +typedef enum +{ +#define _(sym, str) SONIC_EXT_COPP_IP2ME_ERROR_##sym, + foreach_sonic_ext_copp_ip2me_error +#undef _ + SONIC_EXT_COPP_IP2ME_N_ERROR, +} sonic_ext_copp_ip2me_error_t; + +static char *sonic_ext_copp_ip2me_error_strings[] = { +#define _(sym, string) string, + foreach_sonic_ext_copp_ip2me_error +#undef _ +}; + +typedef enum +{ + SONIC_EXT_COPP_IP2ME_NEXT_DROP, + SONIC_EXT_COPP_IP2ME_N_NEXT, +} sonic_ext_copp_ip2me_next_t; + +/* + * Resolve a policer slot's VPP policer_index by name, lazily -- same + * pattern as sonic-ext-copp-ifout, so bind order relative to + * policer_add() doesn't matter and a later policer_update() recreating + * the object under the same name is picked up automatically. + */ +static_always_inline u32 +sonic_ext_copp_ip2me_resolve_index (sonic_ext_copp_ip2me_policer_t *pol) +{ + policer_main_t *pm = policer_get_main (); + uword *p; + + if (PREDICT_FALSE (pm == 0)) + return ~0; + + if (PREDICT_TRUE (pol->policer_index != ~0)) + { + if (PREDICT_TRUE (pool_is_free_index (pm->policers, pol->policer_index) == + 0)) + return pol->policer_index; + } + + p = hash_get_mem (pm->policer_index_by_name, pol->name); + if (!p) + return ~0; + + pol->policer_index = (u32) p[0]; + return pol->policer_index; +} + +static_always_inline int +sonic_ext_copp_ip2me_addr_match (sonic_ext_main_t *sem, u32 dst_addr) +{ + for (u32 i = 0; i < sem->copp_ip2me_n_addrs; i++) + { + if (sem->copp_ip2me_addrs[i].in_use && + sem->copp_ip2me_addrs[i].addr == dst_addr) + return 1; + } + return 0; +} + +/* + * Find the first in-use policer slot matching this packet: either the + * legacy shared IP2ME/SNMP/SSH address-match slot (dst_addr is one of + * our tracked router-interface IPs) or a TCP-dst-port slot (BGP/BGPV6, + * matched independently of the address set so its own bind/unbind + * never touches IP2ME/SNMP/SSH's slot or any other port-matched + * slot). Returns NULL if nothing matches -- caller must pass through + * unpoliced in that case, same as before this multi-slot change. + */ +static_always_inline sonic_ext_copp_ip2me_policer_t * +sonic_ext_copp_ip2me_find_policer (sonic_ext_main_t *sem, u32 dst_addr, + int has_tcp_dport, u16 tcp_dport) +{ + int addr_hit = sonic_ext_copp_ip2me_addr_match (sem, dst_addr); + + for (u32 i = 0; i < sem->copp_ip2me_n_policers; i++) + { + sonic_ext_copp_ip2me_policer_t *pol = &sem->copp_ip2me_policers[i]; + + if (!pol->in_use) + continue; + + if (pol->match_kind == SONIC_EXT_COPP_IP2ME_MATCH_ADDR) + { + if (addr_hit) + return pol; + } + else /* SONIC_EXT_COPP_IP2ME_MATCH_TCP_DPORT */ + { + if (has_tcp_dport && tcp_dport == pol->match_tcp_dport) + return pol; + } + } + + return 0; +} + +/* + * Process one packet: identify which (if any) policer slot it matches + * -- destination-address-based for the legacy shared IP2ME/SNMP/SSH + * slot, or TCP-dst-port-based for BGP/BGPV6's own independent slot -- + * meter a match with that slot's policer, and pick the next node. A + * conforming/unmatched packet CONTINUES on the ip4-punt arc (i.e. + * reaches ip4-punt-redirect next, unmodified) -- this node never + * redirects to a TAP itself, unlike sonic-ext-copp-ifout's + * interface-output node, since ip4-punt-redirect already does that for + * every packet that reaches it. + */ +static_always_inline sonic_ext_copp_ip2me_error_t +sonic_ext_copp_ip2me_x1 (vlib_main_t *vm, sonic_ext_main_t *sem, + vlib_buffer_t *b, u16 *next, u32 *out_dst_addr, + u16 *out_dst_port, u32 *out_policer_index, + u32 *out_verdict, sonic_ext_copp_ip2me_policer_t **out_pol) +{ + ip4_header_t *ip4; + u32 feat_next; + u32 dst_addr; + u16 dst_port = 0; + int has_tcp_dport = 0; + + vnet_feature_next (&feat_next, b); + *next = (u16) feat_next; + *out_pol = 0; + + if (PREDICT_FALSE (sem->copp_ip2me_n_policers == 0 || + b->current_length < sizeof (ip4_header_t))) + { + *out_dst_addr = 0; + *out_dst_port = 0; + *out_policer_index = ~0; + *out_verdict = POLICE_CONFORM; + return SONIC_EXT_COPP_IP2ME_ERROR_PASS; + } + + ip4 = vlib_buffer_get_current (b); + dst_addr = ip4->dst_address.as_u32; + *out_dst_addr = dst_addr; + + if (ip4->protocol == IP_PROTOCOL_TCP && + b->current_length >= + sizeof (ip4_header_t) + sizeof (tcp_header_t)) + { + tcp_header_t *tcp = (tcp_header_t *) (ip4 + 1); + dst_port = clib_net_to_host_u16 (tcp->dst_port); + has_tcp_dport = 1; + } + *out_dst_port = dst_port; + + sonic_ext_copp_ip2me_policer_t *pol = + sonic_ext_copp_ip2me_find_policer (sem, dst_addr, has_tcp_dport, dst_port); + + if (!pol) + { + /* Not an address or port we're tracking -- pass through unaffected */ + *out_policer_index = ~0; + *out_verdict = POLICE_CONFORM; + return SONIC_EXT_COPP_IP2ME_ERROR_PASS; + } + + *out_pol = pol; + + { + u32 policer_index = sonic_ext_copp_ip2me_resolve_index (pol); + *out_policer_index = policer_index; + + if (PREDICT_FALSE (policer_index == ~0)) + { + /* Bound but the named policer doesn't exist in VPP yet -- + * drop rather than silently letting through unpoliced */ + *out_verdict = POLICE_VIOLATE; + *next = SONIC_EXT_COPP_IP2ME_NEXT_DROP; + return SONIC_EXT_COPP_IP2ME_ERROR_DROP_UNRESOLVED; + } + + { + policer_main_t *pm = policer_get_main (); + + if (PREDICT_FALSE (pm == 0)) + { + *out_verdict = POLICE_VIOLATE; + *next = SONIC_EXT_COPP_IP2ME_NEXT_DROP; + return SONIC_EXT_COPP_IP2ME_ERROR_DROP_UNRESOLVED; + } + + policer_t *policer = pool_elt_at_index (pm->policers, policer_index); + /* Same 256-byte reference length convention sonic-ext-copp-ifout + * uses, matching VPP's own pps-mode policer calibration. */ + u32 metered_len = 256; + policer_result_e verdict = vnet_police_packet ( + policer, metered_len, POLICE_CONFORM, + clib_cpu_time_now () >> POLICER_TICKS_PER_PERIOD_SHIFT); + + vlib_combined_counter_main_t *pc = policer_get_counters (); + if (PREDICT_TRUE (pc != 0)) + vlib_increment_combined_counter (&pc[verdict], vm->thread_index, + policer_index, 1, metered_len); + + *out_verdict = verdict; + + if (PREDICT_FALSE (verdict != POLICE_CONFORM)) + { + *next = SONIC_EXT_COPP_IP2ME_NEXT_DROP; + return SONIC_EXT_COPP_IP2ME_ERROR_DROP_EXCEED; + } + } + } + + /* Conform: leave *next as the feature-arc's own "continue" next index + * (already set via vnet_feature_next() above) -- i.e. proceed to + * ip4-punt-redirect exactly as if this feature were never enabled. */ + return SONIC_EXT_COPP_IP2ME_ERROR_PASS; +} + +VLIB_NODE_FN (sonic_ext_copp_ip2me_node) +(vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *frame) +{ + sonic_ext_main_t *sem = &sonic_ext_main; + u32 n_left_from, *from; + vlib_buffer_t *bufs[VLIB_FRAME_SIZE], **b; + u16 nexts[VLIB_FRAME_SIZE], *next; + u32 error_counts[SONIC_EXT_COPP_IP2ME_N_ERROR] = { 0 }; + + from = vlib_frame_vector_args (frame); + n_left_from = frame->n_vectors; + + vlib_get_buffers (vm, from, bufs, n_left_from); + b = bufs; + next = nexts; + + while (n_left_from) + { + u32 dst_addr = 0; + u16 dst_port = 0; + u32 policer_index = ~0; + u32 verdict = POLICE_CONFORM; + sonic_ext_copp_ip2me_policer_t *pol = 0; + sonic_ext_copp_ip2me_error_t err; + + err = sonic_ext_copp_ip2me_x1 (vm, sem, b[0], &next[0], &dst_addr, + &dst_port, &policer_index, &verdict, &pol); + error_counts[err]++; + + if (pol) + { + switch ((policer_result_e) verdict) + { + case POLICE_CONFORM: + pol->conform_packets++; + break; + case POLICE_EXCEED: + pol->exceed_packets++; + break; + case POLICE_VIOLATE: + pol->violate_packets++; + break; + } + } + + if (PREDICT_FALSE ((node->flags & VLIB_NODE_FLAG_TRACE) && + (b[0]->flags & VLIB_BUFFER_IS_TRACED))) + { + sonic_ext_copp_ip2me_trace_t *t = + vlib_add_trace (vm, node, b[0], sizeof (*t)); + t->sw_if_index = vnet_buffer (b[0])->sw_if_index[VLIB_RX]; + t->next_index = next[0]; + t->dst_addr = dst_addr; + t->dst_port = dst_port; + t->policer_index = policer_index; + t->verdict = verdict; + } + + b += 1; + next += 1; + n_left_from -= 1; + } + + vlib_buffer_enqueue_to_next (vm, node, from, nexts, frame->n_vectors); + + for (int i = 0; i < SONIC_EXT_COPP_IP2ME_N_ERROR; i++) + { + if (error_counts[i]) + vlib_node_increment_counter (vm, sonic_ext_copp_ip2me_node.index, i, + error_counts[i]); + } + + return frame->n_vectors; +} + +VLIB_REGISTER_NODE (sonic_ext_copp_ip2me_node) = { + .name = "sonic-ext-copp-ip2me", + .vector_size = sizeof (u32), + .format_trace = format_sonic_ext_copp_ip2me_trace, + .type = VLIB_NODE_TYPE_INTERNAL, + .n_errors = ARRAY_LEN (sonic_ext_copp_ip2me_error_strings), + .error_strings = sonic_ext_copp_ip2me_error_strings, + .n_next_nodes = SONIC_EXT_COPP_IP2ME_N_NEXT, + .next_nodes = { + [SONIC_EXT_COPP_IP2ME_NEXT_DROP] = "ip4-drop", + }, +}; + +/* + * ip4-punt is a global feature arc, not per-interface -- enable this + * feature on it ONCE at init, unconditionally, the same way + * ip4-punt-redirect itself is always enabled. No per-interface + * enable/disable call is needed or ever made (unlike sonic-ext-copp- + * ifout, which has to do this per-TAP because interface-output is a + * per-interface arc; ip4-punt is not). + */ +VNET_FEATURE_INIT (sonic_ext_copp_ip2me_feat, static) = { + .arc_name = "ip4-punt", + .node_name = "sonic-ext-copp-ip2me", + .runs_before = VNET_FEATURES ("ip4-punt-redirect"), +}; + +static int +sonic_ext_copp_ip2me_find_addr (sonic_ext_main_t *sem, u32 addr) +{ + for (u32 i = 0; i < sem->copp_ip2me_n_addrs; i++) + { + if (sem->copp_ip2me_addrs[i].in_use && sem->copp_ip2me_addrs[i].addr == addr) + return (int) i; + } + return -1; +} + +int +sonic_ext_copp_ip2me_addr_add_del (u32 addr, int is_add) +{ + sonic_ext_main_t *sem = &sonic_ext_main; + int idx = sonic_ext_copp_ip2me_find_addr (sem, addr); + + if (!is_add) + { + if (idx < 0) + return 0; + clib_memset (&sem->copp_ip2me_addrs[idx], 0, + sizeof (sem->copp_ip2me_addrs[idx])); + return 0; + } + + if (idx >= 0) + return 0; /* already present */ + + if (sem->copp_ip2me_n_addrs >= SONIC_EXT_COPP_IP2ME_MAX_ADDRS) + return VNET_API_ERROR_QUEUE_FULL; + + idx = (int) sem->copp_ip2me_n_addrs++; + sem->copp_ip2me_addrs[idx].addr = addr; + sem->copp_ip2me_addrs[idx].in_use = 1; + + return 0; +} + +/* + * Find an in-use policer slot by name (used to unbind exactly the + * caller's own slot, never another SAI trap's) or the first free slot + * (used to bind a new one). Returns -1 if not found / table full. + */ +static int +sonic_ext_copp_ip2me_find_policer_slot_by_name (sonic_ext_main_t *sem, + const char *name) +{ + for (u32 i = 0; i < sem->copp_ip2me_n_policers; i++) + { + if (sem->copp_ip2me_policers[i].in_use && + strncmp ((char *) sem->copp_ip2me_policers[i].name, name, + SONIC_EXT_COPP_IFOUT_NAME_LEN) == 0) + return (int) i; + } + return -1; +} + +static int +sonic_ext_copp_ip2me_alloc_policer_slot (sonic_ext_main_t *sem) +{ + for (u32 i = 0; i < sem->copp_ip2me_n_policers; i++) + { + if (!sem->copp_ip2me_policers[i].in_use) + return (int) i; + } + + if (sem->copp_ip2me_n_policers >= SONIC_EXT_COPP_IP2ME_MAX_POLICERS) + return -1; + + return (int) sem->copp_ip2me_n_policers++; +} + +/* + * Bind (or unbind) one independent policer slot. match_kind/ + * match_tcp_dport select what this slot matches; every caller must + * pass its OWN unique policer_name (SwitchVppHostifTrap.cpp always + * uses the SAI trap group's own "copp-policer-0x" string) so + * that unbinding one SAI trap's slot can never remove another trap's + * slot even if, by historical accident, two traps briefly shared a + * name -- each slot is looked up and cleared strictly by its own name. + */ +static int +sonic_ext_copp_ip2me_bind_slot (const char *policer_name, int is_bind, + u8 match_kind, u16 match_tcp_dport) +{ + sonic_ext_main_t *sem = &sonic_ext_main; + int idx = sonic_ext_copp_ip2me_find_policer_slot_by_name (sem, policer_name); + + if (!is_bind) + { + if (idx < 0) + return 0; /* already unbound / never bound -- no-op */ + clib_memset (&sem->copp_ip2me_policers[idx], 0, + sizeof (sem->copp_ip2me_policers[idx])); + return 0; + } + + if (idx < 0) + { + idx = sonic_ext_copp_ip2me_alloc_policer_slot (sem); + if (idx < 0) + return VNET_API_ERROR_QUEUE_FULL; + } + + sonic_ext_copp_ip2me_policer_t *pol = &sem->copp_ip2me_policers[idx]; + + clib_memset (pol, 0, sizeof (*pol)); + snprintf ((char *) pol->name, sizeof (pol->name), "%s", policer_name); + pol->policer_index = ~0; + pol->in_use = 1; + pol->match_kind = match_kind; + pol->match_tcp_dport = match_tcp_dport; + + return 0; +} + +int +sonic_ext_copp_ip2me_bind (const char *policer_name, int is_bind) +{ + return sonic_ext_copp_ip2me_bind_slot (policer_name, is_bind, + SONIC_EXT_COPP_IP2ME_MATCH_ADDR, 0); +} + +int +sonic_ext_copp_ip2me_bind_bgp (const char *policer_name, int is_bind) +{ + /* BGP/BGPV6 both use TCP dst port 179 on the wire; matched + * independently of the address set (see sonic_ext_copp_ip2me_x1()) + * so BGP's own install/uninstall never disturbs IP2ME/SNMP/SSH's + * address-match slot, and vice versa. */ + return sonic_ext_copp_ip2me_bind_slot (policer_name, is_bind, + SONIC_EXT_COPP_IP2ME_MATCH_TCP_DPORT, + 179); +} diff --git a/vppbld/plugins/sonic_ext/copp_udld_node.c b/vppbld/plugins/sonic_ext/copp_udld_node.c new file mode 100644 index 00000000..11666543 --- /dev/null +++ b/vppbld/plugins/sonic_ext/copp_udld_node.c @@ -0,0 +1,394 @@ +/* + * Copyright (c) 2026 SONiC-VPP contributors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * sonic-ext-copp-udld + * + * CoPP punt+policer path for UDLD -- a protocol sonic-ext-copp-ifout + * (see its own file header) CANNOT see, for a reason specific to + * UDLD alone among ARP/LACP/LLDP/UDLD/TTL_ERROR: UDLD is not + * Ethernet-II. Its 14th/15th wire bytes (0x0067 = 103) are BELOW + * 0x0600, so per 802.3 they are a *length* field, not an EtherType. + * VPP's ethernet-input hard-codes this threshold + * (eth_input_next_by_type(): "etype < 0x600 ? LLC : ...") and always + * routes such frames to `llc-input` -- there is no configuration + * knob to send a sub-0x600 frame down the normal EtherType-classify + * path sonic-ext-copp-ifout hooks. Confirmed live via `vppctl show + * trace` + `show error`: a UDLD frame sent to the DUT produces + * `llc-input: unknown llc ssap/dsap` and is dropped inside VPP core, + * before sonic-ext-copp-ifout's feature node on interface-output + * ever runs. + * + * Two wire encodings of UDLD both dead-end the same way and both + * need a registered handler here: + * + * 1. Real UDLD (RFC/Cisco wire format): 802.3 length + 802.2 LLC + * SNAP encapsulation -- LLC dsap=ssap=0xAA (LLC_PROTOCOL_snap), + * control=0x03, then a 5-byte SNAP header with Cisco OUI + * 0x00000c and protocol 0x0111 + * (SNAP_cisco_unidirectional_link_detection -- already listed + * in vnet/snap/snap.h's foreach_snap_cisco_protocol table, but + * nothing in stock VPP calls snap_register_input_protocol() for + * it, so snap-input's SNAP_INPUT_NEXT_DROP catches it). + * + * 2. This repo's copp/test_copp.py PTF UDLDTest (and hence the + * sonic-mgmt test_policer[UDLD] case this fixes): a bare, + * minimal frame with an all-zero 103-byte payload -- i.e. LLC + * dsap=ssap=0x00 (LLC_PROTOCOL_null). Nothing registers that + * SAP either, so llc-input's own LLC_INPUT_NEXT_DROP catches + * it one node earlier than case 1. The test only needs the + * dst-MAC + sub-0x600 "ethertype"/length field 0x0067 to be + * policed -- it does not construct a real SNAP header -- so + * this path must be handled too, or the test (and any other + * minimal/synthetic UDLD generator) never reaches a policer at + * all. + * + * Both dead ends are plugged with ONE shared node + * (sonic_ext_copp_udld_node), registered twice in + * sonic_ext_copp_udld_init() -- once via llc_register_input_protocol + * (LLC_PROTOCOL_null) for case 2, once via snap_register_input_protocol + * (Cisco OUI, unidirectional_link_detection) for case 1's payload + * after llc-input has already advanced past the LLC header and handed + * off to snap-input. Whichever path a given frame took, the node: + * + * 1. Restores the original wire L2 position (rewinds the buffer + * back past whatever llc-input / snap-input already consumed), + * mirroring sonic_ext_redirect_to_ingress_tap()'s "restore to + * l2_hdr_offset" step -- sonic-ext-copp-ifout expects to see an + * intact ethernet_header_t at vlib_buffer_get_current(). + * 2. Looks up the LCP host tap paired with the packet's ingress + * phy (VLIB_RX) -- same lcp_itf_pair_find_by_phy() lookup + * sonic_ext_redirect_to_ingress_tap() and sonic-ext-host-xc + * already use -- and sets VLIB_TX to that tap. + * 3. Hands off directly to the *existing* sonic-ext-copp-ifout + * node (not interface-output), so this new node stays a thin + * "reach the classify/policer node from an LLC/SNAP dead end" + * shim: sonic-ext-copp-ifout's ethertype match against 0x0067 + * (already bound today, see `show sonic-ext copp-ifout` -- + * vpp-idx was permanently -1 for this row before this fix + * because nothing ever reached it) does the actual policing, + * counting, and conform/exceed/violate accounting, with no + * duplicated logic here. + * + * If no LCP pair exists for the ingress phy (e.g. UDLD received on + * an interface VPP doesn't manage as an LCP pair), the packet is + * dropped -- there is no sane TAP to deliver it to, and silently + * falling through to interface-output on whatever left-over VLIB_TX + * happened to be set would misdeliver it. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include + +/* The ethertype/length value sonic-ext-copp-ifout's bind table uses + * for UDLD (see SwitchVppHostifTrap.cpp's buildClassifyMatchForTrapType, + * SAI_HOSTIF_TRAP_TYPE_UDLD case) -- both wire encodings this node + * handles carry this same value in the frame's 14th/15th bytes. */ +#define SONIC_EXT_COPP_UDLD_ETHERTYPE 0x0067 + +typedef struct +{ + u32 rx_sw_if_index; + u32 tx_sw_if_index; + u32 redirected; +} sonic_ext_copp_udld_trace_t; + +static u8 * +format_sonic_ext_copp_udld_trace (u8 *s, va_list *args) +{ + CLIB_UNUSED (vlib_main_t * vm) = va_arg (*args, vlib_main_t *); + CLIB_UNUSED (vlib_node_t * node) = va_arg (*args, vlib_node_t *); + sonic_ext_copp_udld_trace_t *t = + va_arg (*args, sonic_ext_copp_udld_trace_t *); + s = format (s, "SONIC-EXT-COPP-UDLD: rx %u tx %u %s", t->rx_sw_if_index, + t->tx_sw_if_index, + t->redirected ? "-> copp-ifout" : "DROP (no LCP pair)"); + return s; +} + +#define foreach_sonic_ext_copp_udld_error \ + _ (REDIRECTED, "UDLD (LLC/SNAP) redirected to copp-ifout for policing") \ + _ (NO_LCP, "UDLD dropped -- no LCP pair for ingress phy") + +typedef enum +{ +#define _(sym, str) SONIC_EXT_COPP_UDLD_ERROR_##sym, + foreach_sonic_ext_copp_udld_error +#undef _ + SONIC_EXT_COPP_UDLD_N_ERROR, +} sonic_ext_copp_udld_error_t; + +static char *sonic_ext_copp_udld_error_strings[] = { +#define _(sym, string) string, + foreach_sonic_ext_copp_udld_error +#undef _ +}; + +typedef enum +{ + SONIC_EXT_COPP_UDLD_NEXT_DROP, + SONIC_EXT_COPP_UDLD_NEXT_COPP_IFOUT, + SONIC_EXT_COPP_UDLD_N_NEXT, +} sonic_ext_copp_udld_next_t; + +/* Cached "interface-output" feature-arc index. sonic-ext-copp-ifout is a + * feature node on that arc; reaching it via a direct graph next-node jump + * (as this node does, from an LLC/SNAP dead end rather than the real + * interface-output dispatch) leaves b->current_config_index holding + * whatever stale value was set by the arc this packet last actually went + * through (e.g. device-input) -- sonic-ext-copp-ifout's own + * vnet_feature_next() call then reads that garbage and computes a bogus + * next-next index. vnet_feature_arc_start() below re-initializes it + * correctly for interface-output on the TAP we are about to redirect to, + * exactly as if the packet had entered this arc normally. Resolved lazily + * on first use since vnet_get_feature_arc_index() needs the feature + * subsystem to have finished its own init first. */ +static u8 sonic_ext_copp_udld_ifout_arc_index = (u8) ~0; + +VLIB_NODE_FN (sonic_ext_copp_udld_node) +(vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *frame) +{ + vnet_main_t *vnm = vnet_get_main (); + u32 n_left_from, *from; + vlib_buffer_t *bufs[VLIB_FRAME_SIZE], **b; + u16 nexts[VLIB_FRAME_SIZE], *next; + u32 n_redirected = 0, n_no_lcp = 0; + + if (PREDICT_FALSE (sonic_ext_copp_udld_ifout_arc_index == (u8) ~0)) + sonic_ext_copp_udld_ifout_arc_index = + vnet_get_feature_arc_index ("interface-output"); + + from = vlib_frame_vector_args (frame); + n_left_from = frame->n_vectors; + vlib_get_buffers (vm, from, bufs, n_left_from); + b = bufs; + next = nexts; + + while (n_left_from > 0) + { + /* On a LAG member port, VPP's bond plugin (vnet/bonding/node.c) + * rewrites sw_if_index[VLIB_RX] from the physical member to the + * bond aggregate for any ethertype it does not itself special-case + * (LACP/CDP/LLDP are let through unrewritten -- that is specifically + * why those protocols' CoPP punts land on the right member tap while + * UDLD, an ordinary ethertype from the bond plugin's point of view, + * does not). Before doing so it saves the true member interface into + * vnet_buffer2(b)->orig_rx_sw_if_index (left 0 -- never a valid + * sw_if_index -- when no rewrite happened). Prefer that saved value + * so a LAG-member UDLD punt resolves to the member's own LCP tap, + * not the bond's, matching LACP/LLDP's behavior. Confirmed live via + * `vppctl show trace`: without this, UDLD on a bond member landed on + * the bond's own tap (e.g. tap4134/be120) instead of the member's + * (e.g. tap4109/Ethernet48), so a listener on the member's tap never + * saw it even though CoPP policing itself was correct. */ + u32 orig_rx0 = vnet_buffer2 (b[0])->orig_rx_sw_if_index; + u32 rx0 = orig_rx0 ? orig_rx0 : vnet_buffer (b[0])->sw_if_index[VLIB_RX]; + u32 tx0 = ~0; + u32 phy_sw = rx0; + vnet_sw_interface_t *swo; + index_t lipi; + int did_redirect = 0; + + swo = vnet_get_sw_interface_or_null (vnm, rx0); + if (swo && swo->type == VNET_SW_INTERFACE_TYPE_SUB) + phy_sw = swo->sup_sw_if_index; + + lipi = lcp_itf_pair_find_by_phy (phy_sw); + if (PREDICT_TRUE (lipi != INDEX_INVALID)) + { + const lcp_itf_pair_t *lip = lcp_itf_pair_get (lipi); + + /* Rewind past whatever llc-input (and, for the real-SNAP + * path, snap-input too) already consumed so + * sonic-ext-copp-ifout sees an intact Ethernet header, the + * same way sonic_ext_redirect_to_ingress_tap() restores + * to l2_hdr_offset for the aggregate-tap redirect path. */ + i32 adv = (i32) vnet_buffer (b[0])->l2_hdr_offset - + (i32) b[0]->current_data; + if (adv) + vlib_buffer_advance (b[0], adv); + + tx0 = lip->lip_host_sw_if_index; + vnet_buffer (b[0])->sw_if_index[VLIB_TX] = tx0; + + /* Pre-resolve the copp-ifout entry for UDLD via the key SAI bound + * it under (SwitchVppHostifTrap.cpp's SONIC_EXT_COPP_UDLD_ETHERTYPE), + * and tag the buffer with it. Real protocol dispatch got us here + * (LLC-null or LLC+SNAP+Cisco-UDLD-OUI), unlike copp-ifout's own + * fallback match, which reads a fixed wire-byte offset that for + * UDLD holds an 802.3 *length* field (varies with the frame's + * actual TLV payload) rather than a stable EtherType -- that byte + * match can never reliably identify UDLD in general. Leaving the + * tag unset (sonic-ext-capture's ~0 default) if the entry isn't + * bound yet is fine: copp-ifout's fallback path then finds nothing + * either and passes the packet through unpoliced, same as today + * before this trap is configured. */ + { + sonic_ext_main_t *sem = &sonic_ext_main; + int ifout_idx = sonic_ext_copp_ifout_find_entry ( + sem, SONIC_EXT_COPP_UDLD_ETHERTYPE); + + if (ifout_idx >= 0) + { + sonic_ext_buffer_opaque_t *seb = sonic_ext_buffer (b[0]); + seb->copp_ifout_entry_idx = (u32) ifout_idx; + } + } + + /* Re-initialize the interface-output feature-arc position for + * this buffer on the TAP we're redirecting to -- see comment + * on sonic_ext_copp_udld_ifout_arc_index above. Without this, + * sonic-ext-copp-ifout's vnet_feature_next() reads whatever + * stale current_config_index this buffer had from the arc it + * actually traversed (e.g. device-input), producing a bogus + * next node -- observed live: conforming UDLD packets landed + * in ip4-drop instead of ever reaching the TAP. */ + { + u32 dummy_next; + vnet_feature_arc_start (sonic_ext_copp_udld_ifout_arc_index, tx0, + &dummy_next, b[0]); + } + + next[0] = SONIC_EXT_COPP_UDLD_NEXT_COPP_IFOUT; + did_redirect = 1; + n_redirected++; + } + else + { + next[0] = SONIC_EXT_COPP_UDLD_NEXT_DROP; + n_no_lcp++; + } + + if (PREDICT_FALSE ((node->flags & VLIB_NODE_FLAG_TRACE) && + (b[0]->flags & VLIB_BUFFER_IS_TRACED))) + { + sonic_ext_copp_udld_trace_t *t = + vlib_add_trace (vm, node, b[0], sizeof (*t)); + t->rx_sw_if_index = rx0; + t->tx_sw_if_index = tx0; + t->redirected = did_redirect; + } + + b += 1; + next += 1; + n_left_from -= 1; + } + + vlib_buffer_enqueue_to_next (vm, node, from, nexts, frame->n_vectors); + + if (n_redirected) + vlib_node_increment_counter (vm, sonic_ext_copp_udld_node.index, + SONIC_EXT_COPP_UDLD_ERROR_REDIRECTED, + n_redirected); + if (n_no_lcp) + vlib_node_increment_counter (vm, sonic_ext_copp_udld_node.index, + SONIC_EXT_COPP_UDLD_ERROR_NO_LCP, n_no_lcp); + + return frame->n_vectors; +} + +VLIB_REGISTER_NODE (sonic_ext_copp_udld_node) = { + .name = "sonic-ext-copp-udld", + .vector_size = sizeof (u32), + .format_trace = format_sonic_ext_copp_udld_trace, + .type = VLIB_NODE_TYPE_INTERNAL, + .n_errors = ARRAY_LEN (sonic_ext_copp_udld_error_strings), + .error_strings = sonic_ext_copp_udld_error_strings, + .n_next_nodes = SONIC_EXT_COPP_UDLD_N_NEXT, + .next_nodes = { + [SONIC_EXT_COPP_UDLD_NEXT_DROP] = "error-drop", + [SONIC_EXT_COPP_UDLD_NEXT_COPP_IFOUT] = "sonic-ext-copp-ifout", + }, +}; + +/* + * Register with both LLC dead ends UDLD can arrive at -- see file + * header for why both are needed. Run from a VLIB_INIT_FUNCTION (not + * the plugin's main VLIB_PLUGIN_REGISTER-time init) so this runs + * once, after llc-input/snap-input's own node-graph init functions + * (llc_input_init / snap_input_init) have already run and registered + * the node graph edges these calls extend -- both + * llc_register_input_protocol() and snap_register_input_protocol() + * internally call vlib_call_init_function() on their respective + * *_input_init first if needed, so ordering here is self-managing. + */ +/* sonic-mgmt's own PTF UDLDTest (copp/test_copp.py's construct_packet, via + * ptf.testutils.simple_eth_packet with no explicit payload) sends the + * minimal frame described in case 2 above, but its payload filler is NOT + * null bytes -- ptf.testutils.simple_eth_packet pads with the ASCII + * character '0' (pkt / ("0" * (pktlen - len(pkt))), confirmed in the + * vendored ptf/testutils.py). That puts LLC dsap=ssap=0x30 (ASCII '0'), not + * 0x00/LLC_PROTOCOL_null, on the wire for every packet this specific test + * tool generates. Confirmed live via `vppctl show trace`: with only + * LLC_PROTOCOL_null (0x00) registered, this exact traffic hits + * llc-input's own "unknown llc ssap/dsap" drop and never reaches this + * node at all. + * + * llc_register_input_protocol() cannot be called with 0x30 -- it looks up + * llc_protocol_info_t via llc_get_protocol_info(), which only resolves the + * ~20 SAP values VPP's own vnet/llc/llc.h foreach_llc_protocol table lists + * (0x30 is not one), and unconditionally dereferences a NULL result -- + * confirmed by crash: SIGSEGV in llc_register_input_protocol when called + * with 0x30. The actual dispatch llc-input's node reads at runtime is a + * much simpler public array, llc_main.input_next_by_protocol[256], indexed + * directly by the wire dsap byte (see vnet/llc/node.c); the crash-prone + * llc_get_protocol_info() bookkeeping exists only for named-protocol CLI + * introspection this plugin does not need. sonic_ext_copp_udld_register_llc_sap() + * below writes that array directly for 0x30, via the same vlib_node_add_next() + * primitive llc_register_input_protocol() itself uses, without touching the + * SAP-name hash table at all. */ +static void +sonic_ext_copp_udld_register_llc_sap (vlib_main_t *vm, u8 sap, u32 node_index) +{ + llc_main_t *lm = &llc_main; + u32 next_index; + + /* Ensure llc-input's own init (which resets input_next_by_protocol[] to + * all-DROP) has already run, exactly as llc_register_input_protocol() + * itself guarantees before touching the table. */ + { + clib_error_t *error = vlib_call_init_function (vm, llc_input_init); + if (error) + clib_error_report (error); + } + + next_index = vlib_node_add_next (vm, llc_input_node.index, node_index); + lm->input_next_by_protocol[sap] = next_index; +} + +#define SONIC_EXT_COPP_UDLD_LLC_SAP_PTF_TEST 0x30 + +static clib_error_t * +sonic_ext_copp_udld_init (vlib_main_t *vm) +{ + llc_register_input_protocol (vm, LLC_PROTOCOL_null, + sonic_ext_copp_udld_node.index); + + sonic_ext_copp_udld_register_llc_sap ( + vm, SONIC_EXT_COPP_UDLD_LLC_SAP_PTF_TEST, sonic_ext_copp_udld_node.index); + + snap_register_input_protocol (vm, "sonic-ext-copp-udld", IEEE_OUI_cisco, + SNAP_cisco_unidirectional_link_detection, + sonic_ext_copp_udld_node.index); + + return 0; +} + +VLIB_INIT_FUNCTION (sonic_ext_copp_udld_init); diff --git a/vppbld/plugins/sonic_ext/sonic_ext.api b/vppbld/plugins/sonic_ext/sonic_ext.api index 476bac98..ad33a3ea 100644 --- a/vppbld/plugins/sonic_ext/sonic_ext.api +++ b/vppbld/plugins/sonic_ext/sonic_ext.api @@ -33,3 +33,155 @@ autoreply define sonic_ext_ip2me_enable_disable vl_api_interface_index_t sw_if_index; bool enable; }; + +/** \brief Bind (or unbind) an Ethernet ethertype to an existing VPP + policer object, enforced on the interface-output arc of every + linux-cp host TAP -- i.e. after linux-cp has already decided a + packet is CPU-bound and redirected it to the TAP, so this only + ever meters traffic already known to be punted (ARP/LACP/LLDP/ + UDLD/TTL_ERROR), never ordinary forwarded traffic. + + @param client_index - opaque cookie to identify the sender + @param context - sender context, to match reply w/ request + @param ethertype - the Ethernet ethertype to match (host byte + order, e.g. 0x0806 for ARP, 0x8809 for LACP, 0x88cc for + LLDP, or 0x0800 for match_ip4_ttl_expiring below) + @param policer_name - name of an existing VPP policer object + (created via policer_add, e.g. "copp-policer-0x") + @param is_bind - 1 to add/replace the binding, 0 to remove it + @param match_ip4_ttl_expiring - when true (only meaningful with + ethertype 0x0800), additionally requires the IPv4 header's + TTL to be <= 1 for a packet to match this entry -- this is + how CoPP's TTL_ERROR trap is implemented. +*/ +define sonic_ext_copp_ifout_bind +{ + u32 client_index; + u32 context; + u16 ethertype; + string policer_name[64]; + bool is_bind; + bool match_ip4_ttl_expiring; +}; + +define sonic_ext_copp_ifout_bind_reply +{ + u32 context; + i32 retval; +}; + +/** \brief Read back this feature's own conform/exceed/violate packet + counters for one bound ethertype. + + @param ethertype - the ethertype to query counters for +*/ +define sonic_ext_copp_ifout_get_counters +{ + u32 client_index; + u32 context; + u16 ethertype; +}; + +define sonic_ext_copp_ifout_get_counters_reply +{ + u32 context; + i32 retval; + u64 conform_packets; + u64 exceed_packets; + u64 violate_packets; +}; + +/** \brief Add or remove one IPv4 address from the IP2ME set enforced on + the ip4-punt feature arc (device-agnostic -- addresses are checked + regardless of which interface the packet arrived on, since ip4-punt + only runs after routing has already decided the packet is destined + to one of the router's own addresses). + + @param client_index - opaque cookie to identify the sender + @param context - sender context, to match reply w/ request + @param addr - the IPv4 address (network byte order) to add/remove + @param is_add - 1 to add, 0 to remove +*/ +define sonic_ext_copp_ip2me_addr_add_del +{ + u32 client_index; + u32 context; + u32 addr; + bool is_add; +}; + +define sonic_ext_copp_ip2me_addr_add_del_reply +{ + u32 context; + i32 retval; +}; + +/** \brief Bind (or unbind) the shared IP2ME policer used to meter all + currently-registered IP2ME addresses (SSH/SNMP/IP2ME all map to the + same SAI ip2me trap and therefore the same policer). + + @param policer_name - name of an existing VPP policer object + (created via policer_add), looked up once via policer_main's + policer_index_by_name hash + @param is_bind - 1 to bind, 0 to unbind +*/ +define sonic_ext_copp_ip2me_bind +{ + u32 client_index; + u32 context; + string policer_name[64]; + bool is_bind; +}; + +define sonic_ext_copp_ip2me_bind_reply +{ + u32 context; + i32 retval; +}; + +/** \brief Bind (or unbind) an INDEPENDENT policer slot for BGP/BGPV6 + traffic, matched by TCP destination port 179 rather than IP2ME's + destination-address set. BGP/BGPV6 are their own distinct SAI trap + types with their own trap group/policer (queue4_group1, separate + from ip2me's queue1_group1) -- this bind is looked up/cleared + strictly by policer_name, so installing or removing the bgp trap + can never disturb sonic_ext_copp_ip2me_bind()'s separate IP2ME/ + SNMP/SSH slot, or vice versa. + + @param policer_name - name of an existing VPP policer object + (created via policer_add), looked up once via policer_main's + policer_index_by_name hash + @param is_bind - 1 to bind, 0 to unbind +*/ +define sonic_ext_copp_ip2me_bind_bgp +{ + u32 client_index; + u32 context; + string policer_name[64]; + bool is_bind; +}; + +define sonic_ext_copp_ip2me_bind_bgp_reply +{ + u32 context; + i32 retval; +}; + +/** \brief Read back this feature's own conform/exceed/violate packet + counters. +*/ +define sonic_ext_copp_ip2me_get_counters +{ + u32 client_index; + u32 context; +}; + +define sonic_ext_copp_ip2me_get_counters_reply +{ + u32 context; + i32 retval; + u64 conform_packets; + u64 exceed_packets; + u64 violate_packets; +}; + diff --git a/vppbld/plugins/sonic_ext/sonic_ext.c b/vppbld/plugins/sonic_ext/sonic_ext.c index 63d9305e..e0a492fd 100644 --- a/vppbld/plugins/sonic_ext/sonic_ext.c +++ b/vppbld/plugins/sonic_ext/sonic_ext.c @@ -414,6 +414,20 @@ sonic_ext_lcp_pair_add_cb (lcp_itf_pair_t *lip) sonic_ext_host_xc_enable_disable (lip->lip_host_sw_if_index, 1); if (sonic_ext_phy_is_aggregate (lip->lip_phy_sw_if_index)) sonic_ext_aggr_tap_redirect_enable_disable (lip->lip_host_sw_if_index, 1); + /* sonic-ext-copp-ifout: bind ONLY on a direct/member phy's own host + * tap, never on an aggregate's (bond/BVI) own host tap. For a bond + * member, ARP/LACP/LLDP/UDLD/TTL_ERROR is first punted to the + * *aggregate's* tap (see sonic-ext-aggr-tap-redirect's header + * comment), which redirects it onward to the member's own tap and + * re-enters interface-output there. If this policer were also + * bound on the aggregate tap, that single physical packet would be + * policed twice (once per interface-output pass) against the same + * shared CIR budget -- confirmed live: policer hit-count ran ~3x + * the actual packets sent, starving real delivery. Binding only on + * non-aggregate taps polices each packet exactly once, on its + * final (member) tap. */ + if (!sonic_ext_phy_is_aggregate (lip->lip_phy_sw_if_index)) + sonic_ext_copp_ifout_enable_disable (lip->lip_host_sw_if_index, 1); } static void @@ -429,6 +443,25 @@ sonic_ext_lcp_pair_del_cb (lcp_itf_pair_t *lip) sonic_ext_host_xc_enable_disable (lip->lip_host_sw_if_index, 0); if (sonic_ext_phy_is_aggregate (lip->lip_phy_sw_if_index)) sonic_ext_aggr_tap_redirect_enable_disable (lip->lip_host_sw_if_index, 0); + if (!sonic_ext_phy_is_aggregate (lip->lip_phy_sw_if_index)) + sonic_ext_copp_ifout_enable_disable (lip->lip_host_sw_if_index, 0); +} + + +/* + * lcp_itf_pair_walk callback: enable sonic-ext-copp-ifout on this + * pair's host tap. Used at plugin-init time to catch pairs created + * before the plugin's own VLIB_INIT_FUNCTION ran (shouldn't normally + * happen given plugin init ordering, but mirrors the same + * belt-and-suspenders pattern host-xc/aggr-tap-redirect already use). + */ +static walk_rc_t +sonic_ext_copp_ifout_walk_enable_cb (index_t lipi, void *ctx) +{ + const lcp_itf_pair_t *lip = lcp_itf_pair_get (lipi); + if (lip && !sonic_ext_phy_is_aggregate (lip->lip_phy_sw_if_index)) + sonic_ext_copp_ifout_enable_disable (lip->lip_host_sw_if_index, 1); + return WALK_CONTINUE; } static clib_error_t * @@ -452,7 +485,32 @@ sonic_ext_init (vlib_main_t *vm) sonic_ext_set_punt_via_member (1); sonic_ext_set_host_xc (1); + /* sonic-ext-copp-ifout has no on/off toggle -- it always binds, and + * relies on its own (initially empty) bind table to no-op until + * something is actually bound via sonic_ext_copp_ifout_bind(). Walk + * any pre-existing pairs for the same belt-and-suspenders reason as + * above; going forward the LCP pair add/del callback keeps it in + * sync, including across `config reload`. */ + lcp_itf_pair_walk (sonic_ext_copp_ifout_walk_enable_cb, NULL); + + /* sonic-ext-copp-ip2me is a single global feature on the ip4-punt + * arc (dispatched with sw_if_index 0, same as glean-redirect on + * ip4-drop/ip6-drop above) -- enable it once, unconditionally, at + * init. No per-interface binding is needed or ever done: the node + * self-scopes via its own (initially empty) tracked-address table + * and no-ops until addresses are added via + * sonic_ext_copp_ip2me_addr_add_del() / bound via + * sonic_ext_copp_ip2me_bind(). Without this call the node is only + * registered in the ip4-punt arc's graph (which is why `show + * sonic-ext copp-ip2me` reports it as bound with addresses) but + * never actually visited by any packet, silently letting all IP2ME/ + * SNMP/SSH/BGP traffic through completely unpoliced. */ + vnet_feature_enable_disable ("ip4-punt", "sonic-ext-copp-ip2me", 0, 1, 0, + 0); + return 0; } VLIB_INIT_FUNCTION (sonic_ext_init); + + diff --git a/vppbld/plugins/sonic_ext/sonic_ext.h b/vppbld/plugins/sonic_ext/sonic_ext.h index b21e0966..a6ecc2e4 100644 --- a/vppbld/plugins/sonic_ext/sonic_ext.h +++ b/vppbld/plugins/sonic_ext/sonic_ext.h @@ -80,11 +80,29 @@ * VLAN before re-entering interface-output on the member tap, so * Linux observes the same wire frame on the right netdev. */ +/* + * copp_ifout_entry_idx: index into sonic_ext_main_t.copp_ifout_entries[], + * pre-resolved by a producer node that has already identified the packet's + * CoPP class through real protocol dispatch (not a positional byte read). + * ~0 means "no pre-resolved entry -- fall back to the ethertype/length-byte + * match". Set by sonic-ext-copp-udld: UDLD is not Ethernet-II (its 14th/15th + * wire bytes are an 802.3 *length* field, not an EtherType, and that length + * varies with the real TLV payload a UDLD frame carries), so matching it by + * byte value in sonic-ext-copp-ifout the way ARP/LACP/LLDP/TTL_ERROR's real + * EtherTypes are matched cannot work in general -- only a specific test + * packet of exactly one length would ever match. sonic-ext-copp-udld reaches + * this packet via VPP's real LLC-null / LLC+SNAP+Cisco-UDLD-OUI dispatch + * (genuine protocol identification, not a length guess), so it looks up the + * right copp_ifout_entries[] slot itself and tags it here; sonic-ext-copp- + * ifout then polices using the tag directly instead of re-deriving (and + * getting wrong) a match from wire bytes. + */ typedef struct { u32 magic; u32 orig_rx_sw_if_index; u32 orig_vlan_tag; + u32 copp_ifout_entry_idx; } sonic_ext_buffer_opaque_t; STATIC_ASSERT (sizeof (sonic_ext_buffer_opaque_t) <= @@ -97,6 +115,73 @@ sonic_ext_buffer (vlib_buffer_t *b) return (sonic_ext_buffer_opaque_t *) vnet_buffer2 (b)->unused; } +/* + * sonic-ext-copp-ifout: interface-output CoPP policer for + * ARP/LACP/LLDP/UDLD/TTL_ERROR. See copp_ifout_node.c for the full + * design rationale (why interface-output, not device-input). + */ +#define SONIC_EXT_COPP_IFOUT_MAX_ENTRIES 16 +#define SONIC_EXT_COPP_IFOUT_NAME_LEN 64 + +typedef struct +{ + u16 ethertype; /* host byte order */ + u8 name[SONIC_EXT_COPP_IFOUT_NAME_LEN]; + u32 policer_index; + u8 in_use; + u8 match_ip4_ttl_expiring; /* TTL_ERROR trap */ +} sonic_ext_copp_ifout_entry_t; + +/* + * sonic-ext-copp-ip2me: IPv4 destination addresses considered "IP2ME" + * (traffic destined to one of the router's own addresses reaching + * ip4-punt) plus the shared policer binding for IP2ME/SNMP/SSH. See + * copp_ip2me_node.c for the full design rationale. + */ +#define SONIC_EXT_COPP_IP2ME_MAX_ADDRS 256 + +typedef struct +{ + u32 addr; /* network byte order, matches ip4_address_t.as_u32 */ + u8 in_use; +} sonic_ext_copp_ip2me_addr_t; + +/* + * sonic-ext-copp-ip2me now supports more than one independently + * bound policer on the ip4-punt arc, keyed by SAI trap group (one + * VPP policer object per trap group, named "copp-policer-0x"). + * IP2ME/SNMP/SSH share a single SAI trap type/policer and use table + * slot kind ADDR (matched by destination IPv4 address, the + * pre-existing behavior). BGP/BGPV6 are a genuinely distinct SAI + * trap type/trap-group/policer and are matched by TCP destination + * port 179 instead -- BGP has no address set of its own (it rides on + * whichever router-interface IP the peer dials, same as IP2ME + * addresses, but keyed by port rather than address so its own + * install/uninstall never disturbs IP2ME/SNMP/SSH's binding or vice + * versa). Each slot is independently bound/unbound so disabling one + * SAI trap only ever removes its own slot -- see + * sonic_ext_copp_ip2me_bind_keyed() in copp_ip2me_node.c. + */ +#define SONIC_EXT_COPP_IP2ME_MAX_POLICERS 8 + +typedef enum +{ + SONIC_EXT_COPP_IP2ME_MATCH_ADDR = 0, /* dst IPv4 in copp_ip2me_addrs[] */ + SONIC_EXT_COPP_IP2ME_MATCH_TCP_DPORT = 1, /* dst TCP port == match_tcp_dport */ +} sonic_ext_copp_ip2me_match_kind_t; + +typedef struct +{ + u8 name[SONIC_EXT_COPP_IFOUT_NAME_LEN]; + u32 policer_index; + u8 in_use; + u8 match_kind; /* sonic_ext_copp_ip2me_match_kind_t */ + u16 match_tcp_dport; /* only used when match_kind == MATCH_TCP_DPORT */ + u64 conform_packets; + u64 exceed_packets; + u64 violate_packets; +} sonic_ext_copp_ip2me_policer_t; + typedef struct { /* API message ID base */ @@ -120,6 +205,21 @@ typedef struct u64 host_xc_direct; u64 l2_trap_fixups; u64 ip2me_hits; + + /* sonic-ext-copp-ifout: ethertype -> policer binding table. */ + sonic_ext_copp_ifout_entry_t copp_ifout_entries[SONIC_EXT_COPP_IFOUT_MAX_ENTRIES]; + u32 copp_ifout_n_entries; + u64 copp_ifout_conform_packets[SONIC_EXT_COPP_IFOUT_MAX_ENTRIES]; + u64 copp_ifout_exceed_packets[SONIC_EXT_COPP_IFOUT_MAX_ENTRIES]; + u64 copp_ifout_violate_packets[SONIC_EXT_COPP_IFOUT_MAX_ENTRIES]; + + /* sonic-ext-copp-ip2me: IP2ME/SNMP/SSH/BGP/BGPV6 policing on + * ip4-punt, keyed per SAI trap group -- see + * sonic_ext_copp_ip2me_policer_t above. */ + sonic_ext_copp_ip2me_addr_t copp_ip2me_addrs[SONIC_EXT_COPP_IP2ME_MAX_ADDRS]; + u32 copp_ip2me_n_addrs; + sonic_ext_copp_ip2me_policer_t copp_ip2me_policers[SONIC_EXT_COPP_IP2ME_MAX_POLICERS]; + u32 copp_ip2me_n_policers; } sonic_ext_main_t; extern sonic_ext_main_t sonic_ext_main; @@ -132,6 +232,9 @@ extern vlib_node_registration_t sonic_ext_l2_trap_fixup_node; extern vlib_node_registration_t sonic_ext_l2_vlan_filter_node; extern vlib_node_registration_t sonic_ext_ip2me_ip4_node; extern vlib_node_registration_t sonic_ext_ip2me_ip6_node; +extern vlib_node_registration_t sonic_ext_copp_ifout_node; +extern vlib_node_registration_t sonic_ext_copp_ip2me_node; +extern vlib_node_registration_t sonic_ext_copp_udld_node; /* Enable / disable sonic-ext-capture on a given interface. No-op if * the capture sidecar is not yet initialized. */ @@ -189,4 +292,46 @@ int sonic_ext_phy_is_bvi (u32 phy_sw_if_index); * punts to the bond master host tap. */ int sonic_ext_phy_is_bond (u32 phy_sw_if_index); +/* + * sonic-ext-copp-ifout: enable/disable the interface-output CoPP + * policer feature on a given TAP sw_if_index. Driven from the LCP + * pair add/del callback -- see sonic_ext.c. + */ +void sonic_ext_copp_ifout_enable_disable (u32 sw_if_index, int enable); + +/* + * sonic-ext-copp-ifout: look up an entry by wire ethertype/length value. + * Exposed so sonic-ext-copp-udld can pre-resolve UDLD's entry via real + * protocol dispatch (LLC-null / LLC+SNAP+Cisco-UDLD) and tag the buffer + * with the result -- see sonic_ext_buffer_opaque_t.copp_ifout_entry_idx. + * Returns -1 if no matching in-use entry exists. + */ +int sonic_ext_copp_ifout_find_entry (sonic_ext_main_t *sem, u16 ethertype); + +/* + * sonic-ext-copp-ifout: bind (or unbind) an ethertype -> policer-name + * entry consulted by the interface-output node above. See + * copp_ifout_node.c for the full semantics (mirrors the predecessor + * copp_punt_policer plugin's copp_punt_policer_bind()). + */ +int sonic_ext_copp_ifout_bind (u16 ethertype, const char *policer_name, + int is_bind, int match_ip4_ttl_expiring); + +/* + * sonic-ext-copp-ip2me: add/remove an IP2ME address (used by + * IP2ME/SNMP/SSH's address-match slot only -- BGP/BGPV6 do not use + * an address set, see sonic_ext_copp_ip2me_match_kind_t above), and + * bind/unbind a policer for a specific SAI trap group's traffic + * class. sonic_ext_copp_ip2me_bind() is the legacy shared + * IP2ME/SNMP/SSH address-match slot; sonic_ext_copp_ip2me_bind_bgp() + * binds/unbinds a SEPARATE slot matched by TCP dst port 179, so BGP's + * own install/uninstall never disturbs IP2ME/SNMP/SSH's binding (and + * vice versa) -- each SAI trap's classify call must pass its own + * trap group's unique "copp-policer-0x" policer_name. See + * copp_ip2me_node.c. + */ +int sonic_ext_copp_ip2me_addr_add_del (u32 addr, int is_add); +int sonic_ext_copp_ip2me_bind (const char *policer_name, int is_bind); +int sonic_ext_copp_ip2me_bind_bgp (const char *policer_name, int is_bind); + #endif /* __included_sonic_ext_h__ */ diff --git a/vppbld/plugins/sonic_ext/sonic_ext_api.c b/vppbld/plugins/sonic_ext/sonic_ext_api.c index 997be95d..8dd89705 100644 --- a/vppbld/plugins/sonic_ext/sonic_ext_api.c +++ b/vppbld/plugins/sonic_ext/sonic_ext_api.c @@ -56,9 +56,134 @@ vl_api_sonic_ext_ip2me_enable_disable_t_handler ( REPLY_MACRO (VL_API_SONIC_EXT_IP2ME_ENABLE_DISABLE_REPLY); } +static void +vl_api_sonic_ext_copp_ifout_bind_t_handler ( + vl_api_sonic_ext_copp_ifout_bind_t *mp) +{ + vl_api_sonic_ext_copp_ifout_bind_reply_t *rmp; + int rv; + char name[64]; + + snprintf (name, sizeof (name), "%s", mp->policer_name); + rv = sonic_ext_copp_ifout_bind (ntohs (mp->ethertype), name, mp->is_bind, + mp->match_ip4_ttl_expiring); + + REPLY_MACRO (VL_API_SONIC_EXT_COPP_IFOUT_BIND_REPLY); +} + +static void +vl_api_sonic_ext_copp_ifout_get_counters_t_handler ( + vl_api_sonic_ext_copp_ifout_get_counters_t *mp) +{ + sonic_ext_main_t *sem = &sonic_ext_main; + vl_api_sonic_ext_copp_ifout_get_counters_reply_t *rmp; + int rv = 0; + u64 conform = 0, exceed = 0, violate = 0; + u16 ethertype = ntohs (mp->ethertype); + int idx = -1; + + for (u32 i = 0; i < sem->copp_ifout_n_entries; i++) + { + if (sem->copp_ifout_entries[i].in_use && + sem->copp_ifout_entries[i].ethertype == ethertype) + { + idx = (int) i; + break; + } + } + + if (idx < 0) + rv = VNET_API_ERROR_NO_SUCH_ENTRY; + else + { + conform = sem->copp_ifout_conform_packets[idx]; + exceed = sem->copp_ifout_exceed_packets[idx]; + violate = sem->copp_ifout_violate_packets[idx]; + } + + REPLY_MACRO2 (VL_API_SONIC_EXT_COPP_IFOUT_GET_COUNTERS_REPLY, + ({ + rmp->conform_packets = clib_host_to_net_u64 (conform); + rmp->exceed_packets = clib_host_to_net_u64 (exceed); + rmp->violate_packets = clib_host_to_net_u64 (violate); + })); +} + +static void +vl_api_sonic_ext_copp_ip2me_addr_add_del_t_handler ( + vl_api_sonic_ext_copp_ip2me_addr_add_del_t *mp) +{ + vl_api_sonic_ext_copp_ip2me_addr_add_del_reply_t *rmp; + int rv; + + rv = sonic_ext_copp_ip2me_addr_add_del (mp->addr, mp->is_add); + + REPLY_MACRO (VL_API_SONIC_EXT_COPP_IP2ME_ADDR_ADD_DEL_REPLY); +} + +static void +vl_api_sonic_ext_copp_ip2me_bind_t_handler ( + vl_api_sonic_ext_copp_ip2me_bind_t *mp) +{ + vl_api_sonic_ext_copp_ip2me_bind_reply_t *rmp; + int rv; + char name[64]; + + snprintf (name, sizeof (name), "%s", mp->policer_name); + rv = sonic_ext_copp_ip2me_bind (name, mp->is_bind); + + REPLY_MACRO (VL_API_SONIC_EXT_COPP_IP2ME_BIND_REPLY); +} + +static void +vl_api_sonic_ext_copp_ip2me_bind_bgp_t_handler ( + vl_api_sonic_ext_copp_ip2me_bind_bgp_t *mp) +{ + vl_api_sonic_ext_copp_ip2me_bind_bgp_reply_t *rmp; + int rv; + char name[64]; + + snprintf (name, sizeof (name), "%s", mp->policer_name); + rv = sonic_ext_copp_ip2me_bind_bgp (name, mp->is_bind); + + REPLY_MACRO (VL_API_SONIC_EXT_COPP_IP2ME_BIND_BGP_REPLY); +} + +static void +vl_api_sonic_ext_copp_ip2me_get_counters_t_handler ( + vl_api_sonic_ext_copp_ip2me_get_counters_t *mp) +{ + sonic_ext_main_t *sem = &sonic_ext_main; + vl_api_sonic_ext_copp_ip2me_get_counters_reply_t *rmp; + int rv = 0; + u64 conform = 0, exceed = 0, violate = 0; + + /* Aggregate across every bound policer slot (legacy address-match + * IP2ME/SNMP/SSH slot plus any TCP-dst-port slots such as BGP/ + * BGPV6) -- this API predates the multi-slot design and callers + * only ever used it for the single shared IP2ME counter, so summing + * preserves that behavior for anyone still calling it that way. */ + for (u32 i = 0; i < sem->copp_ip2me_n_policers; i++) + { + if (!sem->copp_ip2me_policers[i].in_use) + continue; + conform += sem->copp_ip2me_policers[i].conform_packets; + exceed += sem->copp_ip2me_policers[i].exceed_packets; + violate += sem->copp_ip2me_policers[i].violate_packets; + } + + REPLY_MACRO2 (VL_API_SONIC_EXT_COPP_IP2ME_GET_COUNTERS_REPLY, + ({ + rmp->conform_packets = clib_host_to_net_u64 (conform); + rmp->exceed_packets = clib_host_to_net_u64 (exceed); + rmp->violate_packets = clib_host_to_net_u64 (violate); + })); +} + /* API definitions */ #include + static clib_error_t * sonic_ext_api_init (vlib_main_t *vm) { From 3cd2f886249a7a7732b0f488f79347aa45a97268 Mon Sep 17 00:00:00 2001 From: Nikhil Hegde Date: Fri, 18 Sep 2026 20:58:59 +0000 Subject: [PATCH 6/7] Trim verbose comments in sonic_ext CoPP nodes Signed-off-by: Nikhil Hegde --- vppbld/plugins/sonic_ext/FEATURE.yaml | 9 +- vppbld/plugins/sonic_ext/capture_node.c | 6 +- vppbld/plugins/sonic_ext/copp_ifout_node.c | 59 +---------- vppbld/plugins/sonic_ext/copp_ip2me_node.c | 45 +------- vppbld/plugins/sonic_ext/copp_udld_node.c | 113 ++------------------- vppbld/plugins/sonic_ext/sonic_ext.c | 45 +------- vppbld/plugins/sonic_ext/sonic_ext.h | 76 ++------------ vppbld/plugins/sonic_ext/sonic_ext_api.c | 6 -- 8 files changed, 28 insertions(+), 331 deletions(-) diff --git a/vppbld/plugins/sonic_ext/FEATURE.yaml b/vppbld/plugins/sonic_ext/FEATURE.yaml index 6f423003..52023a5b 100644 --- a/vppbld/plugins/sonic_ext/FEATURE.yaml +++ b/vppbld/plugins/sonic_ext/FEATURE.yaml @@ -3,12 +3,9 @@ maintainer: SONiC-VPP contributors features: - punt-via-member: redirect punted unicast/ARP over aggregated interface (BVI, Bond) to the original member tap - host-xc: bypass ethernet-input for packets injected from the linux-cp host tap - - copp-ifout: per-ethertype CoPP rate policing (ARP, LACP, LLDP, UDLD, - TTL_ERROR) on the interface-output arc of every linux-cp host TAP -- - only meters traffic already decided to be punted, never ordinary - forwarded traffic - - copp-ip2me: CoPP rate policing for IP2ME/SNMP/SSH traffic on the - global ip4-punt arc, ahead of ip4-punt-redirect + - copp-ifout: per-ethertype CoPP rate policing of punted traffic (ARP/LACP/LLDP/UDLD/TTL_ERROR) at linux-cp TAP egress + - copp-ip2me: CoPP identifier IP2ME/SNMP/SSH traffic on the ip4-punt arc + - copp-udld: CoPP identifier for UDLD via real LLC/SNAP dispatch description: "VPP extensions for SONiC features" state: experimental properties: [API, CLI] diff --git a/vppbld/plugins/sonic_ext/capture_node.c b/vppbld/plugins/sonic_ext/capture_node.c index 019ebf5b..19f1cb00 100644 --- a/vppbld/plugins/sonic_ext/capture_node.c +++ b/vppbld/plugins/sonic_ext/capture_node.c @@ -113,11 +113,7 @@ VLIB_NODE_FN (sonic_ext_capture_node) seb->magic = SONIC_EXT_BUFFER_MAGIC; - /* No pre-resolved copp-ifout entry yet -- sonic-ext-copp-udld sets - * this to a real index for the one class of traffic (UDLD) that - * cannot be matched by sonic-ext-copp-ifout's own ethertype/length - * byte read. Every other packet leaves this as ~0 and copp-ifout - * falls back to its normal byte-match path unchanged. */ + /* sonic-ext-copp-udld sets this to a real index for UDLD traffic */ seb->copp_ifout_entry_idx = ~0; n_captured++; diff --git a/vppbld/plugins/sonic_ext/copp_ifout_node.c b/vppbld/plugins/sonic_ext/copp_ifout_node.c index 219b1fe6..27295a8a 100644 --- a/vppbld/plugins/sonic_ext/copp_ifout_node.c +++ b/vppbld/plugins/sonic_ext/copp_ifout_node.c @@ -16,7 +16,6 @@ * * CoPP per-ethertype rate policing for ARP/LACP/LLDP/UDLD/TTL_ERROR, * enforced on the `interface-output` arc of each linux-cp-paired TAP - * -- NOT on `device-input` (see history below). * * BACKGROUND (sonic-net/sonic-buildimage#25801, SONiC-on-VPP CoPP HLD): * VPP's existing classify-based policer feature (policer-classify) @@ -25,45 +24,6 @@ * any of those arcs -- ethernet-input dispatches it directly to * arp-input / linux-cp-punt-xc, which punt straight to the TAP with * no policer consulted at all. - * - * REVISION HISTORY: the first implementation of this policer - * (`copp_punt_policer`, a standalone plugin) registered its - * classify+police node on `device-input`, running unconditionally on - * every packet on every physical interface. Review feedback - * (sonic-net/SONiC#2539, yue-fred-gao) correctly flagged that this - * pays a per-packet tax (measured ~50ns/pkt) on the ~100% of ordinary - * forwarded traffic that never matches, and asked whether the policer - * could instead run on the punt path itself. Confirmed by reading - * linux-cp/lcp_node.c: linux-cp-punt / linux-cp-punt-xc (the ARP/ - * LACP/LLDP/UDLD/TTL_ERROR path) and lcp_arp_phy_node all set - * VLIB_TX = the phy's paired TAP and dispatch straight to - * `interface-output`, rewinding the buffer back to an intact, - * unmodified Ethernet frame first -- so by the time ANY of these - * protocols reaches interface-output on the TAP, the frame layout is - * exactly what device-input classification was already parsing. This - * node moved the same classify+meter logic there: it now only ever - * sees traffic VPP has ALREADY decided is CPU-bound, not the 100% of - * ordinary transit traffic device-input classification paid a tax on - * regardless of match. - * - * Also per reviewer feedback (yue-fred-gao, sonic-net/SONiC#2539, - * 2026-09-14), this was folded into the existing sonic_ext plugin - * (rather than a new standalone plugin) to avoid growing the plugin - * count for closely related SONiC-on-VPP dataplane features. - * - * NOT covered here: BGPV6 / IPv6 ND. Those already worked via VPP's - * own ip6-unicast classify-policer arc before this project. - * - * Delivery: a conforming/unmatched packet simply continues the - * interface-output arc unchanged (falls through to TX) -- linux-cp - * already set VLIB_TX before this node runs, so there is no manual - * TAP-redirect step. Exceed/violate go to error-drop. - * - * Per-TAP feature binding is driven by the LCP pair add/del callback - * (see sonic_ext.c's sonic_ext_lcp_pair_add_cb/_del_cb), the same - * mechanism sonic-ext-aggr-tap-redirect already uses -- this survives - * `config reload` (unlike a manual CLI bind, which does not), since - * the callback re-fires for every LCP pair recreated during reload. */ #include @@ -177,13 +137,8 @@ sonic_ext_copp_ifout_x1 (vlib_main_t *vm, sonic_ext_main_t *sem, ethertype = clib_net_to_host_u16 (eth->type); *out_ethertype = ethertype; - /* Pre-resolved match wins over the byte-match loop below. Set only by - * sonic-ext-copp-udld, which reaches this packet via VPP's real LLC-null / - * LLC+SNAP+Cisco-UDLD-OUI dispatch -- genuine protocol identification. - * UDLD's wire bytes at this offset are an 802.3 *length* field, not an - * EtherType, and that length varies with the frame's actual TLV payload, - * so it cannot be matched here the way ARP/LACP/LLDP/TTL_ERROR's real - * EtherTypes are. See sonic_ext_buffer_opaque_t.copp_ifout_entry_idx. */ + /* Pre-resolved match wins over the byte-match loop below. + * Set by sonic-ext-copp-udld */ { sonic_ext_buffer_opaque_t *seb = sonic_ext_buffer (b); @@ -379,16 +334,10 @@ VLIB_REGISTER_NODE (sonic_ext_copp_ifout_node) = { }; /* - * Feature binding is per-TAP (the LCP host tap of every real phy -- + * Feature binding for every LCP host tap of every real phy -- * not aggregate/BVI/bond taps, which have no CoPP-punted ARP/LACP/ * LLDP/UDLD/TTL_ERROR traffic of their own; those protocols are - * always punted to the *member* phy's own tap, never the aggregate's). - * Driven from the LCP pair add/del callback in sonic_ext.c, exactly - * like sonic-ext-aggr-tap-redirect and sonic-ext-host-xc already are - * -- this is what makes the binding survive `config reload` (a plain - * per-run manual CLI bind would not: LCP pairs, and hence their - * taps, are recreated on every reload, but the callback re-fires for - * each one as it comes back). + * always punted to the member phy's own tap, never the aggregate's). */ void sonic_ext_copp_ifout_enable_disable (u32 sw_if_index, int enable) diff --git a/vppbld/plugins/sonic_ext/copp_ip2me_node.c b/vppbld/plugins/sonic_ext/copp_ip2me_node.c index 4e2a19ac..f50edbd4 100644 --- a/vppbld/plugins/sonic_ext/copp_ip2me_node.c +++ b/vppbld/plugins/sonic_ext/copp_ip2me_node.c @@ -15,39 +15,8 @@ * sonic-ext-copp-ip2me: CoPP enforcement for IP2ME/SNMP/SSH traffic -- * traffic destined to one of the router's own IPv4 addresses that VPP's * dataplane does not answer itself (see sonic-net/sonic-buildimage#25801, - * SONiC-on-VPP CoPP HLD). Folded into sonic_ext (formerly the standalone - * copp_ip2me_policer plugin) alongside sonic-ext-copp-ifout, per the - * same reviewer feedback (yue-fred-gao, sonic-net/SONiC#2539) to avoid - * growing the plugin count for closely related SONiC-on-VPP dataplane - * features. + * SONiC-on-VPP CoPP HLD). * - * BACKGROUND: VPP's built-in classify-based policer feature - * (ip4-policer-classify, on the ip4-unicast arc) only meters traffic on - * whichever interface it has been explicitly bound to. Since the - * classify table matches purely on destination IP, IP2ME traffic - * destined to router-interface A's address can legitimately arrive on - * router-interface B -- if B never had the feature bound, that traffic - * skips policing entirely and reaches VPP's ip4-punt-redirect mechanism - * completely unpoliced. Binding to every possible L3 ingress interface - * is the fix VPP's own classify feature requires, but it needs lazy, - * per-RIF VAPI calls (deferred to avoid syncd's SAI-call watchdog) and - * has proven fragile in practice. - * - * This node avoids the whole binding-scope problem by living on - * ip4-punt instead: a single, always-on, global feature arc that every - * packet reaching this point has already been routed through - * (ip4-lookup -> ip4-local -> ip4-punt), regardless of which interface - * it arrived on. No per-interface binding is needed -- ip4-punt is - * reached the same way no matter the ingress interface, once VPP's own - * dataplane has already concluded "nothing here handles this packet, it - * needs the host." That is exactly SAI's IP2ME semantics, so this - * node only needs to answer one question (is the destination address - * one we're tracking?) and apply the existing SAI-created policer - * object, then let the packet continue unchanged to ip4-punt-redirect - * (conform) or drop it (exceed/violate) -- it never needs to redirect - * to a TAP itself, unlike sonic-ext-copp-ifout's interface-output node, - * since ip4-punt-redirect already does that for every packet that - * reaches it. */ #include @@ -395,12 +364,7 @@ VLIB_REGISTER_NODE (sonic_ext_copp_ip2me_node) = { }; /* - * ip4-punt is a global feature arc, not per-interface -- enable this - * feature on it ONCE at init, unconditionally, the same way - * ip4-punt-redirect itself is always enabled. No per-interface - * enable/disable call is needed or ever made (unlike sonic-ext-copp- - * ifout, which has to do this per-TAP because interface-output is a - * per-interface arc; ip4-punt is not). + * ip4-punt is a global feature arc, not per-interface */ VNET_FEATURE_INIT (sonic_ext_copp_ip2me_feat, static) = { .arc_name = "ip4-punt", @@ -535,10 +499,7 @@ sonic_ext_copp_ip2me_bind (const char *policer_name, int is_bind) int sonic_ext_copp_ip2me_bind_bgp (const char *policer_name, int is_bind) { - /* BGP/BGPV6 both use TCP dst port 179 on the wire; matched - * independently of the address set (see sonic_ext_copp_ip2me_x1()) - * so BGP's own install/uninstall never disturbs IP2ME/SNMP/SSH's - * address-match slot, and vice versa. */ + /* BGP/BGPV6 both use TCP dst port 179 on the wire */ return sonic_ext_copp_ip2me_bind_slot (policer_name, is_bind, SONIC_EXT_COPP_IP2ME_MATCH_TCP_DPORT, 179); diff --git a/vppbld/plugins/sonic_ext/copp_udld_node.c b/vppbld/plugins/sonic_ext/copp_udld_node.c index 11666543..105e9dbb 100644 --- a/vppbld/plugins/sonic_ext/copp_udld_node.c +++ b/vppbld/plugins/sonic_ext/copp_udld_node.c @@ -14,21 +14,10 @@ * * sonic-ext-copp-udld * - * CoPP punt+policer path for UDLD -- a protocol sonic-ext-copp-ifout - * (see its own file header) CANNOT see, for a reason specific to - * UDLD alone among ARP/LACP/LLDP/UDLD/TTL_ERROR: UDLD is not - * Ethernet-II. Its 14th/15th wire bytes (0x0067 = 103) are BELOW - * 0x0600, so per 802.3 they are a *length* field, not an EtherType. + * CoPP punt+policer path for UDLD. UDLD is not Ethernet-II. * VPP's ethernet-input hard-codes this threshold * (eth_input_next_by_type(): "etype < 0x600 ? LLC : ...") and always - * routes such frames to `llc-input` -- there is no configuration - * knob to send a sub-0x600 frame down the normal EtherType-classify - * path sonic-ext-copp-ifout hooks. Confirmed live via `vppctl show - * trace` + `show error`: a UDLD frame sent to the DUT produces - * `llc-input: unknown llc ssap/dsap` and is dropped inside VPP core, - * before sonic-ext-copp-ifout's feature node on interface-output - * ever runs. - * + * routes such frames to `llc-input` * Two wire encodings of UDLD both dead-end the same way and both * need a registered handler here: * @@ -79,12 +68,6 @@ * because nothing ever reached it) does the actual policing, * counting, and conform/exceed/violate accounting, with no * duplicated logic here. - * - * If no LCP pair exists for the ingress phy (e.g. UDLD received on - * an interface VPP doesn't manage as an LCP pair), the packet is - * dropped -- there is no sane TAP to deliver it to, and silently - * falling through to interface-output on whatever left-over VLIB_TX - * happened to be set would misdeliver it. */ #include @@ -148,18 +131,6 @@ typedef enum SONIC_EXT_COPP_UDLD_N_NEXT, } sonic_ext_copp_udld_next_t; -/* Cached "interface-output" feature-arc index. sonic-ext-copp-ifout is a - * feature node on that arc; reaching it via a direct graph next-node jump - * (as this node does, from an LLC/SNAP dead end rather than the real - * interface-output dispatch) leaves b->current_config_index holding - * whatever stale value was set by the arc this packet last actually went - * through (e.g. device-input) -- sonic-ext-copp-ifout's own - * vnet_feature_next() call then reads that garbage and computes a bogus - * next-next index. vnet_feature_arc_start() below re-initializes it - * correctly for interface-output on the TAP we are about to redirect to, - * exactly as if the packet had entered this arc normally. Resolved lazily - * on first use since vnet_get_feature_arc_index() needs the feature - * subsystem to have finished its own init first. */ static u8 sonic_ext_copp_udld_ifout_arc_index = (u8) ~0; VLIB_NODE_FN (sonic_ext_copp_udld_node) @@ -183,21 +154,6 @@ VLIB_NODE_FN (sonic_ext_copp_udld_node) while (n_left_from > 0) { - /* On a LAG member port, VPP's bond plugin (vnet/bonding/node.c) - * rewrites sw_if_index[VLIB_RX] from the physical member to the - * bond aggregate for any ethertype it does not itself special-case - * (LACP/CDP/LLDP are let through unrewritten -- that is specifically - * why those protocols' CoPP punts land on the right member tap while - * UDLD, an ordinary ethertype from the bond plugin's point of view, - * does not). Before doing so it saves the true member interface into - * vnet_buffer2(b)->orig_rx_sw_if_index (left 0 -- never a valid - * sw_if_index -- when no rewrite happened). Prefer that saved value - * so a LAG-member UDLD punt resolves to the member's own LCP tap, - * not the bond's, matching LACP/LLDP's behavior. Confirmed live via - * `vppctl show trace`: without this, UDLD on a bond member landed on - * the bond's own tap (e.g. tap4134/be120) instead of the member's - * (e.g. tap4109/Ethernet48), so a listener on the member's tap never - * saw it even though CoPP policing itself was correct. */ u32 orig_rx0 = vnet_buffer2 (b[0])->orig_rx_sw_if_index; u32 rx0 = orig_rx0 ? orig_rx0 : vnet_buffer (b[0])->sw_if_index[VLIB_RX]; u32 tx0 = ~0; @@ -215,11 +171,8 @@ VLIB_NODE_FN (sonic_ext_copp_udld_node) { const lcp_itf_pair_t *lip = lcp_itf_pair_get (lipi); - /* Rewind past whatever llc-input (and, for the real-SNAP - * path, snap-input too) already consumed so - * sonic-ext-copp-ifout sees an intact Ethernet header, the - * same way sonic_ext_redirect_to_ingress_tap() restores - * to l2_hdr_offset for the aggregate-tap redirect path. */ + /* Rewind past whatever llc-input already consumed so + * sonic-ext-copp-ifout sees an intact Ethernet header */ i32 adv = (i32) vnet_buffer (b[0])->l2_hdr_offset - (i32) b[0]->current_data; if (adv) @@ -230,16 +183,7 @@ VLIB_NODE_FN (sonic_ext_copp_udld_node) /* Pre-resolve the copp-ifout entry for UDLD via the key SAI bound * it under (SwitchVppHostifTrap.cpp's SONIC_EXT_COPP_UDLD_ETHERTYPE), - * and tag the buffer with it. Real protocol dispatch got us here - * (LLC-null or LLC+SNAP+Cisco-UDLD-OUI), unlike copp-ifout's own - * fallback match, which reads a fixed wire-byte offset that for - * UDLD holds an 802.3 *length* field (varies with the frame's - * actual TLV payload) rather than a stable EtherType -- that byte - * match can never reliably identify UDLD in general. Leaving the - * tag unset (sonic-ext-capture's ~0 default) if the entry isn't - * bound yet is fine: copp-ifout's fallback path then finds nothing - * either and passes the packet through unpoliced, same as today - * before this trap is configured. */ + * and tag the buffer with it. */ { sonic_ext_main_t *sem = &sonic_ext_main; int ifout_idx = sonic_ext_copp_ifout_find_entry ( @@ -253,13 +197,7 @@ VLIB_NODE_FN (sonic_ext_copp_udld_node) } /* Re-initialize the interface-output feature-arc position for - * this buffer on the TAP we're redirecting to -- see comment - * on sonic_ext_copp_udld_ifout_arc_index above. Without this, - * sonic-ext-copp-ifout's vnet_feature_next() reads whatever - * stale current_config_index this buffer had from the arc it - * actually traversed (e.g. device-input), producing a bogus - * next node -- observed live: conforming UDLD packets landed - * in ip4-drop instead of ever reaching the TAP. */ + * this buffer on the TAP we're redirecting to */ { u32 dummy_next; vnet_feature_arc_start (sonic_ext_copp_udld_ifout_arc_index, tx0, @@ -319,50 +257,15 @@ VLIB_REGISTER_NODE (sonic_ext_copp_udld_node) = { }; /* - * Register with both LLC dead ends UDLD can arrive at -- see file - * header for why both are needed. Run from a VLIB_INIT_FUNCTION (not - * the plugin's main VLIB_PLUGIN_REGISTER-time init) so this runs - * once, after llc-input/snap-input's own node-graph init functions - * (llc_input_init / snap_input_init) have already run and registered - * the node graph edges these calls extend -- both - * llc_register_input_protocol() and snap_register_input_protocol() - * internally call vlib_call_init_function() on their respective - * *_input_init first if needed, so ordering here is self-managing. + * Register with both LLC dead ends UDLD can arrive at */ -/* sonic-mgmt's own PTF UDLDTest (copp/test_copp.py's construct_packet, via - * ptf.testutils.simple_eth_packet with no explicit payload) sends the - * minimal frame described in case 2 above, but its payload filler is NOT - * null bytes -- ptf.testutils.simple_eth_packet pads with the ASCII - * character '0' (pkt / ("0" * (pktlen - len(pkt))), confirmed in the - * vendored ptf/testutils.py). That puts LLC dsap=ssap=0x30 (ASCII '0'), not - * 0x00/LLC_PROTOCOL_null, on the wire for every packet this specific test - * tool generates. Confirmed live via `vppctl show trace`: with only - * LLC_PROTOCOL_null (0x00) registered, this exact traffic hits - * llc-input's own "unknown llc ssap/dsap" drop and never reaches this - * node at all. - * - * llc_register_input_protocol() cannot be called with 0x30 -- it looks up - * llc_protocol_info_t via llc_get_protocol_info(), which only resolves the - * ~20 SAP values VPP's own vnet/llc/llc.h foreach_llc_protocol table lists - * (0x30 is not one), and unconditionally dereferences a NULL result -- - * confirmed by crash: SIGSEGV in llc_register_input_protocol when called - * with 0x30. The actual dispatch llc-input's node reads at runtime is a - * much simpler public array, llc_main.input_next_by_protocol[256], indexed - * directly by the wire dsap byte (see vnet/llc/node.c); the crash-prone - * llc_get_protocol_info() bookkeeping exists only for named-protocol CLI - * introspection this plugin does not need. sonic_ext_copp_udld_register_llc_sap() - * below writes that array directly for 0x30, via the same vlib_node_add_next() - * primitive llc_register_input_protocol() itself uses, without touching the - * SAP-name hash table at all. */ static void sonic_ext_copp_udld_register_llc_sap (vlib_main_t *vm, u8 sap, u32 node_index) { llc_main_t *lm = &llc_main; u32 next_index; - /* Ensure llc-input's own init (which resets input_next_by_protocol[] to - * all-DROP) has already run, exactly as llc_register_input_protocol() - * itself guarantees before touching the table. */ + /* Ensure llc-input's own init has already run */ { clib_error_t *error = vlib_call_init_function (vm, llc_input_init); if (error) diff --git a/vppbld/plugins/sonic_ext/sonic_ext.c b/vppbld/plugins/sonic_ext/sonic_ext.c index e0a492fd..89cf0d3d 100644 --- a/vppbld/plugins/sonic_ext/sonic_ext.c +++ b/vppbld/plugins/sonic_ext/sonic_ext.c @@ -414,18 +414,6 @@ sonic_ext_lcp_pair_add_cb (lcp_itf_pair_t *lip) sonic_ext_host_xc_enable_disable (lip->lip_host_sw_if_index, 1); if (sonic_ext_phy_is_aggregate (lip->lip_phy_sw_if_index)) sonic_ext_aggr_tap_redirect_enable_disable (lip->lip_host_sw_if_index, 1); - /* sonic-ext-copp-ifout: bind ONLY on a direct/member phy's own host - * tap, never on an aggregate's (bond/BVI) own host tap. For a bond - * member, ARP/LACP/LLDP/UDLD/TTL_ERROR is first punted to the - * *aggregate's* tap (see sonic-ext-aggr-tap-redirect's header - * comment), which redirects it onward to the member's own tap and - * re-enters interface-output there. If this policer were also - * bound on the aggregate tap, that single physical packet would be - * policed twice (once per interface-output pass) against the same - * shared CIR budget -- confirmed live: policer hit-count ran ~3x - * the actual packets sent, starving real delivery. Binding only on - * non-aggregate taps polices each packet exactly once, on its - * final (member) tap. */ if (!sonic_ext_phy_is_aggregate (lip->lip_phy_sw_if_index)) sonic_ext_copp_ifout_enable_disable (lip->lip_host_sw_if_index, 1); } @@ -447,14 +435,6 @@ sonic_ext_lcp_pair_del_cb (lcp_itf_pair_t *lip) sonic_ext_copp_ifout_enable_disable (lip->lip_host_sw_if_index, 0); } - -/* - * lcp_itf_pair_walk callback: enable sonic-ext-copp-ifout on this - * pair's host tap. Used at plugin-init time to catch pairs created - * before the plugin's own VLIB_INIT_FUNCTION ran (shouldn't normally - * happen given plugin init ordering, but mirrors the same - * belt-and-suspenders pattern host-xc/aggr-tap-redirect already use). - */ static walk_rc_t sonic_ext_copp_ifout_walk_enable_cb (index_t lipi, void *ctx) { @@ -484,33 +464,10 @@ sonic_ext_init (vlib_main_t *vm) * at runtime. */ sonic_ext_set_punt_via_member (1); sonic_ext_set_host_xc (1); - - /* sonic-ext-copp-ifout has no on/off toggle -- it always binds, and - * relies on its own (initially empty) bind table to no-op until - * something is actually bound via sonic_ext_copp_ifout_bind(). Walk - * any pre-existing pairs for the same belt-and-suspenders reason as - * above; going forward the LCP pair add/del callback keeps it in - * sync, including across `config reload`. */ lcp_itf_pair_walk (sonic_ext_copp_ifout_walk_enable_cb, NULL); - - /* sonic-ext-copp-ip2me is a single global feature on the ip4-punt - * arc (dispatched with sw_if_index 0, same as glean-redirect on - * ip4-drop/ip6-drop above) -- enable it once, unconditionally, at - * init. No per-interface binding is needed or ever done: the node - * self-scopes via its own (initially empty) tracked-address table - * and no-ops until addresses are added via - * sonic_ext_copp_ip2me_addr_add_del() / bound via - * sonic_ext_copp_ip2me_bind(). Without this call the node is only - * registered in the ip4-punt arc's graph (which is why `show - * sonic-ext copp-ip2me` reports it as bound with addresses) but - * never actually visited by any packet, silently letting all IP2ME/ - * SNMP/SSH/BGP traffic through completely unpoliced. */ - vnet_feature_enable_disable ("ip4-punt", "sonic-ext-copp-ip2me", 0, 1, 0, - 0); + vnet_feature_enable_disable ("ip4-punt", "sonic-ext-copp-ip2me", 0, 1, 0, 0); return 0; } VLIB_INIT_FUNCTION (sonic_ext_init); - - diff --git a/vppbld/plugins/sonic_ext/sonic_ext.h b/vppbld/plugins/sonic_ext/sonic_ext.h index a6ecc2e4..fe191dfa 100644 --- a/vppbld/plugins/sonic_ext/sonic_ext.h +++ b/vppbld/plugins/sonic_ext/sonic_ext.h @@ -80,29 +80,12 @@ * VLAN before re-entering interface-output on the member tap, so * Linux observes the same wire frame on the right netdev. */ -/* - * copp_ifout_entry_idx: index into sonic_ext_main_t.copp_ifout_entries[], - * pre-resolved by a producer node that has already identified the packet's - * CoPP class through real protocol dispatch (not a positional byte read). - * ~0 means "no pre-resolved entry -- fall back to the ethertype/length-byte - * match". Set by sonic-ext-copp-udld: UDLD is not Ethernet-II (its 14th/15th - * wire bytes are an 802.3 *length* field, not an EtherType, and that length - * varies with the real TLV payload a UDLD frame carries), so matching it by - * byte value in sonic-ext-copp-ifout the way ARP/LACP/LLDP/TTL_ERROR's real - * EtherTypes are matched cannot work in general -- only a specific test - * packet of exactly one length would ever match. sonic-ext-copp-udld reaches - * this packet via VPP's real LLC-null / LLC+SNAP+Cisco-UDLD-OUI dispatch - * (genuine protocol identification, not a length guess), so it looks up the - * right copp_ifout_entries[] slot itself and tags it here; sonic-ext-copp- - * ifout then polices using the tag directly instead of re-deriving (and - * getting wrong) a match from wire bytes. - */ typedef struct { u32 magic; u32 orig_rx_sw_if_index; u32 orig_vlan_tag; - u32 copp_ifout_entry_idx; + u32 copp_ifout_entry_idx; /* index into sonic_ext_main_t.copp_ifout_entries[] */ } sonic_ext_buffer_opaque_t; STATIC_ASSERT (sizeof (sonic_ext_buffer_opaque_t) <= @@ -117,8 +100,7 @@ sonic_ext_buffer (vlib_buffer_t *b) /* * sonic-ext-copp-ifout: interface-output CoPP policer for - * ARP/LACP/LLDP/UDLD/TTL_ERROR. See copp_ifout_node.c for the full - * design rationale (why interface-output, not device-input). + * ARP/LACP/LLDP/UDLD/TTL_ERROR. */ #define SONIC_EXT_COPP_IFOUT_MAX_ENTRIES 16 #define SONIC_EXT_COPP_IFOUT_NAME_LEN 64 @@ -132,12 +114,6 @@ typedef struct u8 match_ip4_ttl_expiring; /* TTL_ERROR trap */ } sonic_ext_copp_ifout_entry_t; -/* - * sonic-ext-copp-ip2me: IPv4 destination addresses considered "IP2ME" - * (traffic destined to one of the router's own addresses reaching - * ip4-punt) plus the shared policer binding for IP2ME/SNMP/SSH. See - * copp_ip2me_node.c for the full design rationale. - */ #define SONIC_EXT_COPP_IP2ME_MAX_ADDRS 256 typedef struct @@ -147,20 +123,8 @@ typedef struct } sonic_ext_copp_ip2me_addr_t; /* - * sonic-ext-copp-ip2me now supports more than one independently - * bound policer on the ip4-punt arc, keyed by SAI trap group (one - * VPP policer object per trap group, named "copp-policer-0x"). - * IP2ME/SNMP/SSH share a single SAI trap type/policer and use table - * slot kind ADDR (matched by destination IPv4 address, the - * pre-existing behavior). BGP/BGPV6 are a genuinely distinct SAI - * trap type/trap-group/policer and are matched by TCP destination - * port 179 instead -- BGP has no address set of its own (it rides on - * whichever router-interface IP the peer dials, same as IP2ME - * addresses, but keyed by port rather than address so its own - * install/uninstall never disturbs IP2ME/SNMP/SSH's binding or vice - * versa). Each slot is independently bound/unbound so disabling one - * SAI trap only ever removes its own slot -- see - * sonic_ext_copp_ip2me_bind_keyed() in copp_ip2me_node.c. + * sonic-ext-copp-ip2me supports more than one independently bound + * policer on the ip4-punt arc, keyed by SAI trap group */ #define SONIC_EXT_COPP_IP2ME_MAX_POLICERS 8 @@ -213,9 +177,6 @@ typedef struct u64 copp_ifout_exceed_packets[SONIC_EXT_COPP_IFOUT_MAX_ENTRIES]; u64 copp_ifout_violate_packets[SONIC_EXT_COPP_IFOUT_MAX_ENTRIES]; - /* sonic-ext-copp-ip2me: IP2ME/SNMP/SSH/BGP/BGPV6 policing on - * ip4-punt, keyed per SAI trap group -- see - * sonic_ext_copp_ip2me_policer_t above. */ sonic_ext_copp_ip2me_addr_t copp_ip2me_addrs[SONIC_EXT_COPP_IP2ME_MAX_ADDRS]; u32 copp_ip2me_n_addrs; sonic_ext_copp_ip2me_policer_t copp_ip2me_policers[SONIC_EXT_COPP_IP2ME_MAX_POLICERS]; @@ -293,43 +254,22 @@ int sonic_ext_phy_is_bvi (u32 phy_sw_if_index); int sonic_ext_phy_is_bond (u32 phy_sw_if_index); /* - * sonic-ext-copp-ifout: enable/disable the interface-output CoPP - * policer feature on a given TAP sw_if_index. Driven from the LCP - * pair add/del callback -- see sonic_ext.c. + * enable/disable the interface-output CoPP policer feature on a given + * TAP sw_if_index. */ void sonic_ext_copp_ifout_enable_disable (u32 sw_if_index, int enable); /* - * sonic-ext-copp-ifout: look up an entry by wire ethertype/length value. - * Exposed so sonic-ext-copp-udld can pre-resolve UDLD's entry via real - * protocol dispatch (LLC-null / LLC+SNAP+Cisco-UDLD) and tag the buffer - * with the result -- see sonic_ext_buffer_opaque_t.copp_ifout_entry_idx. + * look up an entry by wire ethertype/length value. * Returns -1 if no matching in-use entry exists. */ int sonic_ext_copp_ifout_find_entry (sonic_ext_main_t *sem, u16 ethertype); /* - * sonic-ext-copp-ifout: bind (or unbind) an ethertype -> policer-name - * entry consulted by the interface-output node above. See - * copp_ifout_node.c for the full semantics (mirrors the predecessor - * copp_punt_policer plugin's copp_punt_policer_bind()). + * bind (or unbind) an ethertype -> policer-name */ int sonic_ext_copp_ifout_bind (u16 ethertype, const char *policer_name, int is_bind, int match_ip4_ttl_expiring); - -/* - * sonic-ext-copp-ip2me: add/remove an IP2ME address (used by - * IP2ME/SNMP/SSH's address-match slot only -- BGP/BGPV6 do not use - * an address set, see sonic_ext_copp_ip2me_match_kind_t above), and - * bind/unbind a policer for a specific SAI trap group's traffic - * class. sonic_ext_copp_ip2me_bind() is the legacy shared - * IP2ME/SNMP/SSH address-match slot; sonic_ext_copp_ip2me_bind_bgp() - * binds/unbinds a SEPARATE slot matched by TCP dst port 179, so BGP's - * own install/uninstall never disturbs IP2ME/SNMP/SSH's binding (and - * vice versa) -- each SAI trap's classify call must pass its own - * trap group's unique "copp-policer-0x" policer_name. See - * copp_ip2me_node.c. - */ int sonic_ext_copp_ip2me_addr_add_del (u32 addr, int is_add); int sonic_ext_copp_ip2me_bind (const char *policer_name, int is_bind); int sonic_ext_copp_ip2me_bind_bgp (const char *policer_name, int is_bind); diff --git a/vppbld/plugins/sonic_ext/sonic_ext_api.c b/vppbld/plugins/sonic_ext/sonic_ext_api.c index 8dd89705..9a26a327 100644 --- a/vppbld/plugins/sonic_ext/sonic_ext_api.c +++ b/vppbld/plugins/sonic_ext/sonic_ext_api.c @@ -158,11 +158,6 @@ vl_api_sonic_ext_copp_ip2me_get_counters_t_handler ( int rv = 0; u64 conform = 0, exceed = 0, violate = 0; - /* Aggregate across every bound policer slot (legacy address-match - * IP2ME/SNMP/SSH slot plus any TCP-dst-port slots such as BGP/ - * BGPV6) -- this API predates the multi-slot design and callers - * only ever used it for the single shared IP2ME counter, so summing - * preserves that behavior for anyone still calling it that way. */ for (u32 i = 0; i < sem->copp_ip2me_n_policers; i++) { if (!sem->copp_ip2me_policers[i].in_use) @@ -183,7 +178,6 @@ vl_api_sonic_ext_copp_ip2me_get_counters_t_handler ( /* API definitions */ #include - static clib_error_t * sonic_ext_api_init (vlib_main_t *vm) { From e490e6628e199cf51931f3819c0b0aa79c0cdb97 Mon Sep 17 00:00:00 2001 From: Nikhil Hegde Date: Sat, 19 Sep 2026 03:14:47 +0000 Subject: [PATCH 7/7] vppbld/sonic_ext: fix copp_udld_node build against vnet_buffer_opaque2_t copp_udld_node.c read orig_rx_sw_if_index via vnet_buffer2(b), a core-VPP field added by the local patch 0008-bond-drop-stats-track-original- member-interface.patch. That patch was removed by upstream commit 2b71bbd (#279, 'New patchless design to support LAG and BVI'), which replaced it with a sonic_ext-local mechanism: capture_node.c stashes the pre-bond-rewrite RX interface into sonic_ext_buffer_opaque_t (inside vnet_buffer2(b)->unused, accessed via sonic_ext_buffer()) instead of patching vnet_buffer_opaque2_t itself. Since 0008 no longer applies orig_rx_sw_if_index onto vnet_buffer_opaque2_t, this line failed to compile: src/plugins/sonic_ext/copp_udld_node.c:201:43: error: no member named 'orig_rx_sw_if_index' in 'vnet_buffer_opaque2_t' Switch to sonic_ext_buffer(b)->orig_rx_sw_if_index, matching the pattern already used by glean_redirect_node.c, aggr_tap_redirect_node.c, and drop_member_stats_node.c -- all of which get the same pre-bond-rewrite member interface from sonic-ext-capture's cookie rather than a core-VPP field. Guard with the magic-cookie check those nodes also use, since the capture node's stash is only trustworthy when the cookie matches. Verified: src/plugins/sonic_ext/copp_udld_node.c now compiles clean (cc -fsyntax-only) against the pinned VPP tree at this commit. Signed-off-by: Nikhil Hegde --- vppbld/plugins/sonic_ext/copp_udld_node.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vppbld/plugins/sonic_ext/copp_udld_node.c b/vppbld/plugins/sonic_ext/copp_udld_node.c index 105e9dbb..f63fc044 100644 --- a/vppbld/plugins/sonic_ext/copp_udld_node.c +++ b/vppbld/plugins/sonic_ext/copp_udld_node.c @@ -154,7 +154,9 @@ VLIB_NODE_FN (sonic_ext_copp_udld_node) while (n_left_from > 0) { - u32 orig_rx0 = vnet_buffer2 (b[0])->orig_rx_sw_if_index; + sonic_ext_buffer_opaque_t *seb0 = sonic_ext_buffer (b[0]); + u32 orig_rx0 = (seb0->magic == SONIC_EXT_BUFFER_MAGIC) ? + seb0->orig_rx_sw_if_index : 0; u32 rx0 = orig_rx0 ? orig_rx0 : vnet_buffer (b[0])->sw_if_index[VLIB_RX]; u32 tx0 = ~0; u32 phy_sw = rx0;