diff --git a/docker-syncd-vpp/conf/startup.conf.tmpl b/docker-syncd-vpp/conf/startup.conf.tmpl index 40a6a53..a2143bc 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 } ## 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 739c82c..2c2dbd7 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/sonic_ext/CMakeLists.txt b/vppbld/plugins/sonic_ext/CMakeLists.txt index 76191b7..87c1635 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 ccf3480..6f42300 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 18d6e05..19f1cb0 100644 --- a/vppbld/plugins/sonic_ext/capture_node.c +++ b/vppbld/plugins/sonic_ext/capture_node.c @@ -112,6 +112,9 @@ VLIB_NODE_FN (sonic_ext_capture_node) } seb->magic = SONIC_EXT_BUFFER_MAGIC; + + /* sonic-ext-copp-udld sets this to a real index for UDLD traffic */ + 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 4f89a27..8490c7b 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 0000000..219b1fe --- /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 0000000..4e2a19a --- /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 0000000..1166654 --- /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 476bac9..ad33a3e 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 63d9305..e0a492f 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 b21e096..a6ecc2e 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 997be95..8dd8970 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) {