diff --git a/modules/ip/control/icmp.c b/modules/ip/control/icmp.c index b416e1278..6df078fb7 100644 --- a/modules/ip/control/icmp.c +++ b/modules/ip/control/icmp.c @@ -174,5 +174,6 @@ RTE_INIT(icmp_module_init) { event_subscribe(GR_EVENT_IFACE_REMOVE, icmp_event_cb); icmp_input_register_callback(RTE_ICMP_TYPE_DEST_UNREACHABLE, icmp_input_cb); icmp_input_register_callback(RTE_ICMP_TYPE_TTL_EXCEEDED, icmp_input_cb); + icmp_input_register_callback(RTE_ICMP_TYPE_PARAM_PROBLEM, icmp_input_cb); icmp_input_register_callback(RTE_ICMP_TYPE_ECHO_REPLY, icmp_input_cb); } diff --git a/modules/ip/datapath/icmp_input.c b/modules/ip/datapath/icmp_input.c index 537fa4778..3087506a7 100644 --- a/modules/ip/datapath/icmp_input.c +++ b/modules/ip/datapath/icmp_input.c @@ -23,6 +23,36 @@ enum { static control_queue_cb_t icmp_cb[UINT8_MAX]; +// RFC 792: the number of bytes of the datagram in error that an ICMP error +// message carries after the quoted IP header. +#define ICMP_QUOTED_DATA_LEN 8 + +// Source quench belongs here too, deprecated by RFC 6633 and never punted. +static inline bool icmp_quotes_datagram(uint8_t type) { + switch (type) { + case RTE_ICMP_TYPE_DEST_UNREACHABLE: + case RTE_ICMP_TYPE_REDIRECT: + case RTE_ICMP_TYPE_TTL_EXCEEDED: + case RTE_ICMP_TYPE_PARAM_PROBLEM: + return true; + default: + return false; + } +} + +// RFC 792: an error message carries its own header, the header of the datagram +// in error and the next 64 bits of it. +static inline bool icmp_quoted_datagram_valid(const struct rte_icmp_hdr *icmp, uint16_t len) { + const struct rte_ipv4_hdr *inner; + + if (len < sizeof(*icmp) + sizeof(*inner) + ICMP_QUOTED_DATA_LEN) + return false; + + inner = PAYLOAD(icmp); + + return len >= sizeof(*icmp) + rte_ipv4_hdr_len(inner) + ICMP_QUOTED_DATA_LEN; +} + static uint16_t icmp_input_process(struct rte_graph *graph, struct rte_node *node, void **objs, uint16_t nb_objs) { struct ip_local_mbuf_data *ip_data; @@ -54,6 +84,11 @@ icmp_input_process(struct rte_graph *graph, struct rte_node *node, void **objs, ip_data->src = ip; edge = OUTPUT; } else if (icmp_cb[icmp->icmp_type]) { + if (icmp_quotes_datagram(icmp->icmp_type) + && !icmp_quoted_datagram_valid(icmp, ip_data->len)) { + edge = INVALID; + goto next; + } control_output_set_cb(mbuf, icmp_cb[icmp->icmp_type], clock_ns()); edge = CONTROL; } else { @@ -108,3 +143,170 @@ GR_NODE_REGISTER(icmp_input_info); GR_DROP_REGISTER(icmp_input_invalid); GR_DROP_REGISTER(icmp_input_unsupported); + +#ifdef __GROUT_UNIT_TEST__ +#include "_cmocka.h" + +#include + +int gr_rte_log_type; +struct log_types log_types = STAILQ_HEAD_INITIALIZER(log_types); +struct node_infos node_infos = STAILQ_HEAD_INITIALIZER(node_infos); +mock_func(rte_edge_t, gr_node_attach_parent(const char *, const char *)); +mock_func(void *, gr_mbuf_trace_add(struct rte_mbuf *, struct rte_node *, size_t)); +mock_func(uint16_t, drop_packets(struct rte_graph *, struct rte_node *, void **, uint16_t)); +mock_func(int, drop_format(char *, size_t, const void *, size_t)); +mock_func(void, ip_input_local_add_proto(uint8_t, const char *)); +mock_func(rte_edge_t, gr_control_input_register_handler(const char *)); +mock_func(int, post_to_stack(rte_edge_t, struct rte_mbuf *)); +mock_func(struct iface *, get_vrf_iface(uint16_t)); +mock_func(bool, addr4_is_local_on_iface(uint16_t, ip4_addr_t)); +mock_func(int, trace_icmp_format(char *, size_t, const struct rte_icmp_hdr *, size_t)); + +// Referenced by the inline helpers in clock.h and control_output.h. +__thread gr_clock_ns_t clock_snapshot_ns; +__thread bool clock_trusted; +int cq_callback_offset; +int cq_priv_offset; + +struct fake_mbuf { + struct rte_icmp_hdr icmp; + struct rte_ipv4_hdr inner_ip; + uint8_t inner_data[ICMP_QUOTED_DATA_LEN]; + struct rte_mbuf mbuf; + uint8_t priv_data[GR_MBUF_PRIV_MAX_SIZE]; +}; + +static struct iface test_iface; + +static void test_cb(void *, uintptr_t, const struct control_queue_drain *) { } + +static int setup(void **) { + // Dynamic fields live inside the mbuf reserved area, as in a real one. + cq_callback_offset = offsetof(struct rte_mbuf, dynfield1); + cq_priv_offset = cq_callback_offset + sizeof(control_queue_cb_t); + icmp_cb[RTE_ICMP_TYPE_TTL_EXCEEDED] = test_cb; + return 0; +} + +static void fake_mbuf_init(struct fake_mbuf *fm, uint8_t type, uint16_t len) { + struct ip_local_mbuf_data *d; + + memset(fm, 0, sizeof(*fm)); + + fm->icmp.icmp_type = type; + fm->icmp.icmp_code = 0; + + fm->inner_ip.version = 4; + fm->inner_ip.ihl = sizeof(fm->inner_ip) / 4; + fm->inner_ip.next_proto_id = IPPROTO_TCP; + + fm->mbuf.buf_addr = &fm->icmp; + fm->mbuf.data_len = sizeof(fm->icmp) + sizeof(fm->inner_ip) + sizeof(fm->inner_data); + fm->mbuf.pkt_len = fm->mbuf.data_len; + fm->mbuf.nb_segs = 1; + + d = ip_local_mbuf_data(&fm->mbuf); + d->src = RTE_IPV4(192, 168, 0, 1); + d->dst = RTE_IPV4(192, 168, 0, 2); + d->len = len; + d->ttl = 64; + d->proto = IPPROTO_ICMP; + d->iface = &test_iface; +} + +static void fake_mbuf_cksum(struct fake_mbuf *fm) { + uint16_t len = ip_local_mbuf_data(&fm->mbuf)->len; + + fm->icmp.icmp_cksum = 0; + fm->icmp.icmp_cksum = ~rte_raw_cksum(&fm->icmp, len); +} + +#define ICMP_ERROR_MIN_LEN \ + (sizeof(struct rte_icmp_hdr) + sizeof(struct rte_ipv4_hdr) + ICMP_QUOTED_DATA_LEN) + +static void icmp_input_too_short(void **) { + struct fake_mbuf fm; + void *obj = &fm.mbuf; + + fake_mbuf_init(&fm, RTE_ICMP_TYPE_ECHO_REQUEST, ICMP_MIN_SIZE - 1); + fake_mbuf_cksum(&fm); + + expect_uint_value(rte_node_enqueue_x1, next, INVALID); + icmp_input_process(NULL, NULL, &obj, 1); +} + +static void icmp_input_bad_cksum(void **) { + struct fake_mbuf fm; + void *obj = &fm.mbuf; + + fake_mbuf_init(&fm, RTE_ICMP_TYPE_ECHO_REQUEST, ICMP_ERROR_MIN_LEN); + fake_mbuf_cksum(&fm); + fm.icmp.icmp_cksum = ~fm.icmp.icmp_cksum; + + expect_uint_value(rte_node_enqueue_x1, next, INVALID); + icmp_input_process(NULL, NULL, &obj, 1); +} + +static void icmp_input_echo_request(void **) { + struct fake_mbuf fm; + void *obj = &fm.mbuf; + + fake_mbuf_init(&fm, RTE_ICMP_TYPE_ECHO_REQUEST, ICMP_ERROR_MIN_LEN); + fake_mbuf_cksum(&fm); + + expect_uint_value(rte_node_enqueue_x1, next, OUTPUT); + icmp_input_process(NULL, NULL, &obj, 1); + + assert_int_equal(fm.icmp.icmp_type, RTE_ICMP_TYPE_ECHO_REPLY); +} + +// RFC 792 wants the quoted header plus 64 bits of the datagram in error. +static void icmp_input_error_too_short(void **) { + struct fake_mbuf fm; + void *obj = &fm.mbuf; + + fake_mbuf_init(&fm, RTE_ICMP_TYPE_TTL_EXCEEDED, ICMP_ERROR_MIN_LEN - 1); + fake_mbuf_cksum(&fm); + + expect_uint_value(rte_node_enqueue_x1, next, INVALID); + icmp_input_process(NULL, NULL, &obj, 1); +} + +// A quoted header claiming options the message does not carry. +static void icmp_input_error_inner_ihl_too_long(void **) { + struct fake_mbuf fm; + void *obj = &fm.mbuf; + + fake_mbuf_init(&fm, RTE_ICMP_TYPE_TTL_EXCEEDED, ICMP_ERROR_MIN_LEN); + fm.inner_ip.ihl = 15; // 60 bytes, way past the message + fake_mbuf_cksum(&fm); + + expect_uint_value(rte_node_enqueue_x1, next, INVALID); + icmp_input_process(NULL, NULL, &obj, 1); +} + +static void icmp_input_error_valid(void **) { + struct fake_mbuf fm; + void *obj = &fm.mbuf; + + fake_mbuf_init(&fm, RTE_ICMP_TYPE_TTL_EXCEEDED, ICMP_ERROR_MIN_LEN); + fake_mbuf_cksum(&fm); + + expect_uint_value(rte_node_enqueue_x1, next, CONTROL); + icmp_input_process(NULL, NULL, &obj, 1); +} + +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup(icmp_input_too_short, setup), + cmocka_unit_test_setup(icmp_input_bad_cksum, setup), + cmocka_unit_test_setup(icmp_input_echo_request, setup), + cmocka_unit_test_setup(icmp_input_error_too_short, setup), + cmocka_unit_test_setup(icmp_input_error_inner_ihl_too_long, setup), + cmocka_unit_test_setup(icmp_input_error_valid, setup), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} +#endif diff --git a/modules/ip/datapath/ip_local.c b/modules/ip/datapath/ip_local.c index ec09e5fab..a6e1f0d6f 100644 --- a/modules/ip/datapath/ip_local.c +++ b/modules/ip/datapath/ip_local.c @@ -10,7 +10,11 @@ LOG_TYPE("graph"); -#define UNKNOWN_PROTO 0 +enum { + UNKNOWN_PROTO = 0, + BAD_LENGTH, + EDGE_COUNT, +}; static rte_edge_t edges[UINT_NUM_VALUES(uint8_t)] = {UNKNOWN_PROTO}; void ip_input_local_add_proto(uint8_t proto, const char *next_node) { @@ -42,14 +46,21 @@ static uint16_t ip_input_local_process( if (edge != UNKNOWN_PROTO) { const struct iface *iface = l3_mbuf_data(mbuf)->iface; struct ip_local_mbuf_data *data = ip_local_mbuf_data(mbuf); - data->src = ip->src_addr; - data->dst = ip->dst_addr; - data->len = rte_be_to_cpu_16(ip->total_length) - rte_ipv4_hdr_len(ip); - data->vrf_id = iface->vrf_id; - data->proto = ip->next_proto_id; - data->ttl = ip->time_to_live; - mbuf->packet_type = RTE_PTYPE_L3_IPV4; - rte_pktmbuf_adj(mbuf, rte_ipv4_hdr_len(ip)); + uint16_t hdr_len = rte_ipv4_hdr_len(ip); + uint16_t total_len = rte_be_to_cpu_16(ip->total_length); + + if (total_len < hdr_len || total_len > rte_pktmbuf_data_len(mbuf)) { + edge = BAD_LENGTH; + } else { + data->src = ip->src_addr; + data->dst = ip->dst_addr; + data->len = total_len - hdr_len; + data->vrf_id = iface->vrf_id; + data->proto = ip->next_proto_id; + data->ttl = ip->time_to_live; + mbuf->packet_type = RTE_PTYPE_L3_IPV4; + rte_pktmbuf_adj(mbuf, hdr_len); + } } rte_node_enqueue_x1(graph, node, edge, mbuf); } @@ -60,9 +71,10 @@ static uint16_t ip_input_local_process( static struct rte_node_register input_node = { .name = "ip_input_local", .process = ip_input_local_process, - .nb_edges = 1, + .nb_edges = EDGE_COUNT, .next_nodes = { [UNKNOWN_PROTO] = "ip_input_local_unknown_proto", + [BAD_LENGTH] = "ip_input_local_bad_length", }, }; @@ -74,3 +86,136 @@ static struct gr_node_info info = { GR_NODE_REGISTER(info); GR_DROP_REGISTER(ip_input_local_unknown_proto); +GR_DROP_REGISTER(ip_input_local_bad_length); + +#ifdef __GROUT_UNIT_TEST__ +#include "_cmocka.h" + +#include + +int gr_rte_log_type; +struct log_types log_types = STAILQ_HEAD_INITIALIZER(log_types); +struct node_infos node_infos = STAILQ_HEAD_INITIALIZER(node_infos); +mock_func(rte_edge_t, gr_node_attach_parent(const char *, const char *)); +mock_func(void *, gr_mbuf_trace_add(struct rte_mbuf *, struct rte_node *, size_t)); +mock_func(uint16_t, drop_packets(struct rte_graph *, struct rte_node *, void **, uint16_t)); +mock_func(int, drop_format(char *, size_t, const void *, size_t)); + +// Any edge value that is not UNKNOWN_PROTO, as if a handler was registered. +#define TEST_EDGE 42 +#define TEST_PROTO IPPROTO_ICMP +#define TEST_PAYLOAD_LEN 8 + +struct fake_mbuf { + struct rte_ipv4_hdr ipv4_hdr; + uint8_t payload[TEST_PAYLOAD_LEN]; + struct rte_mbuf mbuf; + uint8_t priv_data[GR_MBUF_PRIV_MAX_SIZE]; +}; + +static struct iface test_iface; + +static void fake_mbuf_init(struct fake_mbuf *fm) { + memset(fm, 0, sizeof(*fm)); + + fm->ipv4_hdr.version = 4; + fm->ipv4_hdr.ihl = sizeof(fm->ipv4_hdr) / 4; + fm->ipv4_hdr.total_length = rte_cpu_to_be_16(sizeof(fm->ipv4_hdr) + TEST_PAYLOAD_LEN); + fm->ipv4_hdr.time_to_live = 64; + fm->ipv4_hdr.next_proto_id = TEST_PROTO; + fm->ipv4_hdr.src_addr = RTE_IPV4(192, 168, 0, 1); + fm->ipv4_hdr.dst_addr = RTE_IPV4(192, 168, 0, 2); + + fm->mbuf.buf_addr = &fm->ipv4_hdr; + fm->mbuf.data_len = sizeof(fm->ipv4_hdr) + TEST_PAYLOAD_LEN; + fm->mbuf.pkt_len = fm->mbuf.data_len; + fm->mbuf.nb_segs = 1; + + l3_mbuf_data(&fm->mbuf)->iface = &test_iface; +} + +static int setup(void **) { + edges[TEST_PROTO] = TEST_EDGE; + return 0; +} + +// A total length smaller than the header would make the payload length +// underflow. +static void ip_local_total_length_below_ihl(void **) { + struct fake_mbuf fm; + void *obj = &fm.mbuf; + + fake_mbuf_init(&fm); + fm.ipv4_hdr.ihl = 10; // 40 bytes of header + fm.ipv4_hdr.total_length = rte_cpu_to_be_16(sizeof(fm.ipv4_hdr)); + + expect_uint_value(rte_node_enqueue_x1, next, BAD_LENGTH); + ip_input_local_process(NULL, NULL, &obj, 1); +} + +// A total length announcing more payload than the packet carries. +static void ip_local_total_length_beyond_mbuf(void **) { + struct fake_mbuf fm; + void *obj = &fm.mbuf; + + fake_mbuf_init(&fm); + fm.ipv4_hdr.total_length = rte_cpu_to_be_16(fm.mbuf.pkt_len + 1); + + expect_uint_value(rte_node_enqueue_x1, next, BAD_LENGTH); + ip_input_local_process(NULL, NULL, &obj, 1); +} + +// Only the first segment can be read contiguously, which is what every node +// below does, so a payload that reaches into the next one is refused. +static void ip_local_total_length_beyond_first_segment(void **) { + struct fake_mbuf fm; + void *obj = &fm.mbuf; + + fake_mbuf_init(&fm); + fm.mbuf.data_len = sizeof(fm.ipv4_hdr) + TEST_PAYLOAD_LEN / 2; + fm.mbuf.pkt_len = sizeof(fm.ipv4_hdr) + TEST_PAYLOAD_LEN; + fm.mbuf.nb_segs = 2; + + expect_uint_value(rte_node_enqueue_x1, next, BAD_LENGTH); + ip_input_local_process(NULL, NULL, &obj, 1); +} + +// Ethernet padding leaves more bytes than the total length announces, which is +// perfectly valid. +static void ip_local_total_length_below_mbuf(void **) { + struct fake_mbuf fm; + void *obj = &fm.mbuf; + + fake_mbuf_init(&fm); + fm.mbuf.data_len = fm.mbuf.pkt_len = 60; + + expect_uint_value(rte_node_enqueue_x1, next, TEST_EDGE); + ip_input_local_process(NULL, NULL, &obj, 1); + + assert_int_equal(ip_local_mbuf_data(&fm.mbuf)->len, TEST_PAYLOAD_LEN); +} + +static void ip_local_valid_length(void **) { + struct fake_mbuf fm; + void *obj = &fm.mbuf; + + fake_mbuf_init(&fm); + + expect_uint_value(rte_node_enqueue_x1, next, TEST_EDGE); + ip_input_local_process(NULL, NULL, &obj, 1); + + assert_int_equal(ip_local_mbuf_data(&fm.mbuf)->len, TEST_PAYLOAD_LEN); +} + +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup(ip_local_total_length_below_ihl, setup), + cmocka_unit_test_setup(ip_local_total_length_beyond_mbuf, setup), + cmocka_unit_test_setup(ip_local_total_length_beyond_first_segment, setup), + cmocka_unit_test_setup(ip_local_total_length_below_mbuf, setup), + cmocka_unit_test_setup(ip_local_valid_length, setup), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} +#endif diff --git a/modules/ip/datapath/meson.build b/modules/ip/datapath/meson.build index f6766fff5..1aa950b15 100644 --- a/modules/ip/datapath/meson.build +++ b/modules/ip/datapath/meson.build @@ -24,5 +24,13 @@ tests += [ { 'sources': files('ip_input.c'), 'link_args': [], + }, + { + 'sources': files('ip_local.c'), + 'link_args': [], + }, + { + 'sources': files('icmp_input.c'), + 'link_args': [], } ] diff --git a/modules/ip6/datapath/icmp6_input.c b/modules/ip6/datapath/icmp6_input.c index 14bf13cc4..2d3c4a274 100644 --- a/modules/ip6/datapath/icmp6_input.c +++ b/modules/ip6/datapath/icmp6_input.c @@ -27,6 +27,25 @@ enum { static control_queue_cb_t icmp6_cb[UINT8_MAX]; +// RFC 4443 2.3: the checksum covers a pseudo header made of the addresses, the +// upper layer packet length and the ICMPv6 next header value. +static inline int icmp6_cksum_verify(const struct ip6_local_mbuf_data *d, const void *icmp6) { + const struct rte_ipv6_hdr phdr = { + .payload_len = rte_cpu_to_be_16(d->len), + .proto = IPPROTO_ICMPV6, + .src_addr = d->src, + .dst_addr = d->dst, + }; + + return rte_ipv6_udptcp_cksum_verify(&phdr, icmp6); +} + +// RFC 4443 2.1: error messages are the types with the high order bit clear. +// They all quote the invoking packet, of which at least its IPv6 header fits. +static inline bool icmp6_is_error(uint8_t type) { + return (type & 0x80) == 0; +} + static uint16_t icmp6_input_process(struct rte_graph *graph, struct rte_node *node, void **objs, uint16_t nb_objs) { struct ip6_local_mbuf_data *d; @@ -46,6 +65,15 @@ icmp6_input_process(struct rte_graph *graph, struct rte_node *node, void **objs, memcpy(t, icmp6, trace_len); } + if (d->len < GR_ICMP6_HDR_LEN) { + next = INVALID; + goto next; + } + if (icmp6_cksum_verify(d, icmp6) < 0) { + next = BAD_CHECKSUM; + goto next; + } + switch (icmp6->type) { case ICMP6_TYPE_ECHO_REQUEST: if (icmp6->code != 0) { @@ -91,6 +119,11 @@ icmp6_input_process(struct rte_graph *graph, struct rte_node *node, void **objs, break; default: if (icmp6_cb[icmp6->type] != NULL) { + if (icmp6_is_error(icmp6->type) + && d->len < GR_ICMP6_HDR_LEN + sizeof(struct rte_ipv6_hdr)) { + next = INVALID; + goto next; + } control_output_set_cb(mbuf, icmp6_cb[icmp6->type], clock_ns()); next = CONTROL; } else { @@ -150,3 +183,182 @@ GR_DROP_REGISTER(icmp6_input_bad_checksum); GR_DROP_REGISTER(icmp6_input_invalid); GR_DROP_REGISTER(icmp6_input_unsupported); GR_DROP_REGISTER(icmp6_input_no_local_addr); + +#ifdef __GROUT_UNIT_TEST__ +#include "_cmocka.h" + +#include + +int gr_rte_log_type; +struct log_types log_types = STAILQ_HEAD_INITIALIZER(log_types); +struct node_infos node_infos = STAILQ_HEAD_INITIALIZER(node_infos); +mock_func(rte_edge_t, gr_node_attach_parent(const char *, const char *)); +mock_func(void *, gr_mbuf_trace_add(struct rte_mbuf *, struct rte_node *, size_t)); +mock_func(uint16_t, drop_packets(struct rte_graph *, struct rte_node *, void **, uint16_t)); +mock_func(int, drop_format(char *, size_t, const void *, size_t)); +mock_func(void, ip6_input_local_add_proto(uint8_t, const char *)); +mock_func(rte_edge_t, gr_control_input_register_handler(const char *)); +mock_func(int, post_to_stack(rte_edge_t, struct rte_mbuf *)); +mock_func(struct iface *, get_vrf_iface(uint16_t)); +mock_func(bool, addr6_is_local_on_iface(uint16_t, const struct rte_ipv6_addr *)); +mock_func(struct nexthop *, addr6_get_linklocal(uint16_t)); + +// Referenced by the inline helpers in clock.h and control_output.h. +__thread gr_clock_ns_t clock_snapshot_ns; +__thread bool clock_trusted; +int cq_callback_offset; +int cq_priv_offset; +mock_func(int, trace_icmp6_format(char *, size_t, const struct icmp6 *, size_t)); + +#define TEST_PAYLOAD_LEN 8 + +struct fake_mbuf { + struct icmp6 icmp6; + uint8_t payload[64]; + struct rte_mbuf mbuf; + uint8_t priv_data[GR_MBUF_PRIV_MAX_SIZE]; +}; + +static struct iface test_iface; + +static void test_cb(void *, uintptr_t, const struct control_queue_drain *) { } + +static int setup(void **) { + // Dynamic fields live inside the mbuf reserved area, as in a real one. + cq_callback_offset = offsetof(struct rte_mbuf, dynfield1); + cq_priv_offset = cq_callback_offset + sizeof(control_queue_cb_t); + icmp6_cb[ICMP6_ERR_PKT_TOO_BIG] = test_cb; + return 0; +} + +static void fake_mbuf_init(struct fake_mbuf *fm, uint8_t type, uint16_t len) { + struct ip6_local_mbuf_data *d; + + memset(fm, 0, sizeof(*fm)); + + fm->icmp6.type = type; + fm->icmp6.code = 0; + + fm->mbuf.buf_addr = &fm->icmp6; + fm->mbuf.data_len = sizeof(fm->icmp6) + sizeof(fm->payload); + fm->mbuf.pkt_len = fm->mbuf.data_len; + fm->mbuf.nb_segs = 1; + + d = ip6_local_mbuf_data(&fm->mbuf); + memset(&d->src, 0x11, sizeof(d->src)); + memset(&d->dst, 0x22, sizeof(d->dst)); + d->len = len; + d->hop_limit = 64; + d->proto = IPPROTO_ICMPV6; + d->iface = &test_iface; +} + +// Fill in the checksum the node expects for the message as it stands. +static void fake_mbuf_cksum(struct fake_mbuf *fm) { + const struct ip6_local_mbuf_data *d = ip6_local_mbuf_data(&fm->mbuf); + const struct rte_ipv6_hdr phdr = { + .payload_len = rte_cpu_to_be_16(d->len), + .proto = IPPROTO_ICMPV6, + .src_addr = d->src, + .dst_addr = d->dst, + }; + + fm->icmp6.cksum = 0; + fm->icmp6.cksum = rte_ipv6_udptcp_cksum(&phdr, &fm->icmp6); +} + +// RFC 4443: every ICMPv6 message carries the 4 byte preamble and a 4 byte +// body. Spell the minimum out rather than reuse the constant the node tests +// against, so that the bound itself is checked and not just the comparison. +#define ICMP6_RFC_MIN_LEN 8 + +static void icmp6_input_too_short(void **) { + struct fake_mbuf fm; + void *obj = &fm.mbuf; + + fake_mbuf_init(&fm, ICMP6_TYPE_ECHO_REQUEST, ICMP6_RFC_MIN_LEN - 1); + fake_mbuf_cksum(&fm); + + expect_uint_value(rte_node_enqueue_x1, next, INVALID); + icmp6_input_process(NULL, NULL, &obj, 1); +} + +// The shortest message the RFC allows must go through. +static void icmp6_input_shortest_valid(void **) { + struct fake_mbuf fm; + void *obj = &fm.mbuf; + + fake_mbuf_init(&fm, ICMP6_TYPE_ECHO_REQUEST, ICMP6_RFC_MIN_LEN); + fake_mbuf_cksum(&fm); + + expect_uint_value(rte_node_enqueue_x1, next, ICMP6_OUTPUT); + icmp6_input_process(NULL, NULL, &obj, 1); +} + +static void icmp6_input_bad_cksum(void **) { + struct fake_mbuf fm; + void *obj = &fm.mbuf; + + fake_mbuf_init(&fm, ICMP6_TYPE_ECHO_REQUEST, sizeof(struct icmp6) + TEST_PAYLOAD_LEN); + fake_mbuf_cksum(&fm); + fm.icmp6.cksum = ~fm.icmp6.cksum; + + expect_uint_value(rte_node_enqueue_x1, next, BAD_CHECKSUM); + icmp6_input_process(NULL, NULL, &obj, 1); +} + +// A well formed echo request is answered by the datapath. +static void icmp6_input_echo_request(void **) { + struct fake_mbuf fm; + void *obj = &fm.mbuf; + + fake_mbuf_init(&fm, ICMP6_TYPE_ECHO_REQUEST, sizeof(struct icmp6) + TEST_PAYLOAD_LEN); + fake_mbuf_cksum(&fm); + + expect_uint_value(rte_node_enqueue_x1, next, ICMP6_OUTPUT); + icmp6_input_process(NULL, NULL, &obj, 1); + + assert_int_equal(fm.icmp6.type, ICMP6_TYPE_ECHO_REPLY); +} + +// RFC 4443: the 8 byte error header plus the invoking IPv6 header. Spelled +// out on purpose, see icmp6_input_too_short(). +#define ICMP6_ERROR_MIN_LEN 48 + +// RFC 4443: an error message quotes at least the IPv6 header of the packet +// that caused it. +static void icmp6_input_error_too_short(void **) { + struct fake_mbuf fm; + void *obj = &fm.mbuf; + + fake_mbuf_init(&fm, ICMP6_ERR_PKT_TOO_BIG, ICMP6_ERROR_MIN_LEN - 1); + fake_mbuf_cksum(&fm); + + expect_uint_value(rte_node_enqueue_x1, next, INVALID); + icmp6_input_process(NULL, NULL, &obj, 1); +} + +static void icmp6_input_error_valid(void **) { + struct fake_mbuf fm; + void *obj = &fm.mbuf; + + fake_mbuf_init(&fm, ICMP6_ERR_PKT_TOO_BIG, ICMP6_ERROR_MIN_LEN); + fake_mbuf_cksum(&fm); + + expect_uint_value(rte_node_enqueue_x1, next, CONTROL); + icmp6_input_process(NULL, NULL, &obj, 1); +} + +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup(icmp6_input_too_short, setup), + cmocka_unit_test_setup(icmp6_input_shortest_valid, setup), + cmocka_unit_test_setup(icmp6_input_bad_cksum, setup), + cmocka_unit_test_setup(icmp6_input_echo_request, setup), + cmocka_unit_test_setup(icmp6_input_error_too_short, setup), + cmocka_unit_test_setup(icmp6_input_error_valid, setup), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} +#endif diff --git a/modules/ip6/datapath/ip6_local.c b/modules/ip6/datapath/ip6_local.c index c0a861876..ad99676db 100644 --- a/modules/ip6/datapath/ip6_local.c +++ b/modules/ip6/datapath/ip6_local.c @@ -14,6 +14,7 @@ LOG_TYPE("graph"); enum { UNKNOWN_PROTO = 0, BAD_CHECKSUM, + BAD_LENGTH, ERROR, EDGE_COUNT, }; @@ -57,6 +58,11 @@ static uint16_t ip6_input_local_process( d->iface = iface; d->ext_offset = sizeof(*ip); + if (rte_pktmbuf_data_len(m) < sizeof(*ip) + d->len) { + edge = BAD_LENGTH; + goto next; + } + // advance through IPv6 extension headers until we find a registered handler while ((edge = edges[d->proto]) == UNKNOWN_PROTO) { size_t ext_size = 0; @@ -72,6 +78,11 @@ static uint16_t ip6_input_local_process( next_proto = rte_ipv6_get_next_ext(ext, d->proto, &ext_size); if (next_proto < 0) break; // end of extension headers + // the extensions must fit in the announced payload + if (d->len < ext_size) { + edge = BAD_LENGTH; + goto next; + } d->ext_offset += ext_size; d->len -= ext_size; d->proto = next_proto; @@ -80,6 +91,8 @@ static uint16_t ip6_input_local_process( if (edge == UNKNOWN_PROTO) goto next; + m->packet_type = RTE_PTYPE_L3_IPV6; + switch (d->proto) { case IPPROTO_AH: case IPPROTO_HOPOPTS: @@ -92,7 +105,6 @@ static uint16_t ip6_input_local_process( case IPPROTO_TCP: case IPPROTO_SCTP: case IPPROTO_DCCP: - m->packet_type = RTE_PTYPE_L3_IPV6; // These protocols have checksum fields to be verified. break; default: @@ -133,6 +145,7 @@ static struct rte_node_register input_node = { .next_nodes = { [UNKNOWN_PROTO] = "ip6_input_local_unknown_proto", [BAD_CHECKSUM] = "ip6_input_local_bad_checksum", + [BAD_LENGTH] = "ip6_input_local_bad_length", [ERROR] = "ip6_input_local_error", }, }; @@ -146,4 +159,156 @@ GR_NODE_REGISTER(info); GR_DROP_REGISTER(ip6_input_local_unknown_proto); GR_DROP_REGISTER(ip6_input_local_bad_checksum); +GR_DROP_REGISTER(ip6_input_local_bad_length); GR_DROP_REGISTER(ip6_input_local_error); + +#ifdef __GROUT_UNIT_TEST__ +#include "_cmocka.h" + +#include + +int gr_rte_log_type; +struct log_types log_types = STAILQ_HEAD_INITIALIZER(log_types); +struct node_infos node_infos = STAILQ_HEAD_INITIALIZER(node_infos); +mock_func(rte_edge_t, gr_node_attach_parent(const char *, const char *)); +mock_func(void *, gr_mbuf_trace_add(struct rte_mbuf *, struct rte_node *, size_t)); +mock_func(uint16_t, drop_packets(struct rte_graph *, struct rte_node *, void **, uint16_t)); +mock_func(int, drop_format(char *, size_t, const void *, size_t)); + +// Any edge value that is not UNKNOWN_PROTO, as if a handler was registered. +#define TEST_EDGE 42 +#define TEST_PROTO IPPROTO_ICMPV6 +#define TEST_PAYLOAD_LEN 8 + +struct fake_mbuf { + struct rte_ipv6_hdr ipv6_hdr; + uint8_t payload[64]; + struct rte_mbuf mbuf; + uint8_t priv_data[GR_MBUF_PRIV_MAX_SIZE]; +}; + +static struct iface test_iface; + +static void fake_mbuf_init(struct fake_mbuf *fm) { + memset(fm, 0, sizeof(*fm)); + + fm->ipv6_hdr.vtc_flow = rte_cpu_to_be_32(6 << 28); + fm->ipv6_hdr.payload_len = rte_cpu_to_be_16(TEST_PAYLOAD_LEN); + fm->ipv6_hdr.proto = TEST_PROTO; + fm->ipv6_hdr.hop_limits = 64; + memset(&fm->ipv6_hdr.src_addr, 0x11, sizeof(fm->ipv6_hdr.src_addr)); + memset(&fm->ipv6_hdr.dst_addr, 0x22, sizeof(fm->ipv6_hdr.dst_addr)); + + fm->mbuf.buf_addr = &fm->ipv6_hdr; + fm->mbuf.data_len = sizeof(fm->ipv6_hdr) + TEST_PAYLOAD_LEN; + fm->mbuf.pkt_len = fm->mbuf.data_len; + fm->mbuf.nb_segs = 1; + + l3_mbuf_data(&fm->mbuf)->iface = &test_iface; +} + +static int setup(void **) { + edges[TEST_PROTO] = TEST_EDGE; + return 0; +} + +// A payload length announcing more than the packet carries. +static void ip6_local_payload_len_beyond_mbuf(void **) { + struct fake_mbuf fm; + void *obj = &fm.mbuf; + + fake_mbuf_init(&fm); + fm.ipv6_hdr.payload_len = rte_cpu_to_be_16(TEST_PAYLOAD_LEN + 1); + + expect_uint_value(rte_node_enqueue_x1, next, BAD_LENGTH); + ip6_input_local_process(NULL, NULL, &obj, 1); +} + +// An extension header larger than the announced payload would make the +// remaining length underflow as the walk subtracts it. +static void ip6_local_ext_beyond_payload_len(void **) { + struct fake_mbuf fm; + void *obj = &fm.mbuf; + + fake_mbuf_init(&fm); + fm.mbuf.data_len = fm.mbuf.pkt_len = sizeof(fm.ipv6_hdr) + sizeof(fm.payload); + // One hop by hop options header, which rte_ipv6_get_next_ext() sizes at + // 8 bytes, while the payload claims to be shorter than that. + fm.ipv6_hdr.proto = IPPROTO_HOPOPTS; + fm.ipv6_hdr.payload_len = rte_cpu_to_be_16(4); + fm.payload[0] = TEST_PROTO; + fm.payload[1] = 0; + + expect_uint_value(rte_node_enqueue_x1, next, BAD_LENGTH); + ip6_input_local_process(NULL, NULL, &obj, 1); +} + +// Only the first segment can be read contiguously, which is what every node +// below does, so a payload that reaches into the next one is refused. +static void ip6_local_payload_len_beyond_first_segment(void **) { + struct fake_mbuf fm; + void *obj = &fm.mbuf; + + fake_mbuf_init(&fm); + fm.mbuf.data_len = sizeof(fm.ipv6_hdr) + TEST_PAYLOAD_LEN / 2; + fm.mbuf.pkt_len = sizeof(fm.ipv6_hdr) + TEST_PAYLOAD_LEN; + fm.mbuf.nb_segs = 2; + + expect_uint_value(rte_node_enqueue_x1, next, BAD_LENGTH); + ip6_input_local_process(NULL, NULL, &obj, 1); +} + +// Trailing bytes beyond the announced payload are valid. +static void ip6_local_payload_len_below_mbuf(void **) { + struct fake_mbuf fm; + void *obj = &fm.mbuf; + + fake_mbuf_init(&fm); + fm.mbuf.data_len = fm.mbuf.pkt_len = sizeof(fm.ipv6_hdr) + sizeof(fm.payload); + + expect_uint_value(rte_node_enqueue_x1, next, TEST_EDGE); + ip6_input_local_process(NULL, NULL, &obj, 1); + + assert_int_equal(ip6_local_mbuf_data(&fm.mbuf)->len, TEST_PAYLOAD_LEN); +} + +static void ip6_local_valid_length(void **) { + struct fake_mbuf fm; + void *obj = &fm.mbuf; + + fake_mbuf_init(&fm); + + expect_uint_value(rte_node_enqueue_x1, next, TEST_EDGE); + ip6_input_local_process(NULL, NULL, &obj, 1); + + assert_int_equal(ip6_local_mbuf_data(&fm.mbuf)->len, TEST_PAYLOAD_LEN); +} + +// l4_loopback_output picks the address family from the packet type, so a +// protocol with no checksum to verify needs it set just the same. +static void ip6_local_sets_packet_type(void **) { + struct fake_mbuf fm; + void *obj = &fm.mbuf; + + fake_mbuf_init(&fm); + assert_int_not_equal(fm.mbuf.packet_type, RTE_PTYPE_L3_IPV6); + + expect_uint_value(rte_node_enqueue_x1, next, TEST_EDGE); + ip6_input_local_process(NULL, NULL, &obj, 1); + + assert_int_equal(fm.mbuf.packet_type, RTE_PTYPE_L3_IPV6); +} + +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup(ip6_local_payload_len_beyond_mbuf, setup), + cmocka_unit_test_setup(ip6_local_payload_len_beyond_first_segment, setup), + cmocka_unit_test_setup(ip6_local_ext_beyond_payload_len, setup), + cmocka_unit_test_setup(ip6_local_payload_len_below_mbuf, setup), + cmocka_unit_test_setup(ip6_local_valid_length, setup), + cmocka_unit_test_setup(ip6_local_sets_packet_type, setup), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} +#endif diff --git a/modules/ip6/datapath/meson.build b/modules/ip6/datapath/meson.build index 2fd3c5678..dc0894453 100644 --- a/modules/ip6/datapath/meson.build +++ b/modules/ip6/datapath/meson.build @@ -29,4 +29,16 @@ tests += [ 'sources': files('ndp_na_input.c'), 'link_args': [], }, + { + 'sources': files('ndp_ns_input.c'), + 'link_args': [], + }, + { + 'sources': files('ip6_local.c'), + 'link_args': [], + }, + { + 'sources': files('icmp6_input.c'), + 'link_args': [], + }, ] diff --git a/modules/ip6/datapath/ndp_ns_input.c b/modules/ip6/datapath/ndp_ns_input.c index 1b64a389d..f8e0477af 100644 --- a/modules/ip6/datapath/ndp_ns_input.c +++ b/modules/ip6/datapath/ndp_ns_input.c @@ -61,7 +61,7 @@ static uint16_t ndp_ns_input_process( // - ICMP Code is 0. ASSERT_NDP(icmp6->code == 0); // - ICMP length (derived from the IP length) is 24 or more octets. - ASSERT_NDP(d.len >= 0); + ASSERT_NDP(d.len >= sizeof(*icmp6) + sizeof(*ns)); // - Target Address is not a multicast address. ASSERT_NDP(!rte_ipv6_addr_is_mcast(&ns->target)); @@ -125,3 +125,77 @@ GR_NODE_REGISTER(info); GR_DROP_REGISTER(ndp_ns_input_inval); GR_DROP_REGISTER(ndp_ns_input_drop); + +#ifdef __GROUT_UNIT_TEST__ + +#include "_cmocka.h" + +struct node_infos node_infos = STAILQ_HEAD_INITIALIZER(node_infos); + +int cq_callback_offset; +int cq_priv_offset; +mock_func(uint16_t, drop_packets(struct rte_graph *, struct rte_node *, void **, uint16_t)); +mock_func(int, drop_format(char *, size_t, const void *, size_t)); +mock_func(void *, gr_mbuf_trace_add(struct rte_mbuf *, struct rte_node *, size_t)); +mock_func(int, trace_icmp6_format(char *, size_t, const struct icmp6 *, size_t)); +mock_func(struct nexthop *, nexthop_lookup_l3(addr_family_t, uint16_t, uint16_t, const void *)); +mock_func(void, ndp_probe_input_cb(void *, uintptr_t, const struct control_queue_drain *)); + +struct fake_ndp_ns_mbuf { + struct icmp6 icmp6_hdr; + struct icmp6_neigh_solicit ns_hdr; + struct rte_mbuf mbuf; + uint8_t priv_data[GR_MBUF_PRIV_MAX_SIZE]; +}; + +static struct iface test_iface; + +static void init_default_ns_mbuf(struct fake_ndp_ns_mbuf *ndp_mbuf) { + memset(ndp_mbuf, 0, sizeof(*ndp_mbuf)); + + ndp_mbuf->icmp6_hdr.type = ICMP6_TYPE_NEIGH_SOLICIT; + ndp_mbuf->icmp6_hdr.code = 0; + ndp_mbuf->ns_hdr.target = (struct rte_ipv6_addr)RTE_IPV6( + 0xfe80, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x00aa + ); + + ndp_mbuf->mbuf.buf_addr = &ndp_mbuf->icmp6_hdr; + ndp_mbuf->mbuf.data_len = sizeof(struct icmp6) + sizeof(struct icmp6_neigh_solicit); + ndp_mbuf->mbuf.pkt_len = ndp_mbuf->mbuf.data_len; + ndp_mbuf->mbuf.next = NULL; + ndp_mbuf->mbuf.ol_flags = 0; + ndp_mbuf->mbuf.packet_type = RTE_PTYPE_L4_ICMP; + + ip6_local_mbuf_data(&ndp_mbuf->mbuf)->hop_limit = 255; + ip6_local_mbuf_data(&ndp_mbuf->mbuf)->len = ndp_mbuf->mbuf.data_len; + ip6_local_mbuf_data(&ndp_mbuf->mbuf)->src = (struct rte_ipv6_addr)RTE_IPV6( + 0xfe80, 0, 0, 0, 0, 0, 0, 0x00bb + ); + ip6_local_mbuf_data(&ndp_mbuf->mbuf)->dst = (struct rte_ipv6_addr)RTE_IPV6( + 0xfe80, 0, 0, 0, 0, 0, 0, 0x00aa + ); + ip6_local_mbuf_data(&ndp_mbuf->mbuf)->iface = &test_iface; +} + +// A solicitation carrying only the generic ICMPv6 header has no target address +// to read. RFC 4861 requires 24 octets. +static void ndp_ns_input_icmp_len_invalid(void **) { + struct fake_ndp_ns_mbuf ndp_mbuf; + void *obj = &ndp_mbuf.mbuf; + + init_default_ns_mbuf(&ndp_mbuf); + ip6_local_mbuf_data(obj)->len = GR_ICMP6_HDR_LEN; + + expect_uint_value(rte_node_enqueue_x1, next, INVAL); + + ndp_ns_input_process(NULL, NULL, &obj, 1); +} + +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(ndp_ns_input_icmp_len_invalid), + }; + return cmocka_run_group_tests(tests, NULL, NULL); +} + +#endif diff --git a/modules/l4/l4_input_local.c b/modules/l4/l4_input_local.c index 0442f7674..91c492f9a 100644 --- a/modules/l4/l4_input_local.c +++ b/modules/l4/l4_input_local.c @@ -15,6 +15,7 @@ LOG_TYPE("graph"); enum edges { MANAGEMENT = 0, BAD_PROTO, + BAD_LENGTH, EDGE_COUNT, }; @@ -101,12 +102,14 @@ static uint16_t l4_input_local_process( struct rte_udp_hdr *hdr; struct rte_mbuf *mbuf; rte_edge_t edge; + uint16_t avail; uint8_t proto; for (uint16_t i = 0; i < nb_objs; i++) { mbuf = objs[i]; edge = BAD_PROTO; proto = 0; + avail = rte_pktmbuf_data_len(mbuf); if (mbuf->packet_type & RTE_PTYPE_L3_IPV4) proto = ip_local_mbuf_data(mbuf)->proto; @@ -115,25 +118,36 @@ static uint16_t l4_input_local_process( else goto next; - if (proto != IPPROTO_UDP) { + switch (proto) { + case IPPROTO_UDP: + if (avail < sizeof(struct rte_udp_hdr)) { + edge = BAD_LENGTH; + goto next; + } + hdr = rte_pktmbuf_mtod(mbuf, struct rte_udp_hdr *); + edge = udp_edges[hdr->dst_port]; + break; + case IPPROTO_TCP: + if (avail < sizeof(struct rte_tcp_hdr)) { + edge = BAD_LENGTH; + goto next; + } edge = MANAGEMENT; - goto next; + break; + default: + edge = MANAGEMENT; + break; } - - hdr = rte_pktmbuf_mtod(mbuf, struct rte_udp_hdr *); - edge = udp_edges[hdr->dst_port]; next: if (gr_mbuf_is_traced(mbuf)) { struct l4_trace_data *t = gr_mbuf_trace_add(mbuf, node, sizeof(*t)); t->proto = proto; - switch (proto) { - case IPPROTO_UDP: + if (edge == BAD_LENGTH) + memset(&t->tcp, 0, sizeof(t->tcp)); // union, covers udp + else if (proto == IPPROTO_UDP) t->udp = *rte_pktmbuf_mtod(mbuf, struct rte_udp_hdr *); - break; - case IPPROTO_TCP: + else if (proto == IPPROTO_TCP) t->tcp = *rte_pktmbuf_mtod(mbuf, struct rte_tcp_hdr *); - break; - } } rte_node_enqueue_x1(graph, node, edge, mbuf); } @@ -153,6 +167,7 @@ static struct rte_node_register input_node = { .next_nodes = { [MANAGEMENT] = "l4_loopback_output", [BAD_PROTO] = "l4_bad_proto", + [BAD_LENGTH] = "l4_input_local_bad_length", }, }; @@ -164,3 +179,5 @@ static struct gr_node_info info = { }; GR_NODE_REGISTER(info); + +GR_DROP_REGISTER(l4_input_local_bad_length);