| // SPDX-License-Identifier: GPL-2.0 |
| #include <error.h> |
| #include <netinet/tcp.h> |
| #include <test_progs.h> |
| #include "sockmap_helpers.h" |
| #include "test_skmsg_load_helpers.skel.h" |
| #include "test_sockmap_strp.skel.h" |
| |
| #define STRP_PKT_HEAD_LEN 4 |
| #define STRP_PKT_BODY_LEN 6 |
| #define STRP_PKT_FULL_LEN (STRP_PKT_HEAD_LEN + STRP_PKT_BODY_LEN) |
| |
| static const char packet[STRP_PKT_FULL_LEN] = "head+body\0"; |
| static const int test_packet_num = 100; |
| |
| /* Current implementation of tcp_bpf_recvmsg_parser() invokes data_ready |
| * with sk held if an skb exists in sk_receive_queue. Then for the |
| * data_ready implementation of strparser, it will delay the read |
| * operation if sk is held and EAGAIN is returned. |
| */ |
| static int sockmap_strp_consume_pre_data(int p) |
| { |
| int recvd; |
| bool retried = false; |
| char rcv[10]; |
| |
| retry: |
| errno = 0; |
| recvd = recv_timeout(p, rcv, sizeof(rcv), 0, 1); |
| if (recvd < 0 && errno == EAGAIN && retried == false) { |
| /* On the first call, EAGAIN will certainly be returned. |
| * A 1-second wait is enough for the workqueue to finish. |
| */ |
| sleep(1); |
| retried = true; |
| goto retry; |
| } |
| |
| if (!ASSERT_EQ(recvd, STRP_PKT_FULL_LEN, "recv error or truncated data") || |
| !ASSERT_OK(memcmp(packet, rcv, STRP_PKT_FULL_LEN), |
| "data mismatch")) |
| return -1; |
| return 0; |
| } |
| |
| static struct test_sockmap_strp *sockmap_strp_init(int *out_map, bool pass, |
| bool need_parser) |
| { |
| struct test_sockmap_strp *strp = NULL; |
| int verdict, parser; |
| int err; |
| |
| strp = test_sockmap_strp__open_and_load(); |
| *out_map = bpf_map__fd(strp->maps.sock_map); |
| |
| if (need_parser) |
| parser = bpf_program__fd(strp->progs.prog_skb_parser_partial); |
| else |
| parser = bpf_program__fd(strp->progs.prog_skb_parser); |
| |
| if (pass) |
| verdict = bpf_program__fd(strp->progs.prog_skb_verdict_pass); |
| else |
| verdict = bpf_program__fd(strp->progs.prog_skb_verdict); |
| |
| err = bpf_prog_attach(parser, *out_map, BPF_SK_SKB_STREAM_PARSER, 0); |
| if (!ASSERT_OK(err, "bpf_prog_attach stream parser")) |
| goto err; |
| |
| err = bpf_prog_attach(verdict, *out_map, BPF_SK_SKB_STREAM_VERDICT, 0); |
| if (!ASSERT_OK(err, "bpf_prog_attach stream verdict")) |
| goto err; |
| |
| return strp; |
| err: |
| test_sockmap_strp__destroy(strp); |
| return NULL; |
| } |
| |
| /* Dispatch packets to different socket by packet size: |
| * |
| * ------ ------ |
| * | pkt4 || pkt1 |... > remote socket |
| * ------ ------ / ------ ------ |
| * | pkt8 | pkt7 |... |
| * ------ ------ \ ------ ------ |
| * | pkt3 || pkt2 |... > local socket |
| * ------ ------ |
| */ |
| static void test_sockmap_strp_dispatch_pkt(int family, int sotype) |
| { |
| int i, j, zero = 0, one = 1, recvd; |
| int err, map; |
| int c0 = -1, p0 = -1, c1 = -1, p1 = -1; |
| struct test_sockmap_strp *strp = NULL; |
| int test_cnt = 6; |
| char rcv[10]; |
| struct { |
| char data[7]; |
| int data_len; |
| int send_cnt; |
| int *receiver; |
| } send_dir[2] = { |
| /* data expected to deliver to local */ |
| {"llllll", 6, 0, &p0}, |
| /* data expected to deliver to remote */ |
| {"rrrrr", 5, 0, &c1} |
| }; |
| |
| strp = sockmap_strp_init(&map, false, false); |
| if (!ASSERT_TRUE(strp, "sockmap_strp_init")) |
| return; |
| |
| err = create_socket_pairs(family, sotype, &c0, &c1, &p0, &p1); |
| if (!ASSERT_OK(err, "create_socket_pairs()")) |
| goto out; |
| |
| err = bpf_map_update_elem(map, &zero, &p0, BPF_NOEXIST); |
| if (!ASSERT_OK(err, "bpf_map_update_elem(p0)")) |
| goto out_close; |
| |
| err = bpf_map_update_elem(map, &one, &p1, BPF_NOEXIST); |
| if (!ASSERT_OK(err, "bpf_map_update_elem(p1)")) |
| goto out_close; |
| |
| err = setsockopt(c1, IPPROTO_TCP, TCP_NODELAY, &zero, sizeof(zero)); |
| if (!ASSERT_OK(err, "setsockopt(TCP_NODELAY)")) |
| goto out_close; |
| |
| /* deliver data with data size greater than 5 to local */ |
| strp->data->verdict_max_size = 5; |
| |
| for (i = 0; i < test_cnt; i++) { |
| int d = i % 2; |
| |
| xsend(c0, send_dir[d].data, send_dir[d].data_len, 0); |
| send_dir[d].send_cnt++; |
| } |
| |
| for (i = 0; i < 2; i++) { |
| for (j = 0; j < send_dir[i].send_cnt; j++) { |
| int expected = send_dir[i].data_len; |
| |
| recvd = recv_timeout(*send_dir[i].receiver, rcv, |
| expected, MSG_DONTWAIT, |
| IO_TIMEOUT_SEC); |
| if (!ASSERT_EQ(recvd, expected, "recv_timeout()")) |
| goto out_close; |
| if (!ASSERT_OK(memcmp(send_dir[i].data, rcv, recvd), |
| "data mismatch")) |
| goto out_close; |
| } |
| } |
| out_close: |
| close(c0); |
| close(c1); |
| close(p0); |
| close(p1); |
| out: |
| test_sockmap_strp__destroy(strp); |
| } |
| |
| /* We have multiple packets in one skb |
| * ------------ ------------ ------------ |
| * | packet1 | packet2 | ... |
| * ------------ ------------ ------------ |
| */ |
| static void test_sockmap_strp_multiple_pkt(int family, int sotype) |
| { |
| int i, zero = 0; |
| int sent, recvd, total; |
| int err, map; |
| int c = -1, p = -1; |
| struct test_sockmap_strp *strp = NULL; |
| char *snd = NULL, *rcv = NULL; |
| |
| strp = sockmap_strp_init(&map, true, true); |
| if (!ASSERT_TRUE(strp, "sockmap_strp_init")) |
| return; |
| |
| err = create_pair(family, sotype, &c, &p); |
| if (err) |
| goto out; |
| |
| err = bpf_map_update_elem(map, &zero, &p, BPF_NOEXIST); |
| if (!ASSERT_OK(err, "bpf_map_update_elem(zero, p)")) |
| goto out_close; |
| |
| /* construct multiple packets in one buffer */ |
| total = test_packet_num * STRP_PKT_FULL_LEN; |
| snd = malloc(total); |
| rcv = malloc(total + 1); |
| if (!ASSERT_TRUE(snd, "malloc(snd)") || |
| !ASSERT_TRUE(rcv, "malloc(rcv)")) |
| goto out_close; |
| |
| for (i = 0; i < test_packet_num; i++) { |
| memcpy(snd + i * STRP_PKT_FULL_LEN, |
| packet, STRP_PKT_FULL_LEN); |
| } |
| |
| sent = xsend(c, snd, total, 0); |
| if (!ASSERT_EQ(sent, total, "xsend(c)")) |
| goto out_close; |
| |
| /* try to recv one more byte to avoid truncation check */ |
| recvd = recv_timeout(p, rcv, total + 1, MSG_DONTWAIT, IO_TIMEOUT_SEC); |
| if (!ASSERT_EQ(recvd, total, "recv(rcv)")) |
| goto out_close; |
| |
| /* we sent TCP segment with multiple encapsulation |
| * then check whether packets are handled correctly |
| */ |
| if (!ASSERT_OK(memcmp(snd, rcv, total), "data mismatch")) |
| goto out_close; |
| |
| out_close: |
| close(c); |
| close(p); |
| if (snd) |
| free(snd); |
| if (rcv) |
| free(rcv); |
| out: |
| test_sockmap_strp__destroy(strp); |
| } |
| |
| /* Test strparser with partial read */ |
| static void test_sockmap_strp_partial_read(int family, int sotype) |
| { |
| int zero = 0, recvd, off; |
| int err, map; |
| int c = -1, p = -1; |
| struct test_sockmap_strp *strp = NULL; |
| char rcv[STRP_PKT_FULL_LEN + 1] = "0"; |
| |
| strp = sockmap_strp_init(&map, true, true); |
| if (!ASSERT_TRUE(strp, "sockmap_strp_init")) |
| return; |
| |
| err = create_pair(family, sotype, &c, &p); |
| if (err) |
| goto out; |
| |
| /* sk_data_ready of 'p' will be replaced by strparser handler */ |
| err = bpf_map_update_elem(map, &zero, &p, BPF_NOEXIST); |
| if (!ASSERT_OK(err, "bpf_map_update_elem(zero, p)")) |
| goto out_close; |
| |
| /* 1.1 send partial head, 1 byte header left */ |
| off = STRP_PKT_HEAD_LEN - 1; |
| xsend(c, packet, off, 0); |
| recvd = recv_timeout(p, rcv, sizeof(rcv), MSG_DONTWAIT, 1); |
| if (!ASSERT_EQ(-1, recvd, "partial head sent, expected no data")) |
| goto out_close; |
| |
| /* 1.2 send remaining head and body */ |
| xsend(c, packet + off, STRP_PKT_FULL_LEN - off, 0); |
| recvd = recv_timeout(p, rcv, sizeof(rcv), MSG_DONTWAIT, IO_TIMEOUT_SEC); |
| if (!ASSERT_EQ(recvd, STRP_PKT_FULL_LEN, "expected full data")) |
| goto out_close; |
| |
| /* 2.1 send partial head, 1 byte header left */ |
| off = STRP_PKT_HEAD_LEN - 1; |
| xsend(c, packet, off, 0); |
| |
| /* 2.2 send remaining head and partial body, 1 byte body left */ |
| xsend(c, packet + off, STRP_PKT_FULL_LEN - off - 1, 0); |
| off = STRP_PKT_FULL_LEN - 1; |
| recvd = recv_timeout(p, rcv, sizeof(rcv), MSG_DONTWAIT, 1); |
| if (!ASSERT_EQ(-1, recvd, "partial body sent, expected no data")) |
| goto out_close; |
| |
| /* 2.3 send remaining body */ |
| xsend(c, packet + off, STRP_PKT_FULL_LEN - off, 0); |
| recvd = recv_timeout(p, rcv, sizeof(rcv), MSG_DONTWAIT, IO_TIMEOUT_SEC); |
| if (!ASSERT_EQ(recvd, STRP_PKT_FULL_LEN, "expected full data")) |
| goto out_close; |
| |
| out_close: |
| close(c); |
| close(p); |
| |
| out: |
| test_sockmap_strp__destroy(strp); |
| } |
| |
| /* Test simple socket read/write with strparser + FIONREAD */ |
| static void test_sockmap_strp_pass(int family, int sotype, bool fionread) |
| { |
| int zero = 0, pkt_size = STRP_PKT_FULL_LEN, sent, recvd, avail; |
| int err, map; |
| int c = -1, p = -1; |
| int test_cnt = 10, i; |
| struct test_sockmap_strp *strp = NULL; |
| char rcv[STRP_PKT_FULL_LEN + 1] = "0"; |
| |
| strp = sockmap_strp_init(&map, true, true); |
| if (!ASSERT_TRUE(strp, "sockmap_strp_init")) |
| return; |
| |
| err = create_pair(family, sotype, &c, &p); |
| if (err) |
| goto out; |
| |
| /* inject some data before bpf process, it should be read |
| * correctly because we check sk_receive_queue in |
| * tcp_bpf_recvmsg_parser(). |
| */ |
| sent = xsend(c, packet, pkt_size, 0); |
| if (!ASSERT_EQ(sent, pkt_size, "xsend(pre-data)")) |
| goto out_close; |
| |
| /* sk_data_ready of 'p' will be replaced by strparser handler */ |
| err = bpf_map_update_elem(map, &zero, &p, BPF_NOEXIST); |
| if (!ASSERT_OK(err, "bpf_map_update_elem(p)")) |
| goto out_close; |
| |
| /* consume previous data we injected */ |
| if (sockmap_strp_consume_pre_data(p)) |
| goto out_close; |
| |
| /* Previously, we encountered issues such as deadlocks and |
| * sequence errors that resulted in the inability to read |
| * continuously. Therefore, we perform multiple iterations |
| * of testing here. |
| */ |
| for (i = 0; i < test_cnt; i++) { |
| sent = xsend(c, packet, pkt_size, 0); |
| if (!ASSERT_EQ(sent, pkt_size, "xsend(c)")) |
| goto out_close; |
| |
| recvd = recv_timeout(p, rcv, sizeof(rcv), MSG_DONTWAIT, |
| IO_TIMEOUT_SEC); |
| if (!ASSERT_EQ(recvd, pkt_size, "recv_timeout(p)") || |
| !ASSERT_OK(memcmp(packet, rcv, pkt_size), |
| "memcmp, data mismatch")) |
| goto out_close; |
| } |
| |
| if (fionread) { |
| sent = xsend(c, packet, pkt_size, 0); |
| if (!ASSERT_EQ(sent, pkt_size, "second xsend(c)")) |
| goto out_close; |
| |
| err = ioctl(p, FIONREAD, &avail); |
| if (!ASSERT_OK(err, "ioctl(FIONREAD) error") || |
| !ASSERT_EQ(avail, pkt_size, "ioctl(FIONREAD)")) |
| goto out_close; |
| |
| recvd = recv_timeout(p, rcv, sizeof(rcv), MSG_DONTWAIT, |
| IO_TIMEOUT_SEC); |
| if (!ASSERT_EQ(recvd, pkt_size, "second recv_timeout(p)") || |
| !ASSERT_OK(memcmp(packet, rcv, pkt_size), |
| "second memcmp, data mismatch")) |
| goto out_close; |
| } |
| |
| out_close: |
| close(c); |
| close(p); |
| |
| out: |
| test_sockmap_strp__destroy(strp); |
| } |
| |
| /* Test strparser with verdict mode */ |
| static void test_sockmap_strp_verdict(int family, int sotype) |
| { |
| int zero = 0, one = 1, sent, recvd, off; |
| int err, map; |
| int c0 = -1, p0 = -1, c1 = -1, p1 = -1; |
| struct test_sockmap_strp *strp = NULL; |
| char rcv[STRP_PKT_FULL_LEN + 1] = "0"; |
| |
| strp = sockmap_strp_init(&map, false, true); |
| if (!ASSERT_TRUE(strp, "sockmap_strp_init")) |
| return; |
| |
| /* We simulate a reverse proxy server. |
| * When p0 receives data from c0, we forward it to c1. |
| * From c1's perspective, it will consider this data |
| * as being sent by p1. |
| */ |
| err = create_socket_pairs(family, sotype, &c0, &c1, &p0, &p1); |
| if (!ASSERT_OK(err, "create_socket_pairs()")) |
| goto out; |
| |
| err = bpf_map_update_elem(map, &zero, &p0, BPF_NOEXIST); |
| if (!ASSERT_OK(err, "bpf_map_update_elem(p0)")) |
| goto out_close; |
| |
| err = bpf_map_update_elem(map, &one, &p1, BPF_NOEXIST); |
| if (!ASSERT_OK(err, "bpf_map_update_elem(p1)")) |
| goto out_close; |
| |
| sent = xsend(c0, packet, STRP_PKT_FULL_LEN, 0); |
| if (!ASSERT_EQ(sent, STRP_PKT_FULL_LEN, "xsend(c0)")) |
| goto out_close; |
| |
| recvd = recv_timeout(c1, rcv, sizeof(rcv), MSG_DONTWAIT, |
| IO_TIMEOUT_SEC); |
| if (!ASSERT_EQ(recvd, STRP_PKT_FULL_LEN, "recv_timeout(c1)") || |
| !ASSERT_OK(memcmp(packet, rcv, STRP_PKT_FULL_LEN), |
| "received data does not match the sent data")) |
| goto out_close; |
| |
| /* send again to ensure the stream is functioning correctly. */ |
| sent = xsend(c0, packet, STRP_PKT_FULL_LEN, 0); |
| if (!ASSERT_EQ(sent, STRP_PKT_FULL_LEN, "second xsend(c0)")) |
| goto out_close; |
| |
| /* partial read */ |
| off = STRP_PKT_FULL_LEN / 2; |
| recvd = recv_timeout(c1, rcv, off, MSG_DONTWAIT, |
| IO_TIMEOUT_SEC); |
| recvd += recv_timeout(c1, rcv + off, sizeof(rcv) - off, MSG_DONTWAIT, |
| IO_TIMEOUT_SEC); |
| |
| if (!ASSERT_EQ(recvd, STRP_PKT_FULL_LEN, "partial recv_timeout(c1)") || |
| !ASSERT_OK(memcmp(packet, rcv, STRP_PKT_FULL_LEN), |
| "partial received data does not match the sent data")) |
| goto out_close; |
| |
| out_close: |
| close(c0); |
| close(c1); |
| close(p0); |
| close(p1); |
| out: |
| test_sockmap_strp__destroy(strp); |
| } |
| |
| void test_sockmap_strp(void) |
| { |
| if (test__start_subtest("sockmap strp tcp pass")) |
| test_sockmap_strp_pass(AF_INET, SOCK_STREAM, false); |
| if (test__start_subtest("sockmap strp tcp v6 pass")) |
| test_sockmap_strp_pass(AF_INET6, SOCK_STREAM, false); |
| if (test__start_subtest("sockmap strp tcp pass fionread")) |
| test_sockmap_strp_pass(AF_INET, SOCK_STREAM, true); |
| if (test__start_subtest("sockmap strp tcp v6 pass fionread")) |
| test_sockmap_strp_pass(AF_INET6, SOCK_STREAM, true); |
| if (test__start_subtest("sockmap strp tcp verdict")) |
| test_sockmap_strp_verdict(AF_INET, SOCK_STREAM); |
| if (test__start_subtest("sockmap strp tcp v6 verdict")) |
| test_sockmap_strp_verdict(AF_INET6, SOCK_STREAM); |
| if (test__start_subtest("sockmap strp tcp partial read")) |
| test_sockmap_strp_partial_read(AF_INET, SOCK_STREAM); |
| if (test__start_subtest("sockmap strp tcp multiple packets")) |
| test_sockmap_strp_multiple_pkt(AF_INET, SOCK_STREAM); |
| if (test__start_subtest("sockmap strp tcp dispatch")) |
| test_sockmap_strp_dispatch_pkt(AF_INET, SOCK_STREAM); |
| } |