diff --git a/api/gr_api.h b/api/gr_api.h index 6dd6246e9..96ab7a9ca 100644 --- a/api/gr_api.h +++ b/api/gr_api.h @@ -11,7 +11,7 @@ #include // Must be bumped when making non-backward compatible changes in API headers -#define GR_API_VERSION 6 +#define GR_API_VERSION 8 // API request header. struct gr_api_request { diff --git a/modules/infra/api/gr_infra.h b/modules/infra/api/gr_infra.h index af3b637c8..12adf8dd5 100644 --- a/modules/infra/api/gr_infra.h +++ b/modules/infra/api/gr_infra.h @@ -34,6 +34,8 @@ typedef enum : uint16_t { GR_IFACE_F_PACKET_TRACE = GR_BIT16(2), GR_IFACE_F_SNAT_STATIC = GR_BIT16(3), GR_IFACE_F_SNAT_DYNAMIC = GR_BIT16(4), + GR_IFACE_F_PING_IGNORE = GR_BIT16(5), + GR_IFACE_F_ERR_IGNORE = GR_BIT16(6), } gr_iface_flags_t; // Interface state flags. @@ -445,14 +447,30 @@ struct gr_graph_dump_resp { GR_REQ(GR_GRAPH_DUMP, struct gr_graph_dump_req, struct gr_graph_dump_resp); +typedef enum : uint16_t { + GR_GRAPH_SET_RX_BURST = GR_BIT16(0), + GR_GRAPH_SET_VECTOR = GR_BIT16(1), + GR_GRAPH_SET_ICMP_ERROR = GR_BIT16(2), + GR_GRAPH_SET_ARP = GR_BIT16(3), + GR_GRAPH_SET_ICMP = GR_BIT16(4), +} gr_graph_conf_set_attr_t; + struct gr_graph_conf { uint16_t rx_burst_max; // default 64, max 256 uint16_t vector_max; // default 64, max 256 + uint16_t icmp_error_rate; // ICMP errors/sec per node per worker, 0 = no limit, default 1000 + uint16_t arp_rate; // ARP packets/sec per worker, 0 = no limit, default 1000 + uint16_t icmp_rate; // ICMP/ICMPv6 input packets/sec per worker, 0 = no limit, default 1000 }; GR_REQ(GR_GRAPH_CONF_GET, struct gr_empty, struct gr_graph_conf); -GR_REQ(GR_GRAPH_CONF_SET, struct gr_graph_conf, struct gr_empty); +struct gr_graph_conf_set_req { + struct gr_graph_conf; + gr_graph_conf_set_attr_t set_attrs; +}; + +GR_REQ(GR_GRAPH_CONF_SET, struct gr_graph_conf_set_req, struct gr_empty); // packet tracing ////////////////////////////////////////////////////////////// diff --git a/modules/infra/cli/cli_iface.h b/modules/infra/cli/cli_iface.h index 4bb12d677..7c1645ad7 100644 --- a/modules/infra/cli/cli_iface.h +++ b/modules/infra/cli/cli_iface.h @@ -74,14 +74,17 @@ int arg_iface( CLI_CONTEXT(root, INTERFACE_ARG, CTX_ARG("set", "Modify an existing interface.")) #define IFACE_ATTRS_CMD \ - "(up|down),(promisc PROMISC),(mtu MTU),((vrf VRF)|(domain DOMAIN)),(description DESCR)" + "(up|down),(promisc PROMISC),(ping-ignore PING_IGNORE),(err-ignore ERR_IGNORE)," \ + "(mtu MTU),((vrf VRF)|(domain DOMAIN)),(description DESCR)" + +#define IFACE_FLAG_ARG(placeholder, desc) \ + with_help(desc, EC_NODE_OR(placeholder, ec_node_str(desc, "on"), ec_node_str(desc, "off"))) #define IFACE_ATTRS_ARGS \ with_help("Set the interface UP.", ec_node_str("up", "up")), \ - with_help( \ - "Enable/disable promiscuous mode.", \ - EC_NODE_OR("PROMISC", ec_node_str("", "on"), ec_node_str("", "off")) \ - ), \ + IFACE_FLAG_ARG("PROMISC", "Enable/disable promiscuous mode."), \ + IFACE_FLAG_ARG("PING_IGNORE", "Ignore ICMP echo requests."), \ + IFACE_FLAG_ARG("ERR_IGNORE", "Omit sending ICMP errors to their originating IP."), \ with_help("Set the interface DOWN.", ec_node_str("down", "down")), \ with_help( \ "Maximum transmission unit size.", \ diff --git a/modules/infra/cli/graph.c b/modules/infra/cli/graph.c index 3d063a85b..c9ccfd817 100644 --- a/modules/infra/cli/graph.c +++ b/modules/infra/cli/graph.c @@ -13,11 +13,31 @@ #include static cmd_status_t graph_conf_set(struct gr_api_client *c, const struct ec_pnode *p) { - struct gr_graph_conf req = {0}; + struct gr_graph_conf_set_req req = {.set_attrs = 0}; - if (arg_u16(p, "VECTOR", &req.vector_max) < 0 && errno != ENOENT) + if (arg_u16(p, "VECTOR", &req.vector_max) == 0) + req.set_attrs |= GR_GRAPH_SET_VECTOR; + else if (errno != ENOENT) return CMD_ERROR; - if (arg_u16(p, "BURST", &req.rx_burst_max) < 0 && errno != ENOENT) + + if (arg_u16(p, "BURST", &req.rx_burst_max) == 0) + req.set_attrs |= GR_GRAPH_SET_RX_BURST; + else if (errno != ENOENT) + return CMD_ERROR; + + if (arg_u16(p, "ICMP_ERROR_RATE", &req.icmp_error_rate) == 0) + req.set_attrs |= GR_GRAPH_SET_ICMP_ERROR; + else if (errno != ENOENT) + return CMD_ERROR; + + if (arg_u16(p, "ARP_RATE", &req.arp_rate) == 0) + req.set_attrs |= GR_GRAPH_SET_ARP; + else if (errno != ENOENT) + return CMD_ERROR; + + if (arg_u16(p, "ICMP_RATE", &req.icmp_rate) == 0) + req.set_attrs |= GR_GRAPH_SET_ICMP; + else if (errno != ENOENT) return CMD_ERROR; if (gr_api_client_send_recv(c, GR_GRAPH_CONF_SET, sizeof(req), &req, NULL) < 0) @@ -38,6 +58,9 @@ static cmd_status_t graph_conf_show(struct gr_api_client *c, const struct ec_pno struct gr_object *o = gr_object_new(NULL); gr_object_field(o, "vector_max", GR_DISP_INT, "%u", sizes->vector_max); gr_object_field(o, "rx_burst_max", GR_DISP_INT, "%u", sizes->rx_burst_max); + gr_object_field(o, "icmp_error_rate", GR_DISP_INT, "%u", sizes->icmp_error_rate); + gr_object_field(o, "arp_rate", GR_DISP_INT, "%u", sizes->arp_rate); + gr_object_field(o, "icmp_rate", GR_DISP_INT, "%u", sizes->icmp_rate); gr_object_free(o); free(resp_ptr); @@ -76,14 +99,27 @@ static int ctx_init(struct ec_node *root) { ret = CLI_COMMAND( CONF_CTX(root), - "set (vector-max VECTOR),(rx-burst-max BURST)", + "set (vector-max VECTOR),(rx-burst-max BURST),(icmp-error-rate ICMP_ERROR_RATE)," + "(arp-rate ARP_RATE),(icmp-rate ICMP_RATE)", graph_conf_set, - "Configure maximum burst sizes of the packet processing graph.", + "Configure packet processing graph parameters.", with_help( "Maximum size of graph vectors.", ec_node_uint("VECTOR", 1, UINT16_MAX, 10) ), with_help( "Maximum size of RX queue burst.", ec_node_uint("BURST", 1, UINT16_MAX, 10) + ), + with_help( + "ICMP errors/sec per node per worker (0 = no limit).", + ec_node_uint("ICMP_ERROR_RATE", 0, UINT16_MAX, 10) + ), + with_help( + "ARP packets/sec per worker (0 = no limit).", + ec_node_uint("ARP_RATE", 0, UINT16_MAX, 10) + ), + with_help( + "ICMP/ICMPv6 input packets/sec per worker (0 = no limit).", + ec_node_uint("ICMP_RATE", 0, UINT16_MAX, 10) ) ); if (ret < 0) diff --git a/modules/infra/cli/iface.c b/modules/infra/cli/iface.c index 09c0e00eb..fe3222873 100644 --- a/modules/infra/cli/iface.c +++ b/modules/infra/cli/iface.c @@ -213,12 +213,33 @@ static ssize_t iface_flags_format(char *buf, size_t len, const struct gr_iface * SAFE_BUF(snprintf, len, " tracing"); if (iface->flags & (GR_IFACE_F_SNAT_STATIC | GR_IFACE_F_SNAT_DYNAMIC)) SAFE_BUF(snprintf, len, " snat"); + if (iface->flags & GR_IFACE_F_PING_IGNORE) + SAFE_BUF(snprintf, len, " ping-ignore"); + if (iface->flags & GR_IFACE_F_ERR_IGNORE) + SAFE_BUF(snprintf, len, " err-ignore"); return n; err: return -1; } +static uint64_t on_off_flag( + struct gr_iface *iface, + const struct ec_pnode *p, + const char *placeholder, + gr_iface_flags_t flag +) { + const char *value = arg_str(p, placeholder); + if (value != NULL && strcmp(value, "on") == 0) { + iface->flags |= flag; + return GR_IFACE_SET_FLAGS; + } else if (value != NULL && strcmp(value, "off") == 0) { + iface->flags &= ~flag; + return GR_IFACE_SET_FLAGS; + } + return 0; +} + uint64_t parse_iface_args( struct gr_api_client *c, const struct ec_pnode *p, @@ -226,8 +247,8 @@ uint64_t parse_iface_args( size_t info_size, bool update ) { - const char *name, *promisc; uint64_t set_attrs = 0; + const char *name; name = arg_str(p, "NAME"); if (update) { @@ -252,14 +273,10 @@ uint64_t parse_iface_args( iface->flags &= ~GR_IFACE_F_UP; set_attrs |= GR_IFACE_SET_FLAGS; } - promisc = arg_str(p, "PROMISC"); - if (promisc != NULL && strcmp(promisc, "on") == 0) { - iface->flags |= GR_IFACE_F_PROMISC; - set_attrs |= GR_IFACE_SET_FLAGS; - } else if (promisc != NULL && strcmp(promisc, "off") == 0) { - iface->flags &= ~GR_IFACE_F_PROMISC; - set_attrs |= GR_IFACE_SET_FLAGS; - } + + set_attrs |= on_off_flag(iface, p, "PROMISC", GR_IFACE_F_PROMISC); + set_attrs |= on_off_flag(iface, p, "PING_IGNORE", GR_IFACE_F_PING_IGNORE); + set_attrs |= on_off_flag(iface, p, "ERR_IGNORE", GR_IFACE_F_ERR_IGNORE); if (arg_u16(p, "MTU", &iface->mtu) == 0) set_attrs |= GR_IFACE_SET_MTU; diff --git a/modules/infra/control/graph.c b/modules/infra/control/graph.c index dde33018b..28b8631e0 100644 --- a/modules/infra/control/graph.c +++ b/modules/infra/control/graph.c @@ -85,9 +85,12 @@ static struct iface_info_port *find_port(vec struct iface_info_port **ports, uin return port; } -static struct gr_graph_conf graph_conf = { +struct gr_graph_conf graph_conf = { .rx_burst_max = 64, .vector_max = 64, + .icmp_error_rate = 0, + .arp_rate = 0, + .icmp_rate = 0, }; static int @@ -610,19 +613,32 @@ static struct api_out graph_conf_get(const void * /*request*/, struct api_ctx *) } static struct api_out graph_conf_set(const void *request, struct api_ctx *) { - const struct gr_graph_conf *req = request; - struct gr_graph_conf prev = graph_conf; + const struct gr_graph_conf_set_req *req = request; vec struct iface_info_port **ports = NULL; + struct gr_graph_conf prev = graph_conf; struct iface *iface = NULL; int ret; - if (req->rx_burst_max > RTE_GRAPH_BURST_SIZE || req->vector_max > RTE_GRAPH_BURST_SIZE) - return api_out(EOVERFLOW, 0, NULL); - - if (req->rx_burst_max > 0) + if (req->set_attrs & GR_GRAPH_SET_RX_BURST) { + if (req->rx_burst_max == 0) + return api_out(EDOM, 0, NULL); + if (req->rx_burst_max > RTE_GRAPH_BURST_SIZE) + return api_out(EOVERFLOW, 0, NULL); graph_conf.rx_burst_max = req->rx_burst_max; - if (req->vector_max > 0) + } + if (req->set_attrs & GR_GRAPH_SET_VECTOR) { + if (req->vector_max == 0) + return api_out(EDOM, 0, NULL); + if (req->vector_max > RTE_GRAPH_BURST_SIZE) + return api_out(EOVERFLOW, 0, NULL); graph_conf.vector_max = req->vector_max; + } + if (req->set_attrs & GR_GRAPH_SET_ICMP_ERROR) + graph_conf.icmp_error_rate = req->icmp_error_rate; + if (req->set_attrs & GR_GRAPH_SET_ARP) + graph_conf.arp_rate = req->arp_rate; + if (req->set_attrs & GR_GRAPH_SET_ICMP) + graph_conf.icmp_rate = req->icmp_rate; if (graph_conf.rx_burst_max == prev.rx_burst_max && graph_conf.vector_max == prev.vector_max) diff --git a/modules/infra/control/graph.h b/modules/infra/control/graph.h index c7a79f529..f6c1a5be8 100644 --- a/modules/infra/control/graph.h +++ b/modules/infra/control/graph.h @@ -37,6 +37,8 @@ rte_node_enqueue_x1(struct rte_graph *, struct rte_node *, rte_edge_t next, void rte_edge_t gr_node_attach_parent(const char *parent, const char *node); +extern struct gr_graph_conf graph_conf; + uint16_t drop_packets(struct rte_graph *, struct rte_node *, void **, uint16_t); int drop_format(char *buf, size_t buf_len, const void *data, size_t data_len); diff --git a/modules/infra/control/iface.c b/modules/infra/control/iface.c index 827da2ed2..a1b9f1650 100644 --- a/modules/infra/control/iface.c +++ b/modules/infra/control/iface.c @@ -388,6 +388,16 @@ int iface_reconfig( } if ((ret = iface_set_up_down(iface, conf->flags & GR_IFACE_F_UP)) < 0) goto err; + + if (conf->flags & GR_IFACE_F_PING_IGNORE) + iface->flags |= GR_IFACE_F_PING_IGNORE; + else + iface->flags &= ~GR_IFACE_F_PING_IGNORE; + + if (conf->flags & GR_IFACE_F_ERR_IGNORE) + iface->flags |= GR_IFACE_F_ERR_IGNORE; + else + iface->flags &= ~GR_IFACE_F_ERR_IGNORE; } if (set_attrs & GR_IFACE_SET_VRF) { diff --git a/modules/infra/datapath/datapath.h b/modules/infra/datapath/datapath.h index 2c1e58e5f..2738a6f75 100644 --- a/modules/infra/datapath/datapath.h +++ b/modules/infra/datapath/datapath.h @@ -3,4 +3,37 @@ #pragma once +#include + +#include + void *gr_datapath_loop(void *priv); + +struct rate_limit_ctx { + uint16_t tokens; + uint64_t last_refill; +} __attribute__((packed)); + +static inline bool +rate_limited(struct rate_limit_ctx *ctx, const uint16_t max_rate, const uint16_t nb_pkts) { + uint64_t now = rte_rdtsc(); + uint16_t add; + + assert(ctx != NULL); + + if (max_rate == 0) + return false; + + add = (now - ctx->last_refill) * max_rate / rte_get_tsc_hz(); + if (add > 0) { + ctx->tokens = RTE_MIN((uint32_t)ctx->tokens + add, (uint32_t)max_rate); + ctx->last_refill = now; + } + + if (ctx->tokens == 0) + return true; + + ctx->tokens -= RTE_MIN(ctx->tokens, nb_pkts); + + return false; +} diff --git a/modules/ip/datapath/arp_input.c b/modules/ip/datapath/arp_input.c index 2efbb31df..a701cbb43 100644 --- a/modules/ip/datapath/arp_input.c +++ b/modules/ip/datapath/arp_input.c @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-3-Clause // Copyright (c) 2024 Robin Jarry +#include "datapath.h" #include "eth.h" #include "graph.h" #include "mbuf.h" @@ -9,20 +10,29 @@ #include #include +GR_NODE_CTX_TYPE(arp_input_ctx, { struct rate_limit_ctx limit; }); + enum { OP_REQUEST = 0, OP_REPLY, OP_UNSUPP, PROTO_UNSUPP, + RATE_LIMITED, EDGE_COUNT, }; static uint16_t arp_input_process(struct rte_graph *graph, struct rte_node *node, void **objs, uint16_t nb_objs) { + struct arp_input_ctx *ctx = arp_input_ctx(node); struct rte_arp_hdr *arp, *t; struct rte_mbuf *mbuf; rte_edge_t edge; + if (rate_limited(&ctx->limit, graph_conf.arp_rate, nb_objs)) { + rte_node_next_stream_move(graph, node, RATE_LIMITED); + return nb_objs; + } + for (uint16_t i = 0; i < nb_objs; i++) { mbuf = objs[i]; @@ -58,6 +68,13 @@ arp_input_process(struct rte_graph *graph, struct rte_node *node, void **objs, u return nb_objs; } +static int arp_input_init(const struct rte_graph *, struct rte_node *node) { + struct arp_input_ctx *ctx = arp_input_ctx(node); + ctx->limit.tokens = graph_conf.arp_rate; + ctx->limit.last_refill = rte_rdtsc(); + return 0; +} + static void arp_input_register(void) { gr_eth_input_add_type(RTE_BE16(RTE_ETHER_TYPE_ARP), "arp_input"); } @@ -66,6 +83,7 @@ static struct rte_node_register node = { .name = "arp_input", .process = arp_input_process, + .init = arp_input_init, .nb_edges = EDGE_COUNT, .next_nodes = { @@ -73,6 +91,7 @@ static struct rte_node_register node = { [OP_REPLY] = "arp_input_reply", [OP_UNSUPP] = "arp_input_op_unsupp", [PROTO_UNSUPP] = "arp_input_proto_unsupp", + [RATE_LIMITED] = "error_rate_limited", }, }; diff --git a/modules/ip/datapath/icmp_input.c b/modules/ip/datapath/icmp_input.c index b0426246c..c8521650b 100644 --- a/modules/ip/datapath/icmp_input.c +++ b/modules/ip/datapath/icmp_input.c @@ -2,6 +2,7 @@ // Copyright (c) 2024 Robin Jarry #include "control_output.h" +#include "datapath.h" #include "graph.h" #include "ip4_datapath.h" #include "log.h" @@ -12,11 +13,15 @@ #include +GR_NODE_CTX_TYPE(icmp_input_ctx, { struct rate_limit_ctx limit; }); + enum { OUTPUT = 0, CONTROL, INVALID, UNSUPPORTED, + RATE_LIMITED, + IGNORED, EDGE_COUNT, }; @@ -26,13 +31,20 @@ static control_queue_cb_t icmp_cb[UINT8_MAX]; static uint16_t icmp_input_process(struct rte_graph *graph, struct rte_node *node, void **objs, uint16_t nb_objs) { + struct icmp_input_ctx *ctx = icmp_input_ctx(node); struct ip_local_mbuf_data *ip_data; struct rte_icmp_hdr *icmp; + const struct iface *iface; struct rte_mbuf *mbuf; rte_edge_t edge; uint16_t cksum; ip4_addr_t ip; + if (rate_limited(&ctx->limit, graph_conf.icmp_rate, nb_objs)) { + rte_node_next_stream_move(graph, node, RATE_LIMITED); + return nb_objs; + } + for (uint16_t i = 0; i < nb_objs; i++) { mbuf = objs[i]; icmp = rte_pktmbuf_mtod(mbuf, struct rte_icmp_hdr *); @@ -49,6 +61,12 @@ icmp_input_process(struct rte_graph *graph, struct rte_node *node, void **objs, edge = INVALID; goto next; } + iface = mbuf_data(mbuf)->iface; + assert(iface != NULL); + if (iface->flags & GR_IFACE_F_PING_IGNORE) { + edge = IGNORED; + goto next; + } icmp->icmp_type = RTE_ICMP_TYPE_ECHO_REPLY; ip = ip_data->dst; ip_data->dst = ip_data->src; @@ -80,6 +98,13 @@ void icmp_input_register_callback(uint8_t icmp_type, control_queue_cb_t cb) { icmp_cb[icmp_type] = cb; } +static int icmp_input_init(const struct rte_graph *, struct rte_node *node) { + struct icmp_input_ctx *ctx = icmp_input_ctx(node); + ctx->limit.tokens = graph_conf.icmp_rate; + ctx->limit.last_refill = rte_rdtsc(); + return 0; +} + static void icmp_input_register(void) { ip_input_local_add_proto(IPPROTO_ICMP, "icmp_input"); } @@ -88,6 +113,7 @@ static struct rte_node_register icmp_input_node = { .name = "icmp_input", .process = icmp_input_process, + .init = icmp_input_init, .nb_edges = EDGE_COUNT, .next_nodes = { @@ -95,6 +121,8 @@ static struct rte_node_register icmp_input_node = { [CONTROL] = "control_output", [INVALID] = "icmp_input_invalid", [UNSUPPORTED] = "icmp_input_unsupported", + [RATE_LIMITED] = "error_rate_limited", + [IGNORED] = "ping_ignored", }, }; @@ -109,3 +137,4 @@ GR_NODE_REGISTER(icmp_input_info); GR_DROP_REGISTER(icmp_input_invalid); GR_DROP_REGISTER(icmp_input_unsupported); +GR_DROP_REGISTER(ping_ignored); diff --git a/modules/ip/datapath/ip_error.c b/modules/ip/datapath/ip_error.c index d4cb27233..7e8bc9455 100644 --- a/modules/ip/datapath/ip_error.c +++ b/modules/ip/datapath/ip_error.c @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-3-Clause // Copyright (c) 2024 Christophe Fontaine +#include "datapath.h" #include "graph.h" #include "ip4.h" #include "ip4_datapath.h" @@ -13,18 +14,21 @@ GR_NODE_CTX_TYPE(ip_error_ctx, { uint8_t icmp_type; uint8_t icmp_code; + struct rate_limit_ctx limit; }); enum edges { ICMP_OUTPUT = 0, NO_HEADROOM, NO_IP, + RATE_LIMITED, + ERR_IGNORED, EDGE_COUNT, }; static uint16_t ip_error_process(struct rte_graph *graph, struct rte_node *node, void **objs, uint16_t nb_objs) { - const struct ip_error_ctx *ctx = ip_error_ctx(node); + struct ip_error_ctx *ctx = ip_error_ctx(node); struct ip_local_mbuf_data *ip_data; const struct nexthop_info_l3 *l3; const struct nexthop *nh, *local; @@ -36,9 +40,24 @@ ip_error_process(struct rte_graph *graph, struct rte_node *node, void **objs, ui rte_edge_t edge; unsigned len; + if (rate_limited(&ctx->limit, graph_conf.icmp_error_rate, nb_objs)) { + rte_node_next_stream_move(graph, node, RATE_LIMITED); + return nb_objs; + } + for (uint16_t i = 0; i < nb_objs; i++) { mbuf = objs[i]; + iface = l3_mbuf_data(mbuf)->iface; + if (iface == NULL) { + edge = NO_IP; + goto next; + } + if (iface->flags & GR_IFACE_F_ERR_IGNORE) { + edge = ERR_IGNORED; + goto next; + } + ip = rte_pktmbuf_mtod(mbuf, struct rte_ipv4_hdr *); src = ip->src_addr; // RFC792 payload size: ip header + 64 bits of original datagram @@ -52,9 +71,7 @@ ip_error_process(struct rte_graph *graph, struct rte_node *node, void **objs, ui } // Get the local router IP address from the input iface - iface = l3_mbuf_data(mbuf)->iface; - if (iface == NULL - || (nh = fib4_lookup(iface->vrf_id, src, mbuf->hash.rss)) == NULL) { + if ((nh = fib4_lookup(iface->vrf_id, src, mbuf->hash.rss)) == NULL) { edge = NO_IP; goto next; } @@ -101,6 +118,8 @@ static int ttl_exceeded_init(const struct rte_graph *, struct rte_node *node) { struct ip_error_ctx *ctx = ip_error_ctx(node); ctx->icmp_type = RTE_ICMP_TYPE_TTL_EXCEEDED; ctx->icmp_code = RTE_ICMP_CODE_TTL_EXCEEDED; + ctx->limit.tokens = graph_conf.icmp_error_rate; + ctx->limit.last_refill = rte_rdtsc(); return 0; } @@ -108,6 +127,8 @@ static int no_route_init(const struct rte_graph *, struct rte_node *node) { struct ip_error_ctx *ctx = ip_error_ctx(node); ctx->icmp_type = RTE_ICMP_TYPE_DEST_UNREACHABLE; ctx->icmp_code = RTE_ICMP_CODE_UNREACH_NET; + ctx->limit.tokens = graph_conf.icmp_error_rate; + ctx->limit.last_refill = rte_rdtsc(); return 0; } @@ -115,6 +136,8 @@ static int frag_needed_init(const struct rte_graph *, struct rte_node *node) { struct ip_error_ctx *ctx = ip_error_ctx(node); ctx->icmp_type = RTE_ICMP_TYPE_DEST_UNREACHABLE; ctx->icmp_code = RTE_ICMP_CODE_UNREACH_FRAG; + ctx->limit.tokens = graph_conf.icmp_error_rate; + ctx->limit.last_refill = rte_rdtsc(); return 0; } @@ -126,6 +149,8 @@ static struct rte_node_register ip_forward_ttl_exceeded_node = { [ICMP_OUTPUT] = "icmp_output", [NO_HEADROOM] = "error_no_headroom", [NO_IP] = "error_no_local_ip", + [RATE_LIMITED] = "error_rate_limited", + [ERR_IGNORED] = "error_ignored", }, .init = ttl_exceeded_init, }; @@ -138,6 +163,8 @@ static struct rte_node_register no_route_node = { [ICMP_OUTPUT] = "icmp_output", [NO_HEADROOM] = "error_no_headroom", [NO_IP] = "error_no_local_ip", + [RATE_LIMITED] = "error_rate_limited", + [ERR_IGNORED] = "error_ignored", }, .init = no_route_init, }; @@ -150,6 +177,8 @@ static struct rte_node_register frag_needed_node = { [ICMP_OUTPUT] = "icmp_output", [NO_HEADROOM] = "error_no_headroom", [NO_IP] = "error_no_local_ip", + [RATE_LIMITED] = "error_rate_limited", + [ERR_IGNORED] = "error_ignored", }, .init = frag_needed_init, }; @@ -174,3 +203,5 @@ GR_NODE_REGISTER(info_no_route); GR_NODE_REGISTER(info_frag_needed); GR_DROP_REGISTER(error_no_local_ip); +GR_DROP_REGISTER(error_rate_limited); +GR_DROP_REGISTER(error_ignored); diff --git a/modules/ip6/datapath/icmp6_input.c b/modules/ip6/datapath/icmp6_input.c index 9a001aa37..25d3de037 100644 --- a/modules/ip6/datapath/icmp6_input.c +++ b/modules/ip6/datapath/icmp6_input.c @@ -2,6 +2,7 @@ // Copyright (c) 2024 Robin Jarry #include "control_output.h" +#include "datapath.h" #include "graph.h" #include "icmp6.h" #include "ip6.h" @@ -10,7 +11,11 @@ #include "mbuf.h" #include "trace.h" -#include +#include +#include +#include + +GR_NODE_CTX_TYPE(icmp6_input_ctx, { struct rate_limit_ctx limit; }); enum { ICMP6_OUTPUT = 0, @@ -23,6 +28,8 @@ enum { INVALID, UNSUPPORTED, NO_LOCAL_ADDR, + RATE_LIMITED, + IGNORED, EDGE_COUNT, }; @@ -30,12 +37,19 @@ static control_queue_cb_t icmp6_cb[UINT8_MAX]; static uint16_t icmp6_input_process(struct rte_graph *graph, struct rte_node *node, void **objs, uint16_t nb_objs) { + struct icmp6_input_ctx *ctx = icmp6_input_ctx(node); struct ip6_local_mbuf_data *d; - struct icmp6 *icmp6; struct rte_ipv6_addr tmp_ip; + const struct iface *iface; struct rte_mbuf *mbuf; + struct icmp6 *icmp6; rte_edge_t next; + if (rate_limited(&ctx->limit, graph_conf.icmp_rate, nb_objs)) { + rte_node_next_stream_move(graph, node, RATE_LIMITED); + return nb_objs; + } + for (uint16_t i = 0; i < nb_objs; i++) { mbuf = objs[i]; icmp6 = rte_pktmbuf_mtod(mbuf, struct icmp6 *); @@ -53,6 +67,12 @@ icmp6_input_process(struct rte_graph *graph, struct rte_node *node, void **objs, next = INVALID; goto next; } + iface = mbuf_data(mbuf)->iface; + assert(iface != NULL); + if (iface->flags & GR_IFACE_F_PING_IGNORE) { + next = IGNORED; + goto next; + } icmp6->type = ICMP6_TYPE_ECHO_REPLY; if (rte_ipv6_addr_is_mcast(&d->dst)) { struct nexthop *local = addr6_get_linklocal( @@ -114,6 +134,13 @@ void icmp6_input_register_callback(uint8_t icmp6_type, control_queue_cb_t cb) { icmp6_cb[icmp6_type] = cb; } +static int icmp6_input_init(const struct rte_graph *, struct rte_node *node) { + struct icmp6_input_ctx *ctx = icmp6_input_ctx(node); + ctx->limit.tokens = graph_conf.icmp_rate; + ctx->limit.last_refill = rte_rdtsc(); + return 0; +} + static void icmp6_input_register(void) { ip6_input_local_add_proto(IPPROTO_ICMPV6, "icmp6_input"); } @@ -122,6 +149,7 @@ static struct rte_node_register icmp6_input_node = { .name = "icmp6_input", .process = icmp6_input_process, + .init = icmp6_input_init, .nb_edges = EDGE_COUNT, .next_nodes = { @@ -135,6 +163,8 @@ static struct rte_node_register icmp6_input_node = { [INVALID] = "icmp6_input_invalid", [UNSUPPORTED] = "icmp6_input_unsupported", [NO_LOCAL_ADDR] = "icmp6_input_no_local_addr", + [RATE_LIMITED] = "error_rate_limited", + [IGNORED] = "ping_ignored", }, }; diff --git a/modules/ip6/datapath/ip6_error.c b/modules/ip6/datapath/ip6_error.c index 16fde4772..77626b4c5 100644 --- a/modules/ip6/datapath/ip6_error.c +++ b/modules/ip6/datapath/ip6_error.c @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-3-Clause // Copyright (c) 2024 Christophe Fontaine +#include "datapath.h" #include "graph.h" #include "icmp6.h" #include "ip6.h" @@ -10,22 +11,26 @@ #include "mbuf.h" #include +#include GR_NODE_CTX_TYPE(ip6_error_ctx, { icmp6_type_t icmp_type; uint8_t icmp_code; + struct rate_limit_ctx limit; }); enum edges { ICMP_OUTPUT = 0, NO_HEADROOM, NO_IP, + RATE_LIMITED, + ERR_IGNORED, EDGE_COUNT, }; static uint16_t ip6_error_process(struct rte_graph *graph, struct rte_node *node, void **objs, uint16_t nb_objs) { - const struct ip6_error_ctx *ctx = ip6_error_ctx(node); + struct ip6_error_ctx *ctx = ip6_error_ctx(node); struct icmp6_err_dest_unreach *du; struct icmp6_err_ttl_exceeded *te; const struct nexthop_info_l3 *l3; @@ -37,12 +42,27 @@ ip6_error_process(struct rte_graph *graph, struct rte_node *node, void **objs, u struct icmp6 *icmp6; rte_edge_t edge; + if (rate_limited(&ctx->limit, graph_conf.icmp_error_rate, nb_objs)) { + rte_node_next_stream_move(graph, node, RATE_LIMITED); + return nb_objs; + } + for (uint16_t i = 0; i < nb_objs; i++) { mbuf = objs[i]; if (gr_mbuf_is_traced(mbuf)) gr_mbuf_trace_add(mbuf, node, 0); + iface = l3_mbuf_data(mbuf)->iface; + if (iface == NULL) { + edge = NO_IP; + goto next; + } + if (iface->flags & GR_IFACE_F_ERR_IGNORE) { + edge = ERR_IGNORED; + goto next; + } + // Get the pointer to the start of the ipv6 header before // prepending any data ip = rte_pktmbuf_mtod(mbuf, struct rte_ipv6_hdr *); @@ -83,11 +103,6 @@ ip6_error_process(struct rte_graph *graph, struct rte_node *node, void **objs, u icmp6->code = ctx->icmp_code; // Get the local router IP address from the input iface - iface = l3_mbuf_data(mbuf)->iface; - if (iface == NULL) { - edge = NO_IP; - goto next; - } if ((nh = addr6_get_preferred(iface->id, &ip->src_addr)) == NULL) { edge = NO_IP; goto next; @@ -110,6 +125,8 @@ static int ttl_exceeded_init(const struct rte_graph *, struct rte_node *node) { struct ip6_error_ctx *ctx = ip6_error_ctx(node); ctx->icmp_type = ICMP6_ERR_TTL_EXCEEDED; ctx->icmp_code = 0; + ctx->limit.tokens = graph_conf.icmp_error_rate; + ctx->limit.last_refill = rte_rdtsc(); return 0; } @@ -117,6 +134,8 @@ static int no_route_init(const struct rte_graph *, struct rte_node *node) { struct ip6_error_ctx *ctx = ip6_error_ctx(node); ctx->icmp_type = ICMP6_ERR_DEST_UNREACH; ctx->icmp_code = 0; + ctx->limit.tokens = graph_conf.icmp_error_rate; + ctx->limit.last_refill = rte_rdtsc(); return 0; } @@ -128,6 +147,8 @@ static struct rte_node_register dest_unreach_node = { [ICMP_OUTPUT] = "icmp6_output", [NO_HEADROOM] = "error_no_headroom", [NO_IP] = "error_no_local_ip", + [RATE_LIMITED] = "error_rate_limited", + [ERR_IGNORED] = "error_ignored", }, .init = no_route_init, }; @@ -140,6 +161,8 @@ static struct rte_node_register ttl_exceeded_node = { [ICMP_OUTPUT] = "icmp6_output", [NO_HEADROOM] = "error_no_headroom", [NO_IP] = "error_no_local_ip", + [RATE_LIMITED] = "error_rate_limited", + [ERR_IGNORED] = "error_ignored", }, .init = ttl_exceeded_init, }; diff --git a/smoke/ip6_builtin_icmp_test.sh b/smoke/ip6_builtin_icmp_test.sh index 8056c2585..9241380e0 100755 --- a/smoke/ip6_builtin_icmp_test.sh +++ b/smoke/ip6_builtin_icmp_test.sh @@ -15,7 +15,7 @@ for n in 0 1; do netns_add $ns move_to_netns $p $ns ip -n $ns addr add fd00:ba4:$n::2/64 dev $p - ip -n $ns route add fd00:ba4::/62 via fd00:ba4:$n::1 dev $p + ip -n $ns route add default via fd00:ba4:$n::1 dev $p done grcli ping fd00:ba4:0::2 count 10 delay 100 @@ -26,3 +26,52 @@ grcli ping fd00:baa::1 count 1 && fail "ping to unknown route succeeded" grcli ping fd00:ba4:1::3 count 1 && fail "ping to non-existent host succeeded" grcli traceroute fd00:ba4:1::2 + +# Test ICMPv6 error rate limiting +grcli stats reset +grcli graph config set icmp-error-rate 1 + +# Send a burst of pings to a non-routable destination to trigger ICMPv6 dest +# unreachable errors. Most of them should be rate limited. +pids="" +for _ in {0..20}; do + ip netns exec n0 ping6 -q -W 1 -c 1 -n fd00:baa::1 & + pids="$pids $!" +done +wait $pids || true +grcli -j stats show software brief pattern error_rate_limited | + jq -e '.error_rate_limited > 0' || + fail "icmpv6 error rate limiting did not drop any packet" + +# Test ICMPv6 input rate limiting +grcli stats reset +grcli graph config set icmp-rate 1 + +# Send a burst of pings to the router local address. Most of them should be +# rate limited. +ip netns exec n0 ping6 -w 1 -i 0.01 -c 50 -n fd00:ba4:0::1 || true +grcli -j stats show software brief pattern error_rate_limited | + jq -e '.error_rate_limited > 0' || + fail "icmpv6 input rate limiting did not drop any packet" + +grcli graph config set icmp-error-rate 0 icmp-rate 0 + +# Test per-interface ping-ignore flag +grcli interface set port p0 ping-ignore on +grcli stats reset +ip netns exec n0 ping6 -W 1 -c 3 -n fd00:ba4:0::1 || true +grcli -j stats show software brief pattern ping_ignored | + jq -e '.ping_ignored > 0' || + fail "ping-ignore flag did not drop any echo request" +grcli interface set port p0 ping-ignore off +ip netns exec n0 ping6 -W 1 -c 1 -n fd00:ba4:0::1 || + fail "ping failed after disabling ping-ignore" + +# Test per-interface err-ignore flag +grcli interface set port p0 err-ignore on +grcli stats reset +ip netns exec n0 ping6 -W 1 -c 3 -n fd00:baa::1 || true +grcli -j stats show software brief pattern error_ignored | + jq -e '.error_ignored > 0' || + fail "err-ignore flag did not drop any ICMPv6 error" +grcli interface set port p0 err-ignore off diff --git a/smoke/ip_builtin_icmp_test.sh b/smoke/ip_builtin_icmp_test.sh index d6893b2c1..f73673f52 100755 --- a/smoke/ip_builtin_icmp_test.sh +++ b/smoke/ip_builtin_icmp_test.sh @@ -31,3 +31,52 @@ grcli ping 1.1.1.1 count 1 && fail "ping to unknown route succeeded" grcli ping 172.16.1.3 count 1 && fail "ping to non-existent host succeeded" grcli traceroute 172.16.0.2 + +# Test ICMP error rate limiting +grcli stats reset +grcli graph config set icmp-error-rate 1 + +# Send a burst of pings to a non-routable destination to trigger ICMP dest +# unreachable errors. Most of them should be rate limited. +pids="" +for _ in {0..20}; do + ip netns exec n0 ping -q -W 1 -c 1 -n 1.1.1.1 & + pids="$pids $!" +done +wait $pids || true +grcli -j stats show software brief pattern error_rate_limited | + jq -e '.error_rate_limited > 0' || + fail "icmp error rate limiting did not drop any packet" + +# Test ICMP input rate limiting +grcli stats reset +grcli graph config set icmp-rate 1 + +# Send a burst of pings to the router local address. Most of them should be +# rate limited. +ip netns exec n0 ping -w 1 -i 0.01 -c 50 -n 172.16.0.1 || true +grcli -j stats show software brief pattern error_rate_limited | + jq -e '.error_rate_limited > 0' || + fail "icmp input rate limiting did not drop any packet" + +grcli graph config set icmp-error-rate 0 icmp-rate 0 + +# Test per-interface ping-ignore flag +grcli interface set port p0 ping-ignore on +grcli stats reset +ip netns exec n0 ping -W 1 -c 3 -n 172.16.0.1 || true +grcli -j stats show software brief pattern ping_ignored | + jq -e '.ping_ignored > 0' || + fail "ping-ignore flag did not drop any echo request" +grcli interface set port p0 ping-ignore off +ip netns exec n0 ping -W 1 -c 1 -n 172.16.0.1 || + fail "ping failed after disabling ping-ignore" + +# Test per-interface err-ignore flag +grcli interface set port p0 err-ignore on +grcli stats reset +ip netns exec n0 ping -W 1 -c 3 -n 1.1.1.1 || true +grcli -j stats show software brief pattern error_ignored | + jq -e '.error_ignored > 0' || + fail "err-ignore flag did not drop any ICMP error" +grcli interface set port p0 err-ignore off