Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ jobs:
libibverbs-dev libasan8 libcmocka-dev libedit-dev libarchive-dev \
libevent-dev libmnl-dev libnuma-dev python3-pyelftools \
socat tcpdump traceroute graphviz iproute2 iputils-ping ndisc6 jq \
dnsmasq systemd-coredump \
dnsmasq systemd-coredump libpcap-dev libbpf-dev \
"linux-modules-extra-$(uname -r)"
if echo $MESON_EXTRA_OPTS | grep -q frr=enabled ; then
sudo apt-get install -qy --no-install-recommends \
Expand Down Expand Up @@ -148,7 +148,8 @@ jobs:
apt install -qy --no-install-recommends \
make gcc ccache git meson scdoc python3-pyelftools ca-certificates pkg-config \
crossbuild-essential-arm64 libcmocka-dev:arm64 libedit-dev:arm64 \
libevent-dev:arm64 libmnl-dev:arm64 libnuma-dev:arm64
libevent-dev:arm64 libmnl-dev:arm64 libnuma-dev:arm64 \
libpcap-dev:arm64 libbpf-dev:arm64
- uses: actions/checkout@v4
with:
persist-credentials: false
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
libjson-c-dev libmnl-dev libnuma-dev libprotobuf-c-dev libreadline-dev \
librtr-dev libtool libyang-dev meson ninja-build \
patch pkg-config protobuf-c-compiler python3-dev python3-pyelftools \
systemd-dev texinfo
systemd-dev texinfo libpcap-dev libbpf-dev
- uses: actions/checkout@v4
with:
fetch-depth: 0 # force fetch all history
Expand Down Expand Up @@ -99,7 +99,7 @@ jobs:
libyang-devel libtool make meson ninja-build \
numactl-devel pkgconf protobuf-c-compiler protobuf-c-devel \
python3-pyelftools python3-devel rdma-core-devel readline-devel \
rpm-build scl-utils systemd
rpm-build scl-utils systemd libpcap-devel libbpf-devel
- uses: actions/checkout@v4
with:
fetch-depth: 0 # force fetch all history
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ image.
dnf install git gcc make meson ninja-build pkgconf \
python3-pyelftools scdoc libmnl-devel \
libcmocka-devel libedit-devel libevent-devel numactl-devel \
libarchive-devel rdma-core-devel
libarchive-devel rdma-core-devel libpcap-devel libbpf-devel
```

or
Expand All @@ -270,7 +270,7 @@ or
apt install git gcc make meson ninja-build pkgconf \
python3-pyelftools scdoc \
libcmocka-dev libedit-dev libevent-dev libnuma-dev libmnl-dev \
libarchive-dev libibverbs-dev
libarchive-dev libibverbs-dev libpcap-dev libbpf-dev
```

Important: `grout` requires at least `gcc` 13 or `clang` 15.
Expand Down
938 changes: 478 additions & 460 deletions docs/graph.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 7 additions & 2 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ dpdk_dep = dependency(
'werror=false',
'tests=false',
'enable_drivers=net/virtio,net/vhost,net/i40e,net/ice,net/iavf,net/ixgbe,net/null,net/tap,common/mlx5,net/mlx5,bus/auxiliary,net/vmxnet3',
'enable_libs=graph,hash,fib,rib,pcapng,gso,vhost,cryptodev,dmadev,security',
'enable_libs=graph,hash,fib,rib,pcapng,bpf,gso,vhost,cryptodev,dmadev,security',
'disable_apps=*',
'enable_docs=false',
'developer_mode=disabled',
Expand Down Expand Up @@ -103,6 +103,11 @@ elif compiler.has_function(
grout_cflags += ['-DHAVE_RTE_FIB_TBL8_GET_STATS']
endif

pcap_dep = dependency('libpcap', required: false, method: 'pkg-config')
if not pcap_dep.found()
pcap_dep = compiler.find_library('pcap', required: true)
endif

src = []
inc = []

Expand All @@ -124,7 +129,7 @@ subdir('frr')
grout_exe = executable(
'grout', src,
include_directories: inc + api_inc,
dependencies: [dpdk_dep, ev_core_dep, ev_extra_dep, ev_thread_dep, mnl_dep, numa_dep],
dependencies: [dpdk_dep, ev_core_dep, ev_extra_dep, ev_thread_dep, mnl_dep, numa_dep, pcap_dep],
c_args: ['-D__GROUT_MAIN__'] + grout_cflags,
install: true,
)
Expand Down
5 changes: 5 additions & 0 deletions modules/infra/api/gr_infra.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ 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_MIRROR = GR_BIT16(5),
} gr_iface_flags_t;

// Interface state flags.
Expand Down Expand Up @@ -70,6 +71,9 @@ typedef enum : uint8_t {
#define GR_IFACE_SET_VRF GR_BIT64(3)
#define GR_IFACE_SET_DOMAIN GR_BIT64(4)
#define GR_IFACE_SET_DESCR GR_BIT64(5)
#define GR_IFACE_SET_MIRROR_FILTER GR_BIT64(6)

#define GR_IFACE_MIRROR_FILTER_SIZE 64

// Generic struct for all network interfaces.
struct __gr_iface_base {
Expand All @@ -86,6 +90,7 @@ struct __gr_iface_base {
uint16_t vrf_id;
uint16_t domain_id; // Link domain interface ID (!GR_IFACE_MODE_VRF).
uint32_t speed; // Link speed in Megabit/sec.
char mirror_filter[GR_IFACE_MIRROR_FILTER_SIZE]; // BPF filter (tcpdump expr), empty = none.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move this field to gr_iface next to description[256]? And only store a strdup'd() copy in the control plane structure like it is done for name and description.

};

// Complete interface structure including type-specific info.
Expand Down
11 changes: 10 additions & 1 deletion modules/infra/cli/gr_cli_iface.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,23 @@ 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),(mtu MTU),((vrf VRF)|(domain DOMAIN)),(description DESCR)," \
"(mirror MIRROR),(filter FILTER)"

#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")) \
), \
with_help( \
"Enable/disable port mirroring.", \
EC_NODE_OR("MIRROR", ec_node_str("", "on"), ec_node_str("", "off")) \
), \
with_help( \
"BPF filter expression (tcpdump syntax), empty to clear.", \
ec_node("any", "FILTER") \
), \
with_help("Set the interface DOWN.", ec_node_str("down", "down")), \
with_help( \
"Maximum transmission unit size.", \
Expand Down
29 changes: 28 additions & 1 deletion modules/infra/cli/iface.c
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ 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_MIRROR)
SAFE_BUF(snprintf, len, " mirror");

return n;
err:
Expand All @@ -223,7 +225,7 @@ uint64_t parse_iface_args(
size_t info_size,
bool update
) {
const char *name, *promisc;
const char *name, *promisc, *mirror, *filter;
uint64_t set_attrs = 0;

name = arg_str(p, "NAME");
Expand Down Expand Up @@ -256,6 +258,24 @@ uint64_t parse_iface_args(
iface->flags &= ~GR_IFACE_F_PROMISC;
set_attrs |= GR_IFACE_SET_FLAGS;
}
mirror = arg_str(p, "MIRROR");
if (mirror != NULL && strcmp(mirror, "on") == 0) {
iface->flags |= GR_IFACE_F_MIRROR;
set_attrs |= GR_IFACE_SET_FLAGS;
} else if (mirror != NULL && strcmp(mirror, "off") == 0) {
iface->flags &= ~GR_IFACE_F_MIRROR;
set_attrs |= GR_IFACE_SET_FLAGS;
}
filter = arg_str(p, "FILTER");
if (filter != NULL) {
if (strlen(filter) >= GR_IFACE_MIRROR_FILTER_SIZE) {
errno = ENAMETOOLONG;
goto err;
}
memccpy(iface->mirror_filter, filter, 0, GR_IFACE_MIRROR_FILTER_SIZE);
iface->mirror_filter[GR_IFACE_MIRROR_FILTER_SIZE - 1] = '\0';
set_attrs |= GR_IFACE_SET_MIRROR_FILTER;
}

if (arg_u16(p, "MTU", &iface->mtu) == 0)
set_attrs |= GR_IFACE_SET_MTU;
Expand Down Expand Up @@ -517,6 +537,13 @@ static cmd_status_t iface_show(struct gr_api_client *c, const struct ec_pnode *p
else
gr_object_field(o, "speed", GR_DISP_INT, "%u", iface->speed);

if (iface->flags & GR_IFACE_F_MIRROR) {
if (iface->mirror_filter[0] != '\0')
printf("mirror-filter: %s\n", iface->mirror_filter);
else
printf("mirror-filter: (none)\n");
}

type = type_from_id(iface->type);
assert(type != NULL);
type->show(c, iface, o);
Expand Down
5 changes: 5 additions & 0 deletions modules/infra/control/gr_iface.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
#include <stdint.h>
#include <sys/queue.h>

struct rte_bpf;

struct __rte_cache_aligned iface {
BASE(__gr_iface_base);

Expand All @@ -23,6 +25,7 @@ struct __rte_cache_aligned iface {
int cp_id; // Control plane (Linux) port ID
int cp_fd; // control plane fd
struct event *cp_ev; // libevent to poll cp_fd
struct rte_bpf *mirror_bpf; // Compiled BPF filter, NULL = mirror all.
alignas(alignof(void *)) uint8_t info[/* size depends on type */];
};

Expand Down Expand Up @@ -82,6 +85,8 @@ int iface_set_up_down(struct iface *, bool up);
int iface_set_promisc(struct iface *, bool enabled);
uint16_t ifaces_count(gr_iface_type_t type_id);
struct iface *iface_next(gr_iface_type_t type_id, const struct iface *prev);
int iface_mirror_filter_compile(const char *expr, struct rte_bpf **out);
void iface_mirror_filter_destroy(struct rte_bpf *bpf);

uint16_t vrf_default_get_or_create(void);

Expand Down
36 changes: 36 additions & 0 deletions modules/infra/control/iface.c
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,11 @@ struct iface *iface_create(const struct gr_iface *conf, const void *api_info) {
if (iface_set_up_down(iface, up) < 0)
goto destroy;

if ((iface->flags & GR_IFACE_F_MIRROR) && conf->mirror_filter[0] != '\0') {
if (iface_mirror_filter_compile(conf->mirror_filter, &iface->mirror_bpf) < 0)
goto destroy;
}

return iface;
fail:
if (vrf_ref)
Expand Down Expand Up @@ -378,6 +383,35 @@ int iface_reconfig(
goto err;
if ((ret = iface_set_up_down(iface, conf->flags & GR_IFACE_F_UP)) < 0)
goto err;
if (conf->flags & GR_IFACE_F_MIRROR)
iface->flags |= GR_IFACE_F_MIRROR;
else {
iface->flags &= ~GR_IFACE_F_MIRROR;
struct rte_bpf *old = iface->mirror_bpf;
iface->mirror_bpf = NULL;
rte_rcu_qsbr_synchronize(gr_datapath_rcu(), RTE_QSBR_THRID_INVALID);
iface_mirror_filter_destroy(old);
iface->mirror_filter[0] = '\0';
}
}

if (set_attrs & GR_IFACE_SET_MIRROR_FILTER) {
struct rte_bpf *new_bpf = NULL;
struct rte_bpf *old_bpf;

memccpy(iface->mirror_filter, conf->mirror_filter, 0, GR_IFACE_MIRROR_FILTER_SIZE);
iface->mirror_filter[GR_IFACE_MIRROR_FILTER_SIZE - 1] = '\0';

if (conf->mirror_filter[0] != '\0') {
ret = iface_mirror_filter_compile(conf->mirror_filter, &new_bpf);
if (ret < 0)
goto err;
}

old_bpf = iface->mirror_bpf;
iface->mirror_bpf = new_bpf;
rte_rcu_qsbr_synchronize(gr_datapath_rcu(), RTE_QSBR_THRID_INVALID);
iface_mirror_filter_destroy(old_bpf);
}

if (set_attrs & GR_IFACE_SET_VRF) {
Expand Down Expand Up @@ -612,6 +646,8 @@ int iface_destroy(struct iface *iface) {
iface->flags &= ~GR_IFACE_F_UP;
gr_event_push(GR_EVENT_IFACE_STATUS_DOWN, iface);
}
iface_mirror_filter_destroy(iface->mirror_bpf);
iface->mirror_bpf = NULL;
detach_domain(iface);

ifaces[iface->id] = NULL;
Expand Down
1 change: 1 addition & 0 deletions modules/infra/control/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ src += files(
'lacp.c',
'loopback.c',
'mempool.c',
'mirror.c',
'netlink.c',
'nexthop.c',
'port.c',
Expand Down
61 changes: 61 additions & 0 deletions modules/infra/control/mirror.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 Christophe Fontaine

#include <gr_iface.h>
#include <gr_infra.h>
#include <gr_log.h>

#include <rte_bpf.h>
#include <rte_malloc.h>

#include <pcap.h>
#include <string.h>

int iface_mirror_filter_compile(const char *expr, struct rte_bpf **out) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of changing the signature to:

struct rte_bpf *iface_mirror_filter_compile(const char *expr);

And relying on errno_set_null() to propagate the error code.

struct bpf_program fcode;
struct rte_bpf_prm *prm;
struct rte_bpf *bpf;
pcap_t *pcap;

*out = NULL;

if (expr == NULL || expr[0] == '\0')
return 0;
Comment on lines +136 to +137

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably worth returning EINVAL here.


pcap = pcap_open_dead(DLT_EN10MB, 262144);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not familiar with libpcap, could you add a comment explaining what are these arguments? Especially the 262144 magic number.

if (pcap == NULL) {
LOG(ERR, "pcap_open_dead failed");
return errno_set(EINVAL);
}

if (pcap_compile(pcap, &fcode, expr, 1, PCAP_NETMASK_UNKNOWN) != 0) {
LOG(ERR, "mirror filter \"%s\": %s", expr, pcap_geterr(pcap));
pcap_close(pcap);
return errno_set(EINVAL);
}

prm = rte_bpf_convert(&fcode);
pcap_freecode(&fcode);
pcap_close(pcap);

if (prm == NULL) {
LOG(ERR, "rte_bpf_convert \"%s\": %s", expr, rte_strerror(rte_errno));
return -rte_errno;
}

bpf = rte_bpf_load(prm);
rte_free(prm);

if (bpf == NULL) {
LOG(ERR, "rte_bpf_load \"%s\": %s", expr, rte_strerror(rte_errno));
return -rte_errno;
}

*out = bpf;
return 0;
}

void iface_mirror_filter_destroy(struct rte_bpf *bpf) {
if (bpf != NULL)
rte_bpf_destroy(bpf);
}
3 changes: 3 additions & 0 deletions modules/infra/datapath/gr_mbuf.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ void gr_mbuf_trace_copy(struct rte_mbuf *dst, struct rte_mbuf *src);
// Detach the trace items from an mbuf and store them in the trace buffer.
void gr_mbuf_trace_finish(struct rte_mbuf *m);

// dynfield to store a unique packet id, for pcapng
extern int pcapng_packetid_offset;

// Deep copy of an mbuf: duplicates mbuf, copies mbuf priv data and traces
static inline struct rte_mbuf *gr_mbuf_copy(struct rte_mbuf *m, size_t data_len) {
struct rte_mbuf *copy = rte_pktmbuf_copy(m, m->pool, 0, data_len);
Expand Down
Loading
Loading