mirror of
https://github.com/xdp-project/bpf-examples.git
synced 2024-05-06 15:54:53 +00:00
Add pkt-loop-filter example
Add an example to filter looping packets on (for instance) a bond interface, by recording the egress MAC+VLAN and dropping any packets that come in on other (related) interfaces with the same MAC+VLAN. Signed-off-by: Toke Høiland-Jørgensen <toke@redhat.com>
This commit is contained in:
1
pkt-loop-filter/.gitignore
vendored
Normal file
1
pkt-loop-filter/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
pkt-loop-filter
|
9
pkt-loop-filter/Makefile
Normal file
9
pkt-loop-filter/Makefile
Normal file
@@ -0,0 +1,9 @@
|
||||
# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause)
|
||||
|
||||
USER_TARGETS += pkt-loop-filter
|
||||
BPF_TARGETS += pkt-loop-filter.kern
|
||||
EXTRA_DEPS := pkt-loop-filter.h
|
||||
|
||||
LIB_DIR = ../lib
|
||||
|
||||
include $(LIB_DIR)/common.mk
|
23
pkt-loop-filter/README.org
Normal file
23
pkt-loop-filter/README.org
Normal file
@@ -0,0 +1,23 @@
|
||||
* Packet loop filter
|
||||
|
||||
This example shows how to filter looping packets, for example when two bond
|
||||
interfaces are attached to a switch that loops packets back through the other
|
||||
bond interface. It works by attaching ingress and egress TC filters to one or
|
||||
more interfaces, and keeping track of the source VLAN+MAC on every packet going
|
||||
out any of the included interfaces. If a packet comes back in with a source
|
||||
MAC+VLAN that was already seen on egress, that packet is simply dropped (subject
|
||||
to a 10-second expiry time).
|
||||
|
||||
To load, simply execute the userspace binary with all interface names to load
|
||||
the filter to; all interfaces loaded this way will share the same map, so the
|
||||
filter will work across all of them. For instance, if a bond interface is using
|
||||
underlying veth0 and veth1, execute =./pkt-loop-filter veth0 veth1= to enable
|
||||
the filter on packets looping through the two.
|
||||
|
||||
To unload, add the =--unload= parameter to the userspace utility. Note that the
|
||||
same set of interfaces should be supplied on load and unload; the tool doesn't
|
||||
check for this so if it's not, the unload will only be partial.
|
||||
|
||||
The BPF programs will record which interface a given source MAC+VLAN was last
|
||||
seen on, as well as the number of packets dropped for that MAC+VLAN. These
|
||||
statistics are kept in the BPF map and can be dumped using =bpftool=.
|
140
pkt-loop-filter/pkt-loop-filter.c
Normal file
140
pkt-loop-filter/pkt-loop-filter.c
Normal file
@@ -0,0 +1,140 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <net/if.h>
|
||||
#include <linux/if_arp.h>
|
||||
|
||||
#include <bpf/libbpf.h>
|
||||
|
||||
#define MAX_IFINDEXES 10
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
int err = 0, i, num_ifindexes = 0, _err, ingress_fd, egress_fd;
|
||||
const char *filename = "pkt-loop-filter.kern.o";
|
||||
int ifindex[MAX_IFINDEXES];
|
||||
struct bpf_object *obj;
|
||||
bool unload = false;
|
||||
DECLARE_LIBBPF_OPTS(bpf_tc_hook, hook, .attach_point = BPF_TC_INGRESS | BPF_TC_EGRESS);
|
||||
|
||||
if (argc < 2) {
|
||||
fprintf(stderr, "Usage: %s <ifname> [..ifname] [--unload]\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (i = 0; i < MAX_IFINDEXES; i++) {
|
||||
char *ifname = argv[i+1];
|
||||
|
||||
if (i + 1 >= argc)
|
||||
break;
|
||||
|
||||
if (!strcmp(ifname, "--unload")) {
|
||||
unload = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
ifindex[num_ifindexes] = if_nametoindex(ifname);
|
||||
if (!ifindex[num_ifindexes]) {
|
||||
fprintf(stderr, "Couldn't find interface '%s'\n", ifname);
|
||||
return 1;
|
||||
}
|
||||
num_ifindexes++;
|
||||
}
|
||||
|
||||
if (!num_ifindexes) {
|
||||
fprintf(stderr, "Need at least one interface name\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (unload)
|
||||
goto unload;
|
||||
|
||||
obj = bpf_object__open(filename);
|
||||
err = libbpf_get_error(obj);
|
||||
if (err) {
|
||||
fprintf(stderr, "Couldn't open file: %s\n", filename);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = bpf_object__load(obj);
|
||||
if (err) {
|
||||
fprintf(stderr, "Failed to load object\n");
|
||||
goto out;
|
||||
}
|
||||
|
||||
egress_fd = bpf_program__fd(bpf_object__find_program_by_name(obj, "record_egress_pkt"));
|
||||
if (egress_fd < 0) {
|
||||
fprintf(stderr, "Couldn't find program 'record_egress_pkt'\n");
|
||||
err = -ENOENT;
|
||||
goto out;
|
||||
}
|
||||
|
||||
ingress_fd = bpf_program__fd(bpf_object__find_program_by_name(obj, "filter_ingress_pkt"));
|
||||
if (ingress_fd < 0) {
|
||||
fprintf(stderr, "Couldn't find program 'filter_ingress_pkt'\n");
|
||||
err = -ENOENT;
|
||||
goto out;
|
||||
}
|
||||
|
||||
for (i = 0; i < num_ifindexes; i++) {
|
||||
DECLARE_LIBBPF_OPTS(bpf_tc_opts, attach_egress,
|
||||
.prog_fd = egress_fd);
|
||||
DECLARE_LIBBPF_OPTS(bpf_tc_opts, attach_ingress,
|
||||
.prog_fd = ingress_fd);
|
||||
char ifname[IF_NAMESIZE];
|
||||
|
||||
if (!if_indextoname(ifindex[i], ifname)) {
|
||||
err = -errno;
|
||||
perror("if_indextoname");
|
||||
goto out;
|
||||
}
|
||||
|
||||
hook.ifindex = ifindex[i];
|
||||
hook.attach_point = BPF_TC_EGRESS | BPF_TC_INGRESS;
|
||||
err = bpf_tc_hook_create(&hook);
|
||||
if (err && err != -EEXIST) {
|
||||
fprintf(stderr, "Couldn't create egress hook for interface %s\n", ifname);
|
||||
goto unload;
|
||||
}
|
||||
|
||||
hook.attach_point = BPF_TC_EGRESS;
|
||||
err = bpf_tc_attach(&hook, &attach_egress);
|
||||
if (err) {
|
||||
fprintf(stderr, "Couldn't attach egress program to interface %s: %s\n", ifname, strerror(errno));
|
||||
goto unload;
|
||||
}
|
||||
|
||||
hook.attach_point = BPF_TC_INGRESS;
|
||||
err = bpf_tc_attach(&hook, &attach_ingress);
|
||||
if (err) {
|
||||
fprintf(stderr, "Couldn't attach ingress program to interface %s: %s\n", ifname, strerror(errno));
|
||||
goto unload;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
out:
|
||||
bpf_object__close(obj);
|
||||
return err;
|
||||
|
||||
unload:
|
||||
for (i = 0; i < num_ifindexes; i++) {
|
||||
char ifname[IF_NAMESIZE];
|
||||
|
||||
hook.ifindex = ifindex[i];
|
||||
hook.attach_point = BPF_TC_EGRESS | BPF_TC_INGRESS;
|
||||
_err = bpf_tc_hook_destroy(&hook);
|
||||
if (_err) {
|
||||
fprintf(stderr, "Couldn't remove clsact qdisc on %s\n",
|
||||
if_indextoname(ifindex[i], ifname));
|
||||
err = _err;
|
||||
}
|
||||
|
||||
}
|
||||
return err;
|
||||
}
|
19
pkt-loop-filter/pkt-loop-filter.h
Normal file
19
pkt-loop-filter/pkt-loop-filter.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#ifndef __PKT_LOOP_FILTER_H__
|
||||
#define __PKT_LOOP_FILTER_H__
|
||||
|
||||
#define NS_PER_SEC 1000000000ULL
|
||||
#define STATE_LIFETIME (10 * NS_PER_SEC)
|
||||
|
||||
struct pkt_loop_key {
|
||||
__u8 src_mac[6];
|
||||
__u16 src_vlan;
|
||||
};
|
||||
|
||||
struct pkt_loop_data {
|
||||
__u64 expiry_time;
|
||||
__u32 ifindex;
|
||||
__u32 drops;
|
||||
|
||||
};
|
||||
|
||||
#endif
|
84
pkt-loop-filter/pkt-loop-filter.kern.c
Normal file
84
pkt-loop-filter/pkt-loop-filter.kern.c
Normal file
@@ -0,0 +1,84 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
#include <linux/bpf.h>
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <xdp/parsing_helpers.h>
|
||||
#include <linux/pkt_cls.h>
|
||||
|
||||
#include "pkt-loop-filter.h"
|
||||
|
||||
/* We use an LRU map to avoid having to do cleanup: We just rely on the LRU
|
||||
* mechanism to evict old entries as the map fills up.
|
||||
*/
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_LRU_HASH);
|
||||
__type(key, struct pkt_loop_key);
|
||||
__type(value, struct pkt_loop_data);
|
||||
__uint(max_entries, 16384);
|
||||
} iface_state SEC(".maps");
|
||||
|
||||
static int parse_pkt(struct __sk_buff *skb, struct pkt_loop_key *key)
|
||||
{
|
||||
void *data_end = (void *)(unsigned long long)skb->data_end;
|
||||
void *data = (void *)(unsigned long long)skb->data;
|
||||
struct hdr_cursor nh = { .pos = data };
|
||||
struct ethhdr *eth;
|
||||
int eth_type;
|
||||
|
||||
/* Parse Ethernet and IP/IPv6 headers */
|
||||
eth_type = parse_ethhdr(&nh, data_end, ð);
|
||||
if (eth_type < 0)
|
||||
return eth_type;
|
||||
|
||||
__builtin_memcpy(key->src_mac, eth->h_source, ETH_ALEN);
|
||||
key->src_vlan = skb->vlan_tci;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC("tc")
|
||||
int record_egress_pkt(struct __sk_buff *skb)
|
||||
{
|
||||
struct pkt_loop_data value = { .ifindex = skb->ifindex }, *v;
|
||||
struct pkt_loop_key key;
|
||||
int err;
|
||||
|
||||
err = parse_pkt(skb, &key);
|
||||
if (err)
|
||||
goto out;
|
||||
|
||||
v = bpf_map_lookup_elem(&iface_state, &key);
|
||||
if (!v) {
|
||||
bpf_map_update_elem(&iface_state, &key, &value, BPF_NOEXIST);
|
||||
v = bpf_map_lookup_elem(&iface_state, &key);
|
||||
if (!v)
|
||||
goto out;
|
||||
}
|
||||
v->expiry_time = bpf_ktime_get_coarse_ns() + STATE_LIFETIME;
|
||||
v->ifindex = skb->ifindex;
|
||||
out:
|
||||
return TC_ACT_OK;
|
||||
}
|
||||
|
||||
SEC("tc")
|
||||
int filter_ingress_pkt(struct __sk_buff *skb)
|
||||
{
|
||||
struct pkt_loop_data *value;
|
||||
struct pkt_loop_key key;
|
||||
int err;
|
||||
|
||||
err = parse_pkt(skb, &key);
|
||||
if (err)
|
||||
goto out;
|
||||
|
||||
value = bpf_map_lookup_elem(&iface_state, &key);
|
||||
if (value && value->expiry_time > bpf_ktime_get_coarse_ns()) {
|
||||
value->drops++;
|
||||
return TC_ACT_SHOT;
|
||||
|
||||
}
|
||||
|
||||
out:
|
||||
return TC_ACT_OK;
|
||||
}
|
||||
|
||||
char _license[] SEC("license") = "GPL";
|
Reference in New Issue
Block a user