selftests/bpf: Verify that the cgroup_skb filters receive expected packets.

This test case includes four scenarios:

1. Connect to the server from outside the cgroup and close the connection
   from outside the cgroup.
2. Connect to the server from outside the cgroup and close the connection
   from inside the cgroup.
3. Connect to the server from inside the cgroup and close the connection
   from outside the cgroup.
4. Connect to the server from inside the cgroup and close the connection
   from inside the cgroup.

The test case is to verify that cgroup_skb/{egress, ingress} filters
receive expected packets including SYN, SYN/ACK, ACK, FIN, and FIN/ACK.

Signed-off-by: Kui-Feng Lee <kuifeng@meta.com>
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
Link: https://lore.kernel.org/bpf/20230624014600.576756-3-kuifeng@meta.com
This commit is contained in:
Kui-Feng Lee 2023-06-23 18:46:00 -07:00 committed by Daniel Borkmann
parent 223f5f79f2
commit 539c7e67aa
5 changed files with 832 additions and 0 deletions

View file

@ -277,6 +277,18 @@ int join_cgroup(const char *relative_path)
return join_cgroup_from_top(cgroup_path);
}
/**
* join_root_cgroup() - Join the root cgroup
*
* This function joins the root cgroup.
*
* On success, it returns 0, otherwise on failure it returns 1.
*/
int join_root_cgroup(void)
{
return join_cgroup_from_top(CGROUP_MOUNT_PATH);
}
/**
* join_parent_cgroup() - Join a cgroup in the parent process workdir
* @relative_path: The cgroup path, relative to parent process workdir, to join

View file

@ -22,6 +22,7 @@ void remove_cgroup(const char *relative_path);
unsigned long long get_cgroup_id(const char *relative_path);
int join_cgroup(const char *relative_path);
int join_root_cgroup(void);
int join_parent_cgroup(const char *relative_path);
int setup_cgroup_environment(void);

View file

@ -0,0 +1,35 @@
/* SPDX-License-Identifier: GPL-2.0 */
/* Copyright (c) 2023 Meta Platforms, Inc. and affiliates. */
/* Define states of a socket to tracking messages sending to and from the
* socket.
*
* These states are based on rfc9293 with some modifications to support
* tracking of messages sent out from a socket. For example, when a SYN is
* received, a new socket is transiting to the SYN_RECV state defined in
* rfc9293. But, we put it in SYN_RECV_SENDING_SYN_ACK state and when
* SYN-ACK is sent out, it moves to SYN_RECV state. With this modification,
* we can track the message sent out from a socket.
*/
#ifndef __CGROUP_TCP_SKB_H__
#define __CGROUP_TCP_SKB_H__
enum {
INIT,
CLOSED,
SYN_SENT,
SYN_RECV_SENDING_SYN_ACK,
SYN_RECV,
ESTABLISHED,
FIN_WAIT1,
FIN_WAIT2,
CLOSE_WAIT_SENDING_ACK,
CLOSE_WAIT,
CLOSING,
LAST_ACK,
TIME_WAIT_SENDING_ACK,
TIME_WAIT,
};
#endif /* __CGROUP_TCP_SKB_H__ */

View file

@ -0,0 +1,402 @@
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2023 Facebook */
#include <test_progs.h>
#include <linux/in6.h>
#include <sys/socket.h>
#include <sched.h>
#include <unistd.h>
#include "cgroup_helpers.h"
#include "testing_helpers.h"
#include "cgroup_tcp_skb.skel.h"
#include "cgroup_tcp_skb.h"
#define CGROUP_TCP_SKB_PATH "/test_cgroup_tcp_skb"
static int install_filters(int cgroup_fd,
struct bpf_link **egress_link,
struct bpf_link **ingress_link,
struct bpf_program *egress_prog,
struct bpf_program *ingress_prog,
struct cgroup_tcp_skb *skel)
{
/* Prepare filters */
skel->bss->g_sock_state = 0;
skel->bss->g_unexpected = 0;
*egress_link =
bpf_program__attach_cgroup(egress_prog,
cgroup_fd);
if (!ASSERT_OK_PTR(egress_link, "egress_link"))
return -1;
*ingress_link =
bpf_program__attach_cgroup(ingress_prog,
cgroup_fd);
if (!ASSERT_OK_PTR(ingress_link, "ingress_link"))
return -1;
return 0;
}
static void uninstall_filters(struct bpf_link **egress_link,
struct bpf_link **ingress_link)
{
bpf_link__destroy(*egress_link);
*egress_link = NULL;
bpf_link__destroy(*ingress_link);
*ingress_link = NULL;
}
static int create_client_sock_v6(void)
{
int fd;
fd = socket(AF_INET6, SOCK_STREAM, 0);
if (fd < 0) {
perror("socket");
return -1;
}
return fd;
}
static int create_server_sock_v6(void)
{
struct sockaddr_in6 addr = {
.sin6_family = AF_INET6,
.sin6_port = htons(0),
.sin6_addr = IN6ADDR_LOOPBACK_INIT,
};
int fd, err;
fd = socket(AF_INET6, SOCK_STREAM, 0);
if (fd < 0) {
perror("socket");
return -1;
}
err = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
if (err < 0) {
perror("bind");
return -1;
}
err = listen(fd, 1);
if (err < 0) {
perror("listen");
return -1;
}
return fd;
}
static int get_sock_port_v6(int fd)
{
struct sockaddr_in6 addr;
socklen_t len;
int err;
len = sizeof(addr);
err = getsockname(fd, (struct sockaddr *)&addr, &len);
if (err < 0) {
perror("getsockname");
return -1;
}
return ntohs(addr.sin6_port);
}
static int connect_client_server_v6(int client_fd, int listen_fd)
{
struct sockaddr_in6 addr = {
.sin6_family = AF_INET6,
.sin6_addr = IN6ADDR_LOOPBACK_INIT,
};
int err;
addr.sin6_port = htons(get_sock_port_v6(listen_fd));
if (addr.sin6_port < 0)
return -1;
err = connect(client_fd, (struct sockaddr *)&addr, sizeof(addr));
if (err < 0) {
perror("connect");
return -1;
}
return 0;
}
/* Connect to the server in a cgroup from the outside of the cgroup. */
static int talk_to_cgroup(int *client_fd, int *listen_fd, int *service_fd,
struct cgroup_tcp_skb *skel)
{
int err, cp;
char buf[5];
/* Create client & server socket */
err = join_root_cgroup();
if (!ASSERT_OK(err, "join_root_cgroup"))
return -1;
*client_fd = create_client_sock_v6();
if (!ASSERT_GE(*client_fd, 0, "client_fd"))
return -1;
err = join_cgroup(CGROUP_TCP_SKB_PATH);
if (!ASSERT_OK(err, "join_cgroup"))
return -1;
*listen_fd = create_server_sock_v6();
if (!ASSERT_GE(*listen_fd, 0, "listen_fd"))
return -1;
skel->bss->g_sock_port = get_sock_port_v6(*listen_fd);
/* Connect client to server */
err = connect_client_server_v6(*client_fd, *listen_fd);
if (!ASSERT_OK(err, "connect_client_server_v6"))
return -1;
*service_fd = accept(*listen_fd, NULL, NULL);
if (!ASSERT_GE(*service_fd, 0, "service_fd"))
return -1;
err = join_root_cgroup();
if (!ASSERT_OK(err, "join_root_cgroup"))
return -1;
cp = write(*client_fd, "hello", 5);
if (!ASSERT_EQ(cp, 5, "write"))
return -1;
cp = read(*service_fd, buf, 5);
if (!ASSERT_EQ(cp, 5, "read"))
return -1;
return 0;
}
/* Connect to the server out of a cgroup from inside the cgroup. */
static int talk_to_outside(int *client_fd, int *listen_fd, int *service_fd,
struct cgroup_tcp_skb *skel)
{
int err, cp;
char buf[5];
/* Create client & server socket */
err = join_root_cgroup();
if (!ASSERT_OK(err, "join_root_cgroup"))
return -1;
*listen_fd = create_server_sock_v6();
if (!ASSERT_GE(*listen_fd, 0, "listen_fd"))
return -1;
err = join_cgroup(CGROUP_TCP_SKB_PATH);
if (!ASSERT_OK(err, "join_cgroup"))
return -1;
*client_fd = create_client_sock_v6();
if (!ASSERT_GE(*client_fd, 0, "client_fd"))
return -1;
err = join_root_cgroup();
if (!ASSERT_OK(err, "join_root_cgroup"))
return -1;
skel->bss->g_sock_port = get_sock_port_v6(*listen_fd);
/* Connect client to server */
err = connect_client_server_v6(*client_fd, *listen_fd);
if (!ASSERT_OK(err, "connect_client_server_v6"))
return -1;
*service_fd = accept(*listen_fd, NULL, NULL);
if (!ASSERT_GE(*service_fd, 0, "service_fd"))
return -1;
cp = write(*client_fd, "hello", 5);
if (!ASSERT_EQ(cp, 5, "write"))
return -1;
cp = read(*service_fd, buf, 5);
if (!ASSERT_EQ(cp, 5, "read"))
return -1;
return 0;
}
static int close_connection(int *closing_fd, int *peer_fd, int *listen_fd,
struct cgroup_tcp_skb *skel)
{
__u32 saved_packet_count = 0;
int err;
int i;
/* Wait for ACKs to be sent */
saved_packet_count = skel->bss->g_packet_count;
usleep(100000); /* 0.1s */
for (i = 0;
skel->bss->g_packet_count != saved_packet_count && i < 10;
i++) {
saved_packet_count = skel->bss->g_packet_count;
usleep(100000); /* 0.1s */
}
if (!ASSERT_EQ(skel->bss->g_packet_count, saved_packet_count,
"packet_count"))
return -1;
skel->bss->g_packet_count = 0;
saved_packet_count = 0;
/* Half shutdown to make sure the closing socket having a chance to
* receive a FIN from the peer.
*/
err = shutdown(*closing_fd, SHUT_WR);
if (!ASSERT_OK(err, "shutdown closing_fd"))
return -1;
/* Wait for FIN and the ACK of the FIN to be observed */
for (i = 0;
skel->bss->g_packet_count < saved_packet_count + 2 && i < 10;
i++)
usleep(100000); /* 0.1s */
if (!ASSERT_GE(skel->bss->g_packet_count, saved_packet_count + 2,
"packet_count"))
return -1;
saved_packet_count = skel->bss->g_packet_count;
/* Fully shutdown the connection */
err = close(*peer_fd);
if (!ASSERT_OK(err, "close peer_fd"))
return -1;
*peer_fd = -1;
/* Wait for FIN and the ACK of the FIN to be observed */
for (i = 0;
skel->bss->g_packet_count < saved_packet_count + 2 && i < 10;
i++)
usleep(100000); /* 0.1s */
if (!ASSERT_GE(skel->bss->g_packet_count, saved_packet_count + 2,
"packet_count"))
return -1;
err = close(*closing_fd);
if (!ASSERT_OK(err, "close closing_fd"))
return -1;
*closing_fd = -1;
close(*listen_fd);
*listen_fd = -1;
return 0;
}
/* This test case includes four scenarios:
* 1. Connect to the server from outside the cgroup and close the connection
* from outside the cgroup.
* 2. Connect to the server from outside the cgroup and close the connection
* from inside the cgroup.
* 3. Connect to the server from inside the cgroup and close the connection
* from outside the cgroup.
* 4. Connect to the server from inside the cgroup and close the connection
* from inside the cgroup.
*
* The test case is to verify that cgroup_skb/{egress,ingress} filters
* receive expected packets including SYN, SYN/ACK, ACK, FIN, and FIN/ACK.
*/
void test_cgroup_tcp_skb(void)
{
struct bpf_link *ingress_link = NULL;
struct bpf_link *egress_link = NULL;
int client_fd = -1, listen_fd = -1;
struct cgroup_tcp_skb *skel;
int service_fd = -1;
int cgroup_fd = -1;
int err;
skel = cgroup_tcp_skb__open_and_load();
if (!ASSERT_OK(!skel, "skel_open_load"))
return;
err = setup_cgroup_environment();
if (!ASSERT_OK(err, "setup_cgroup_environment"))
goto cleanup;
cgroup_fd = create_and_get_cgroup(CGROUP_TCP_SKB_PATH);
if (!ASSERT_GE(cgroup_fd, 0, "cgroup_fd"))
goto cleanup;
/* Scenario 1 */
err = install_filters(cgroup_fd, &egress_link, &ingress_link,
skel->progs.server_egress,
skel->progs.server_ingress,
skel);
if (!ASSERT_OK(err, "install_filters"))
goto cleanup;
err = talk_to_cgroup(&client_fd, &listen_fd, &service_fd, skel);
if (!ASSERT_OK(err, "talk_to_cgroup"))
goto cleanup;
err = close_connection(&client_fd, &service_fd, &listen_fd, skel);
if (!ASSERT_OK(err, "close_connection"))
goto cleanup;
ASSERT_EQ(skel->bss->g_unexpected, 0, "g_unexpected");
ASSERT_EQ(skel->bss->g_sock_state, CLOSED, "g_sock_state");
uninstall_filters(&egress_link, &ingress_link);
/* Scenario 2 */
err = install_filters(cgroup_fd, &egress_link, &ingress_link,
skel->progs.server_egress_srv,
skel->progs.server_ingress_srv,
skel);
err = talk_to_cgroup(&client_fd, &listen_fd, &service_fd, skel);
if (!ASSERT_OK(err, "talk_to_cgroup"))
goto cleanup;
err = close_connection(&service_fd, &client_fd, &listen_fd, skel);
if (!ASSERT_OK(err, "close_connection"))
goto cleanup;
ASSERT_EQ(skel->bss->g_unexpected, 0, "g_unexpected");
ASSERT_EQ(skel->bss->g_sock_state, TIME_WAIT, "g_sock_state");
uninstall_filters(&egress_link, &ingress_link);
/* Scenario 3 */
err = install_filters(cgroup_fd, &egress_link, &ingress_link,
skel->progs.client_egress_srv,
skel->progs.client_ingress_srv,
skel);
err = talk_to_outside(&client_fd, &listen_fd, &service_fd, skel);
if (!ASSERT_OK(err, "talk_to_outside"))
goto cleanup;
err = close_connection(&service_fd, &client_fd, &listen_fd, skel);
if (!ASSERT_OK(err, "close_connection"))
goto cleanup;
ASSERT_EQ(skel->bss->g_unexpected, 0, "g_unexpected");
ASSERT_EQ(skel->bss->g_sock_state, CLOSED, "g_sock_state");
uninstall_filters(&egress_link, &ingress_link);
/* Scenario 4 */
err = install_filters(cgroup_fd, &egress_link, &ingress_link,
skel->progs.client_egress,
skel->progs.client_ingress,
skel);
err = talk_to_outside(&client_fd, &listen_fd, &service_fd, skel);
if (!ASSERT_OK(err, "talk_to_outside"))
goto cleanup;
err = close_connection(&client_fd, &service_fd, &listen_fd, skel);
if (!ASSERT_OK(err, "close_connection"))
goto cleanup;
ASSERT_EQ(skel->bss->g_unexpected, 0, "g_unexpected");
ASSERT_EQ(skel->bss->g_sock_state, TIME_WAIT, "g_sock_state");
uninstall_filters(&egress_link, &ingress_link);
cleanup:
close(client_fd);
close(listen_fd);
close(service_fd);
close(cgroup_fd);
bpf_link__destroy(egress_link);
bpf_link__destroy(ingress_link);
cleanup_cgroup_environment();
cgroup_tcp_skb__destroy(skel);
}

View file

@ -0,0 +1,382 @@
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2023 Meta Platforms, Inc. and affiliates. */
#include <linux/bpf.h>
#include <bpf/bpf_endian.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/in.h>
#include <linux/in6.h>
#include <linux/ipv6.h>
#include <linux/tcp.h>
#include <sys/types.h>
#include <sys/socket.h>
#include "cgroup_tcp_skb.h"
char _license[] SEC("license") = "GPL";
__u16 g_sock_port = 0;
__u32 g_sock_state = 0;
int g_unexpected = 0;
__u32 g_packet_count = 0;
int needed_tcp_pkt(struct __sk_buff *skb, struct tcphdr *tcph)
{
struct ipv6hdr ip6h;
if (skb->protocol != bpf_htons(ETH_P_IPV6))
return 0;
if (bpf_skb_load_bytes(skb, 0, &ip6h, sizeof(ip6h)))
return 0;
if (ip6h.nexthdr != IPPROTO_TCP)
return 0;
if (bpf_skb_load_bytes(skb, sizeof(ip6h), tcph, sizeof(*tcph)))
return 0;
if (tcph->source != bpf_htons(g_sock_port) &&
tcph->dest != bpf_htons(g_sock_port))
return 0;
return 1;
}
/* Run accept() on a socket in the cgroup to receive a new connection. */
static int egress_accept(struct tcphdr *tcph)
{
if (g_sock_state == SYN_RECV_SENDING_SYN_ACK) {
if (tcph->fin || !tcph->syn || !tcph->ack)
g_unexpected++;
else
g_sock_state = SYN_RECV;
return 1;
}
return 0;
}
static int ingress_accept(struct tcphdr *tcph)
{
switch (g_sock_state) {
case INIT:
if (!tcph->syn || tcph->fin || tcph->ack)
g_unexpected++;
else
g_sock_state = SYN_RECV_SENDING_SYN_ACK;
break;
case SYN_RECV:
if (tcph->fin || tcph->syn || !tcph->ack)
g_unexpected++;
else
g_sock_state = ESTABLISHED;
break;
default:
return 0;
}
return 1;
}
/* Run connect() on a socket in the cgroup to start a new connection. */
static int egress_connect(struct tcphdr *tcph)
{
if (g_sock_state == INIT) {
if (!tcph->syn || tcph->fin || tcph->ack)
g_unexpected++;
else
g_sock_state = SYN_SENT;
return 1;
}
return 0;
}
static int ingress_connect(struct tcphdr *tcph)
{
if (g_sock_state == SYN_SENT) {
if (tcph->fin || !tcph->syn || !tcph->ack)
g_unexpected++;
else
g_sock_state = ESTABLISHED;
return 1;
}
return 0;
}
/* The connection is closed by the peer outside the cgroup. */
static int egress_close_remote(struct tcphdr *tcph)
{
switch (g_sock_state) {
case ESTABLISHED:
break;
case CLOSE_WAIT_SENDING_ACK:
if (tcph->fin || tcph->syn || !tcph->ack)
g_unexpected++;
else
g_sock_state = CLOSE_WAIT;
break;
case CLOSE_WAIT:
if (!tcph->fin)
g_unexpected++;
else
g_sock_state = LAST_ACK;
break;
default:
return 0;
}
return 1;
}
static int ingress_close_remote(struct tcphdr *tcph)
{
switch (g_sock_state) {
case ESTABLISHED:
if (tcph->fin)
g_sock_state = CLOSE_WAIT_SENDING_ACK;
break;
case LAST_ACK:
if (tcph->fin || tcph->syn || !tcph->ack)
g_unexpected++;
else
g_sock_state = CLOSED;
break;
default:
return 0;
}
return 1;
}
/* The connection is closed by the endpoint inside the cgroup. */
static int egress_close_local(struct tcphdr *tcph)
{
switch (g_sock_state) {
case ESTABLISHED:
if (tcph->fin)
g_sock_state = FIN_WAIT1;
break;
case TIME_WAIT_SENDING_ACK:
if (tcph->fin || tcph->syn || !tcph->ack)
g_unexpected++;
else
g_sock_state = TIME_WAIT;
break;
default:
return 0;
}
return 1;
}
static int ingress_close_local(struct tcphdr *tcph)
{
switch (g_sock_state) {
case ESTABLISHED:
break;
case FIN_WAIT1:
if (tcph->fin || tcph->syn || !tcph->ack)
g_unexpected++;
else
g_sock_state = FIN_WAIT2;
break;
case FIN_WAIT2:
if (!tcph->fin || tcph->syn || !tcph->ack)
g_unexpected++;
else
g_sock_state = TIME_WAIT_SENDING_ACK;
break;
default:
return 0;
}
return 1;
}
/* Check the types of outgoing packets of a server socket to make sure they
* are consistent with the state of the server socket.
*
* The connection is closed by the client side.
*/
SEC("cgroup_skb/egress")
int server_egress(struct __sk_buff *skb)
{
struct tcphdr tcph;
if (!needed_tcp_pkt(skb, &tcph))
return 1;
g_packet_count++;
/* Egress of the server socket. */
if (egress_accept(&tcph) || egress_close_remote(&tcph))
return 1;
g_unexpected++;
return 1;
}
/* Check the types of incoming packets of a server socket to make sure they
* are consistent with the state of the server socket.
*
* The connection is closed by the client side.
*/
SEC("cgroup_skb/ingress")
int server_ingress(struct __sk_buff *skb)
{
struct tcphdr tcph;
if (!needed_tcp_pkt(skb, &tcph))
return 1;
g_packet_count++;
/* Ingress of the server socket. */
if (ingress_accept(&tcph) || ingress_close_remote(&tcph))
return 1;
g_unexpected++;
return 1;
}
/* Check the types of outgoing packets of a server socket to make sure they
* are consistent with the state of the server socket.
*
* The connection is closed by the server side.
*/
SEC("cgroup_skb/egress")
int server_egress_srv(struct __sk_buff *skb)
{
struct tcphdr tcph;
if (!needed_tcp_pkt(skb, &tcph))
return 1;
g_packet_count++;
/* Egress of the server socket. */
if (egress_accept(&tcph) || egress_close_local(&tcph))
return 1;
g_unexpected++;
return 1;
}
/* Check the types of incoming packets of a server socket to make sure they
* are consistent with the state of the server socket.
*
* The connection is closed by the server side.
*/
SEC("cgroup_skb/ingress")
int server_ingress_srv(struct __sk_buff *skb)
{
struct tcphdr tcph;
if (!needed_tcp_pkt(skb, &tcph))
return 1;
g_packet_count++;
/* Ingress of the server socket. */
if (ingress_accept(&tcph) || ingress_close_local(&tcph))
return 1;
g_unexpected++;
return 1;
}
/* Check the types of outgoing packets of a client socket to make sure they
* are consistent with the state of the client socket.
*
* The connection is closed by the server side.
*/
SEC("cgroup_skb/egress")
int client_egress_srv(struct __sk_buff *skb)
{
struct tcphdr tcph;
if (!needed_tcp_pkt(skb, &tcph))
return 1;
g_packet_count++;
/* Egress of the server socket. */
if (egress_connect(&tcph) || egress_close_remote(&tcph))
return 1;
g_unexpected++;
return 1;
}
/* Check the types of incoming packets of a client socket to make sure they
* are consistent with the state of the client socket.
*
* The connection is closed by the server side.
*/
SEC("cgroup_skb/ingress")
int client_ingress_srv(struct __sk_buff *skb)
{
struct tcphdr tcph;
if (!needed_tcp_pkt(skb, &tcph))
return 1;
g_packet_count++;
/* Ingress of the server socket. */
if (ingress_connect(&tcph) || ingress_close_remote(&tcph))
return 1;
g_unexpected++;
return 1;
}
/* Check the types of outgoing packets of a client socket to make sure they
* are consistent with the state of the client socket.
*
* The connection is closed by the client side.
*/
SEC("cgroup_skb/egress")
int client_egress(struct __sk_buff *skb)
{
struct tcphdr tcph;
if (!needed_tcp_pkt(skb, &tcph))
return 1;
g_packet_count++;
/* Egress of the server socket. */
if (egress_connect(&tcph) || egress_close_local(&tcph))
return 1;
g_unexpected++;
return 1;
}
/* Check the types of incoming packets of a client socket to make sure they
* are consistent with the state of the client socket.
*
* The connection is closed by the client side.
*/
SEC("cgroup_skb/ingress")
int client_ingress(struct __sk_buff *skb)
{
struct tcphdr tcph;
if (!needed_tcp_pkt(skb, &tcph))
return 1;
g_packet_count++;
/* Ingress of the server socket. */
if (ingress_connect(&tcph) || ingress_close_local(&tcph))
return 1;
g_unexpected++;
return 1;
}