Merge tag 'for-netdev' of https://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next
Martin KaFai Lau says: ==================== pull-request: bpf-next 2025-04-17 We've added 12 non-merge commits during the last 9 day(s) which contain a total of 18 files changed, 1748 insertions(+), 19 deletions(-). The main changes are: 1) bpf qdisc support, from Amery Hung. A qdisc can be implemented in bpf struct_ops programs and can be used the same as other existing qdiscs in the "tc qdisc" command. 2) Add xsk tail adjustment tests, from Tushar Vyavahare. * tag 'for-netdev' of https://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next: selftests/bpf: Test attaching bpf qdisc to mq and non root selftests/bpf: Add a bpf fq qdisc to selftest selftests/bpf: Add a basic fifo qdisc test libbpf: Support creating and destroying qdisc bpf: net_sched: Disable attaching bpf qdisc to non root bpf: net_sched: Support updating bstats bpf: net_sched: Add a qdisc watchdog timer bpf: net_sched: Add basic bpf qdisc kfuncs bpf: net_sched: Support implementation of Qdisc_ops in bpf bpf: Prepare to reuse get_ctx_arg_idx selftests/xsk: Add tail adjustment tests and support check selftests/xsk: Add packet stream replacement function ==================== Link: https://patch.msgid.link/20250417184338.3152168-1-martin.lau@linux.dev Signed-off-by: Jakub Kicinski <kuba@kernel.org>
This commit is contained in:
@@ -522,6 +522,7 @@ bool btf_param_match_suffix(const struct btf *btf,
|
||||
const char *suffix);
|
||||
int btf_ctx_arg_offset(const struct btf *btf, const struct btf_type *func_proto,
|
||||
u32 arg_no);
|
||||
u32 btf_ctx_arg_idx(struct btf *btf, const struct btf_type *func_proto, int off);
|
||||
|
||||
struct bpf_verifier_log;
|
||||
|
||||
|
||||
+3
-3
@@ -6391,8 +6391,8 @@ static bool is_int_ptr(struct btf *btf, const struct btf_type *t)
|
||||
return btf_type_is_int(t);
|
||||
}
|
||||
|
||||
static u32 get_ctx_arg_idx(struct btf *btf, const struct btf_type *func_proto,
|
||||
int off)
|
||||
u32 btf_ctx_arg_idx(struct btf *btf, const struct btf_type *func_proto,
|
||||
int off)
|
||||
{
|
||||
const struct btf_param *args;
|
||||
const struct btf_type *t;
|
||||
@@ -6672,7 +6672,7 @@ bool btf_ctx_access(int off, int size, enum bpf_access_type type,
|
||||
tname, off);
|
||||
return false;
|
||||
}
|
||||
arg = get_ctx_arg_idx(btf, t, off);
|
||||
arg = btf_ctx_arg_idx(btf, t, off);
|
||||
args = (const struct btf_param *)(t + 1);
|
||||
/* if (t == NULL) Fall back to default BPF prog with
|
||||
* MAX_BPF_FUNC_REG_ARGS u64 arguments.
|
||||
|
||||
@@ -403,6 +403,18 @@ config NET_SCH_ETS
|
||||
|
||||
If unsure, say N.
|
||||
|
||||
config NET_SCH_BPF
|
||||
bool "BPF-based Qdisc"
|
||||
depends on BPF_SYSCALL && BPF_JIT && DEBUG_INFO_BTF
|
||||
help
|
||||
This option allows BPF-based queueing disiplines. With BPF struct_ops,
|
||||
users can implement supported operators in Qdisc_ops using BPF programs.
|
||||
The queue holding skb can be built with BPF maps or graphs.
|
||||
|
||||
Say Y here if you want to use BPF-based Qdisc.
|
||||
|
||||
If unsure, say N.
|
||||
|
||||
menuconfig NET_SCH_DEFAULT
|
||||
bool "Allow override default queue discipline"
|
||||
help
|
||||
|
||||
@@ -62,6 +62,7 @@ obj-$(CONFIG_NET_SCH_FQ_PIE) += sch_fq_pie.o
|
||||
obj-$(CONFIG_NET_SCH_CBS) += sch_cbs.o
|
||||
obj-$(CONFIG_NET_SCH_ETF) += sch_etf.o
|
||||
obj-$(CONFIG_NET_SCH_TAPRIO) += sch_taprio.o
|
||||
obj-$(CONFIG_NET_SCH_BPF) += bpf_qdisc.o
|
||||
|
||||
obj-$(CONFIG_NET_CLS_U32) += cls_u32.o
|
||||
obj-$(CONFIG_NET_CLS_ROUTE4) += cls_route.o
|
||||
|
||||
@@ -0,0 +1,461 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
|
||||
#include <linux/types.h>
|
||||
#include <linux/bpf_verifier.h>
|
||||
#include <linux/bpf.h>
|
||||
#include <linux/btf.h>
|
||||
#include <linux/filter.h>
|
||||
#include <net/pkt_sched.h>
|
||||
#include <net/pkt_cls.h>
|
||||
|
||||
#define QDISC_OP_IDX(op) (offsetof(struct Qdisc_ops, op) / sizeof(void (*)(void)))
|
||||
#define QDISC_MOFF_IDX(moff) (moff / sizeof(void (*)(void)))
|
||||
|
||||
static struct bpf_struct_ops bpf_Qdisc_ops;
|
||||
|
||||
struct bpf_sched_data {
|
||||
struct qdisc_watchdog watchdog;
|
||||
};
|
||||
|
||||
struct bpf_sk_buff_ptr {
|
||||
struct sk_buff *skb;
|
||||
};
|
||||
|
||||
static int bpf_qdisc_init(struct btf *btf)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
BTF_ID_LIST_SINGLE(bpf_qdisc_ids, struct, Qdisc)
|
||||
BTF_ID_LIST_SINGLE(bpf_sk_buff_ids, struct, sk_buff)
|
||||
BTF_ID_LIST_SINGLE(bpf_sk_buff_ptr_ids, struct, bpf_sk_buff_ptr)
|
||||
|
||||
static bool bpf_qdisc_is_valid_access(int off, int size,
|
||||
enum bpf_access_type type,
|
||||
const struct bpf_prog *prog,
|
||||
struct bpf_insn_access_aux *info)
|
||||
{
|
||||
struct btf *btf = prog->aux->attach_btf;
|
||||
u32 arg;
|
||||
|
||||
arg = btf_ctx_arg_idx(btf, prog->aux->attach_func_proto, off);
|
||||
if (prog->aux->attach_st_ops_member_off == offsetof(struct Qdisc_ops, enqueue)) {
|
||||
if (arg == 2 && type == BPF_READ) {
|
||||
info->reg_type = PTR_TO_BTF_ID | PTR_TRUSTED;
|
||||
info->btf = btf;
|
||||
info->btf_id = bpf_sk_buff_ptr_ids[0];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return bpf_tracing_btf_ctx_access(off, size, type, prog, info);
|
||||
}
|
||||
|
||||
static int bpf_qdisc_qdisc_access(struct bpf_verifier_log *log,
|
||||
const struct bpf_reg_state *reg,
|
||||
int off, size_t *end)
|
||||
{
|
||||
switch (off) {
|
||||
case offsetof(struct Qdisc, limit):
|
||||
*end = offsetofend(struct Qdisc, limit);
|
||||
break;
|
||||
case offsetof(struct Qdisc, q) + offsetof(struct qdisc_skb_head, qlen):
|
||||
*end = offsetof(struct Qdisc, q) + offsetofend(struct qdisc_skb_head, qlen);
|
||||
break;
|
||||
case offsetof(struct Qdisc, qstats) ... offsetofend(struct Qdisc, qstats) - 1:
|
||||
*end = offsetofend(struct Qdisc, qstats);
|
||||
break;
|
||||
default:
|
||||
return -EACCES;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int bpf_qdisc_sk_buff_access(struct bpf_verifier_log *log,
|
||||
const struct bpf_reg_state *reg,
|
||||
int off, size_t *end)
|
||||
{
|
||||
switch (off) {
|
||||
case offsetof(struct sk_buff, tstamp):
|
||||
*end = offsetofend(struct sk_buff, tstamp);
|
||||
break;
|
||||
case offsetof(struct sk_buff, cb) + offsetof(struct qdisc_skb_cb, data[0]) ...
|
||||
offsetof(struct sk_buff, cb) + offsetof(struct qdisc_skb_cb,
|
||||
data[QDISC_CB_PRIV_LEN - 1]):
|
||||
*end = offsetof(struct sk_buff, cb) +
|
||||
offsetofend(struct qdisc_skb_cb, data[QDISC_CB_PRIV_LEN - 1]);
|
||||
break;
|
||||
default:
|
||||
return -EACCES;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int bpf_qdisc_btf_struct_access(struct bpf_verifier_log *log,
|
||||
const struct bpf_reg_state *reg,
|
||||
int off, int size)
|
||||
{
|
||||
const struct btf_type *t, *skbt, *qdisct;
|
||||
size_t end;
|
||||
int err;
|
||||
|
||||
skbt = btf_type_by_id(reg->btf, bpf_sk_buff_ids[0]);
|
||||
qdisct = btf_type_by_id(reg->btf, bpf_qdisc_ids[0]);
|
||||
t = btf_type_by_id(reg->btf, reg->btf_id);
|
||||
|
||||
if (t == skbt) {
|
||||
err = bpf_qdisc_sk_buff_access(log, reg, off, &end);
|
||||
} else if (t == qdisct) {
|
||||
err = bpf_qdisc_qdisc_access(log, reg, off, &end);
|
||||
} else {
|
||||
bpf_log(log, "only read is supported\n");
|
||||
return -EACCES;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
bpf_log(log, "no write support to %s at off %d\n",
|
||||
btf_name_by_offset(reg->btf, t->name_off), off);
|
||||
return -EACCES;
|
||||
}
|
||||
|
||||
if (off + size > end) {
|
||||
bpf_log(log,
|
||||
"write access at off %d with size %d beyond the member of %s ended at %zu\n",
|
||||
off, size, btf_name_by_offset(reg->btf, t->name_off), end);
|
||||
return -EACCES;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
BTF_ID_LIST(bpf_qdisc_init_prologue_ids)
|
||||
BTF_ID(func, bpf_qdisc_init_prologue)
|
||||
|
||||
static int bpf_qdisc_gen_prologue(struct bpf_insn *insn_buf, bool direct_write,
|
||||
const struct bpf_prog *prog)
|
||||
{
|
||||
struct bpf_insn *insn = insn_buf;
|
||||
|
||||
if (prog->aux->attach_st_ops_member_off != offsetof(struct Qdisc_ops, init))
|
||||
return 0;
|
||||
|
||||
/* r6 = r1; // r6 will be "u64 *ctx". r1 is "u64 *ctx".
|
||||
* r2 = r1[16]; // r2 will be "struct netlink_ext_ack *extack"
|
||||
* r1 = r1[0]; // r1 will be "struct Qdisc *sch"
|
||||
* r0 = bpf_qdisc_init_prologue(r1, r2);
|
||||
* if r0 == 0 goto pc+1;
|
||||
* BPF_EXIT;
|
||||
* r1 = r6; // r1 will be "u64 *ctx".
|
||||
*/
|
||||
*insn++ = BPF_MOV64_REG(BPF_REG_6, BPF_REG_1);
|
||||
*insn++ = BPF_LDX_MEM(BPF_DW, BPF_REG_2, BPF_REG_1, 16);
|
||||
*insn++ = BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_1, 0);
|
||||
*insn++ = BPF_CALL_KFUNC(0, bpf_qdisc_init_prologue_ids[0]);
|
||||
*insn++ = BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, 1);
|
||||
*insn++ = BPF_EXIT_INSN();
|
||||
*insn++ = BPF_MOV64_REG(BPF_REG_1, BPF_REG_6);
|
||||
*insn++ = prog->insnsi[0];
|
||||
|
||||
return insn - insn_buf;
|
||||
}
|
||||
|
||||
BTF_ID_LIST(bpf_qdisc_reset_destroy_epilogue_ids)
|
||||
BTF_ID(func, bpf_qdisc_reset_destroy_epilogue)
|
||||
|
||||
static int bpf_qdisc_gen_epilogue(struct bpf_insn *insn_buf, const struct bpf_prog *prog,
|
||||
s16 ctx_stack_off)
|
||||
{
|
||||
struct bpf_insn *insn = insn_buf;
|
||||
|
||||
if (prog->aux->attach_st_ops_member_off != offsetof(struct Qdisc_ops, reset) &&
|
||||
prog->aux->attach_st_ops_member_off != offsetof(struct Qdisc_ops, destroy))
|
||||
return 0;
|
||||
|
||||
/* r1 = stack[ctx_stack_off]; // r1 will be "u64 *ctx"
|
||||
* r1 = r1[0]; // r1 will be "struct Qdisc *sch"
|
||||
* r0 = bpf_qdisc_reset_destroy_epilogue(r1);
|
||||
* BPF_EXIT;
|
||||
*/
|
||||
*insn++ = BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_FP, ctx_stack_off);
|
||||
*insn++ = BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_1, 0);
|
||||
*insn++ = BPF_CALL_KFUNC(0, bpf_qdisc_reset_destroy_epilogue_ids[0]);
|
||||
*insn++ = BPF_EXIT_INSN();
|
||||
|
||||
return insn - insn_buf;
|
||||
}
|
||||
|
||||
__bpf_kfunc_start_defs();
|
||||
|
||||
/* bpf_skb_get_hash - Get the flow hash of an skb.
|
||||
* @skb: The skb to get the flow hash from.
|
||||
*/
|
||||
__bpf_kfunc u32 bpf_skb_get_hash(struct sk_buff *skb)
|
||||
{
|
||||
return skb_get_hash(skb);
|
||||
}
|
||||
|
||||
/* bpf_kfree_skb - Release an skb's reference and drop it immediately.
|
||||
* @skb: The skb whose reference to be released and dropped.
|
||||
*/
|
||||
__bpf_kfunc void bpf_kfree_skb(struct sk_buff *skb)
|
||||
{
|
||||
kfree_skb(skb);
|
||||
}
|
||||
|
||||
/* bpf_qdisc_skb_drop - Drop an skb by adding it to a deferred free list.
|
||||
* @skb: The skb whose reference to be released and dropped.
|
||||
* @to_free_list: The list of skbs to be dropped.
|
||||
*/
|
||||
__bpf_kfunc void bpf_qdisc_skb_drop(struct sk_buff *skb,
|
||||
struct bpf_sk_buff_ptr *to_free_list)
|
||||
{
|
||||
__qdisc_drop(skb, (struct sk_buff **)to_free_list);
|
||||
}
|
||||
|
||||
/* bpf_qdisc_watchdog_schedule - Schedule a qdisc to a later time using a timer.
|
||||
* @sch: The qdisc to be scheduled.
|
||||
* @expire: The expiry time of the timer.
|
||||
* @delta_ns: The slack range of the timer.
|
||||
*/
|
||||
__bpf_kfunc void bpf_qdisc_watchdog_schedule(struct Qdisc *sch, u64 expire, u64 delta_ns)
|
||||
{
|
||||
struct bpf_sched_data *q = qdisc_priv(sch);
|
||||
|
||||
qdisc_watchdog_schedule_range_ns(&q->watchdog, expire, delta_ns);
|
||||
}
|
||||
|
||||
/* bpf_qdisc_init_prologue - Hidden kfunc called in prologue of .init. */
|
||||
__bpf_kfunc int bpf_qdisc_init_prologue(struct Qdisc *sch,
|
||||
struct netlink_ext_ack *extack)
|
||||
{
|
||||
struct bpf_sched_data *q = qdisc_priv(sch);
|
||||
struct net_device *dev = qdisc_dev(sch);
|
||||
struct Qdisc *p;
|
||||
|
||||
if (sch->parent != TC_H_ROOT) {
|
||||
p = qdisc_lookup(dev, TC_H_MAJ(sch->parent));
|
||||
if (!p)
|
||||
return -ENOENT;
|
||||
|
||||
if (!(p->flags & TCQ_F_MQROOT)) {
|
||||
NL_SET_ERR_MSG(extack, "BPF qdisc only supported on root or mq");
|
||||
return -EINVAL;
|
||||
}
|
||||
}
|
||||
|
||||
qdisc_watchdog_init(&q->watchdog, sch);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* bpf_qdisc_reset_destroy_epilogue - Hidden kfunc called in epilogue of .reset
|
||||
* and .destroy
|
||||
*/
|
||||
__bpf_kfunc void bpf_qdisc_reset_destroy_epilogue(struct Qdisc *sch)
|
||||
{
|
||||
struct bpf_sched_data *q = qdisc_priv(sch);
|
||||
|
||||
qdisc_watchdog_cancel(&q->watchdog);
|
||||
}
|
||||
|
||||
/* bpf_qdisc_bstats_update - Update Qdisc basic statistics
|
||||
* @sch: The qdisc from which an skb is dequeued.
|
||||
* @skb: The skb to be dequeued.
|
||||
*/
|
||||
__bpf_kfunc void bpf_qdisc_bstats_update(struct Qdisc *sch, const struct sk_buff *skb)
|
||||
{
|
||||
bstats_update(&sch->bstats, skb);
|
||||
}
|
||||
|
||||
__bpf_kfunc_end_defs();
|
||||
|
||||
BTF_KFUNCS_START(qdisc_kfunc_ids)
|
||||
BTF_ID_FLAGS(func, bpf_skb_get_hash, KF_TRUSTED_ARGS)
|
||||
BTF_ID_FLAGS(func, bpf_kfree_skb, KF_RELEASE)
|
||||
BTF_ID_FLAGS(func, bpf_qdisc_skb_drop, KF_RELEASE)
|
||||
BTF_ID_FLAGS(func, bpf_dynptr_from_skb, KF_TRUSTED_ARGS)
|
||||
BTF_ID_FLAGS(func, bpf_qdisc_watchdog_schedule, KF_TRUSTED_ARGS)
|
||||
BTF_ID_FLAGS(func, bpf_qdisc_init_prologue, KF_TRUSTED_ARGS)
|
||||
BTF_ID_FLAGS(func, bpf_qdisc_reset_destroy_epilogue, KF_TRUSTED_ARGS)
|
||||
BTF_ID_FLAGS(func, bpf_qdisc_bstats_update, KF_TRUSTED_ARGS)
|
||||
BTF_KFUNCS_END(qdisc_kfunc_ids)
|
||||
|
||||
BTF_SET_START(qdisc_common_kfunc_set)
|
||||
BTF_ID(func, bpf_skb_get_hash)
|
||||
BTF_ID(func, bpf_kfree_skb)
|
||||
BTF_ID(func, bpf_dynptr_from_skb)
|
||||
BTF_SET_END(qdisc_common_kfunc_set)
|
||||
|
||||
BTF_SET_START(qdisc_enqueue_kfunc_set)
|
||||
BTF_ID(func, bpf_qdisc_skb_drop)
|
||||
BTF_ID(func, bpf_qdisc_watchdog_schedule)
|
||||
BTF_SET_END(qdisc_enqueue_kfunc_set)
|
||||
|
||||
BTF_SET_START(qdisc_dequeue_kfunc_set)
|
||||
BTF_ID(func, bpf_qdisc_watchdog_schedule)
|
||||
BTF_ID(func, bpf_qdisc_bstats_update)
|
||||
BTF_SET_END(qdisc_dequeue_kfunc_set)
|
||||
|
||||
enum qdisc_ops_kf_flags {
|
||||
QDISC_OPS_KF_COMMON = 0,
|
||||
QDISC_OPS_KF_ENQUEUE = 1 << 0,
|
||||
QDISC_OPS_KF_DEQUEUE = 1 << 1,
|
||||
};
|
||||
|
||||
static const u32 qdisc_ops_context_flags[] = {
|
||||
[QDISC_OP_IDX(enqueue)] = QDISC_OPS_KF_ENQUEUE,
|
||||
[QDISC_OP_IDX(dequeue)] = QDISC_OPS_KF_DEQUEUE,
|
||||
[QDISC_OP_IDX(init)] = QDISC_OPS_KF_COMMON,
|
||||
[QDISC_OP_IDX(reset)] = QDISC_OPS_KF_COMMON,
|
||||
[QDISC_OP_IDX(destroy)] = QDISC_OPS_KF_COMMON,
|
||||
};
|
||||
|
||||
static int bpf_qdisc_kfunc_filter(const struct bpf_prog *prog, u32 kfunc_id)
|
||||
{
|
||||
u32 moff, flags;
|
||||
|
||||
if (!btf_id_set8_contains(&qdisc_kfunc_ids, kfunc_id))
|
||||
return 0;
|
||||
|
||||
if (prog->aux->st_ops != &bpf_Qdisc_ops)
|
||||
return -EACCES;
|
||||
|
||||
moff = prog->aux->attach_st_ops_member_off;
|
||||
flags = qdisc_ops_context_flags[QDISC_MOFF_IDX(moff)];
|
||||
|
||||
if ((flags & QDISC_OPS_KF_ENQUEUE) &&
|
||||
btf_id_set_contains(&qdisc_enqueue_kfunc_set, kfunc_id))
|
||||
return 0;
|
||||
|
||||
if ((flags & QDISC_OPS_KF_DEQUEUE) &&
|
||||
btf_id_set_contains(&qdisc_dequeue_kfunc_set, kfunc_id))
|
||||
return 0;
|
||||
|
||||
if (btf_id_set_contains(&qdisc_common_kfunc_set, kfunc_id))
|
||||
return 0;
|
||||
|
||||
return -EACCES;
|
||||
}
|
||||
|
||||
static const struct btf_kfunc_id_set bpf_qdisc_kfunc_set = {
|
||||
.owner = THIS_MODULE,
|
||||
.set = &qdisc_kfunc_ids,
|
||||
.filter = bpf_qdisc_kfunc_filter,
|
||||
};
|
||||
|
||||
static const struct bpf_verifier_ops bpf_qdisc_verifier_ops = {
|
||||
.get_func_proto = bpf_base_func_proto,
|
||||
.is_valid_access = bpf_qdisc_is_valid_access,
|
||||
.btf_struct_access = bpf_qdisc_btf_struct_access,
|
||||
.gen_prologue = bpf_qdisc_gen_prologue,
|
||||
.gen_epilogue = bpf_qdisc_gen_epilogue,
|
||||
};
|
||||
|
||||
static int bpf_qdisc_init_member(const struct btf_type *t,
|
||||
const struct btf_member *member,
|
||||
void *kdata, const void *udata)
|
||||
{
|
||||
const struct Qdisc_ops *uqdisc_ops;
|
||||
struct Qdisc_ops *qdisc_ops;
|
||||
u32 moff;
|
||||
|
||||
uqdisc_ops = (const struct Qdisc_ops *)udata;
|
||||
qdisc_ops = (struct Qdisc_ops *)kdata;
|
||||
|
||||
moff = __btf_member_bit_offset(t, member) / 8;
|
||||
switch (moff) {
|
||||
case offsetof(struct Qdisc_ops, priv_size):
|
||||
if (uqdisc_ops->priv_size)
|
||||
return -EINVAL;
|
||||
qdisc_ops->priv_size = sizeof(struct bpf_sched_data);
|
||||
return 1;
|
||||
case offsetof(struct Qdisc_ops, peek):
|
||||
qdisc_ops->peek = qdisc_peek_dequeued;
|
||||
return 0;
|
||||
case offsetof(struct Qdisc_ops, id):
|
||||
if (bpf_obj_name_cpy(qdisc_ops->id, uqdisc_ops->id,
|
||||
sizeof(qdisc_ops->id)) <= 0)
|
||||
return -EINVAL;
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int bpf_qdisc_reg(void *kdata, struct bpf_link *link)
|
||||
{
|
||||
return register_qdisc(kdata);
|
||||
}
|
||||
|
||||
static void bpf_qdisc_unreg(void *kdata, struct bpf_link *link)
|
||||
{
|
||||
return unregister_qdisc(kdata);
|
||||
}
|
||||
|
||||
static int Qdisc_ops__enqueue(struct sk_buff *skb__ref, struct Qdisc *sch,
|
||||
struct sk_buff **to_free)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
static struct sk_buff *Qdisc_ops__dequeue(struct Qdisc *sch)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int Qdisc_ops__init(struct Qdisc *sch, struct nlattr *arg,
|
||||
struct netlink_ext_ack *extack)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void Qdisc_ops__reset(struct Qdisc *sch)
|
||||
{
|
||||
}
|
||||
|
||||
static void Qdisc_ops__destroy(struct Qdisc *sch)
|
||||
{
|
||||
}
|
||||
|
||||
static struct Qdisc_ops __bpf_ops_qdisc_ops = {
|
||||
.enqueue = Qdisc_ops__enqueue,
|
||||
.dequeue = Qdisc_ops__dequeue,
|
||||
.init = Qdisc_ops__init,
|
||||
.reset = Qdisc_ops__reset,
|
||||
.destroy = Qdisc_ops__destroy,
|
||||
};
|
||||
|
||||
static struct bpf_struct_ops bpf_Qdisc_ops = {
|
||||
.verifier_ops = &bpf_qdisc_verifier_ops,
|
||||
.reg = bpf_qdisc_reg,
|
||||
.unreg = bpf_qdisc_unreg,
|
||||
.init_member = bpf_qdisc_init_member,
|
||||
.init = bpf_qdisc_init,
|
||||
.name = "Qdisc_ops",
|
||||
.cfi_stubs = &__bpf_ops_qdisc_ops,
|
||||
.owner = THIS_MODULE,
|
||||
};
|
||||
|
||||
BTF_ID_LIST(bpf_sk_buff_dtor_ids)
|
||||
BTF_ID(func, bpf_kfree_skb)
|
||||
|
||||
static int __init bpf_qdisc_kfunc_init(void)
|
||||
{
|
||||
int ret;
|
||||
const struct btf_id_dtor_kfunc skb_kfunc_dtors[] = {
|
||||
{
|
||||
.btf_id = bpf_sk_buff_ids[0],
|
||||
.kfunc_btf_id = bpf_sk_buff_dtor_ids[0]
|
||||
},
|
||||
};
|
||||
|
||||
ret = register_btf_kfunc_id_set(BPF_PROG_TYPE_STRUCT_OPS, &bpf_qdisc_kfunc_set);
|
||||
ret = ret ?: register_btf_id_dtor_kfuncs(skb_kfunc_dtors,
|
||||
ARRAY_SIZE(skb_kfunc_dtors),
|
||||
THIS_MODULE);
|
||||
ret = ret ?: register_bpf_struct_ops(&bpf_Qdisc_ops, Qdisc_ops);
|
||||
|
||||
return ret;
|
||||
}
|
||||
late_initcall(bpf_qdisc_kfunc_init);
|
||||
+4
-3
@@ -25,6 +25,7 @@
|
||||
#include <linux/hrtimer.h>
|
||||
#include <linux/slab.h>
|
||||
#include <linux/hashtable.h>
|
||||
#include <linux/bpf.h>
|
||||
|
||||
#include <net/netdev_lock.h>
|
||||
#include <net/net_namespace.h>
|
||||
@@ -359,7 +360,7 @@ static struct Qdisc_ops *qdisc_lookup_ops(struct nlattr *kind)
|
||||
read_lock(&qdisc_mod_lock);
|
||||
for (q = qdisc_base; q; q = q->next) {
|
||||
if (nla_strcmp(kind, q->id) == 0) {
|
||||
if (!try_module_get(q->owner))
|
||||
if (!bpf_try_module_get(q, q->owner))
|
||||
q = NULL;
|
||||
break;
|
||||
}
|
||||
@@ -1370,7 +1371,7 @@ err_out3:
|
||||
netdev_put(dev, &sch->dev_tracker);
|
||||
qdisc_free(sch);
|
||||
err_out2:
|
||||
module_put(ops->owner);
|
||||
bpf_module_put(ops, ops->owner);
|
||||
err_out:
|
||||
*errp = err;
|
||||
return NULL;
|
||||
@@ -1782,7 +1783,7 @@ static void request_qdisc_module(struct nlattr *kind)
|
||||
|
||||
ops = qdisc_lookup_ops(kind);
|
||||
if (ops) {
|
||||
module_put(ops->owner);
|
||||
bpf_module_put(ops, ops->owner);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <linux/if_vlan.h>
|
||||
#include <linux/skb_array.h>
|
||||
#include <linux/if_macvlan.h>
|
||||
#include <linux/bpf.h>
|
||||
#include <net/sch_generic.h>
|
||||
#include <net/pkt_sched.h>
|
||||
#include <net/dst.h>
|
||||
@@ -1078,7 +1079,7 @@ static void __qdisc_destroy(struct Qdisc *qdisc)
|
||||
ops->destroy(qdisc);
|
||||
|
||||
lockdep_unregister_key(&qdisc->root_lock_key);
|
||||
module_put(ops->owner);
|
||||
bpf_module_put(ops, ops->owner);
|
||||
netdev_put(dev, &qdisc->dev_tracker);
|
||||
|
||||
trace_qdisc_destroy(qdisc);
|
||||
|
||||
@@ -1283,6 +1283,7 @@ enum bpf_tc_attach_point {
|
||||
BPF_TC_INGRESS = 1 << 0,
|
||||
BPF_TC_EGRESS = 1 << 1,
|
||||
BPF_TC_CUSTOM = 1 << 2,
|
||||
BPF_TC_QDISC = 1 << 3,
|
||||
};
|
||||
|
||||
#define BPF_TC_PARENT(a, b) \
|
||||
@@ -1297,9 +1298,11 @@ struct bpf_tc_hook {
|
||||
int ifindex;
|
||||
enum bpf_tc_attach_point attach_point;
|
||||
__u32 parent;
|
||||
__u32 handle;
|
||||
const char *qdisc;
|
||||
size_t :0;
|
||||
};
|
||||
#define bpf_tc_hook__last_field parent
|
||||
#define bpf_tc_hook__last_field qdisc
|
||||
|
||||
struct bpf_tc_opts {
|
||||
size_t sz;
|
||||
|
||||
+17
-3
@@ -529,9 +529,9 @@ int bpf_xdp_query_id(int ifindex, int flags, __u32 *prog_id)
|
||||
}
|
||||
|
||||
|
||||
typedef int (*qdisc_config_t)(struct libbpf_nla_req *req);
|
||||
typedef int (*qdisc_config_t)(struct libbpf_nla_req *req, const struct bpf_tc_hook *hook);
|
||||
|
||||
static int clsact_config(struct libbpf_nla_req *req)
|
||||
static int clsact_config(struct libbpf_nla_req *req, const struct bpf_tc_hook *hook)
|
||||
{
|
||||
req->tc.tcm_parent = TC_H_CLSACT;
|
||||
req->tc.tcm_handle = TC_H_MAKE(TC_H_CLSACT, 0);
|
||||
@@ -539,6 +539,16 @@ static int clsact_config(struct libbpf_nla_req *req)
|
||||
return nlattr_add(req, TCA_KIND, "clsact", sizeof("clsact"));
|
||||
}
|
||||
|
||||
static int qdisc_config(struct libbpf_nla_req *req, const struct bpf_tc_hook *hook)
|
||||
{
|
||||
const char *qdisc = OPTS_GET(hook, qdisc, NULL);
|
||||
|
||||
req->tc.tcm_parent = OPTS_GET(hook, parent, TC_H_ROOT);
|
||||
req->tc.tcm_handle = OPTS_GET(hook, handle, 0);
|
||||
|
||||
return nlattr_add(req, TCA_KIND, qdisc, strlen(qdisc) + 1);
|
||||
}
|
||||
|
||||
static int attach_point_to_config(struct bpf_tc_hook *hook,
|
||||
qdisc_config_t *config)
|
||||
{
|
||||
@@ -552,6 +562,9 @@ static int attach_point_to_config(struct bpf_tc_hook *hook,
|
||||
return 0;
|
||||
case BPF_TC_CUSTOM:
|
||||
return -EOPNOTSUPP;
|
||||
case BPF_TC_QDISC:
|
||||
*config = &qdisc_config;
|
||||
return 0;
|
||||
default:
|
||||
return -EINVAL;
|
||||
}
|
||||
@@ -596,7 +609,7 @@ static int tc_qdisc_modify(struct bpf_tc_hook *hook, int cmd, int flags)
|
||||
req.tc.tcm_family = AF_UNSPEC;
|
||||
req.tc.tcm_ifindex = OPTS_GET(hook, ifindex, 0);
|
||||
|
||||
ret = config(&req);
|
||||
ret = config(&req, hook);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
|
||||
@@ -639,6 +652,7 @@ int bpf_tc_hook_destroy(struct bpf_tc_hook *hook)
|
||||
case BPF_TC_INGRESS:
|
||||
case BPF_TC_EGRESS:
|
||||
return libbpf_err(__bpf_tc_detach(hook, NULL, true));
|
||||
case BPF_TC_QDISC:
|
||||
case BPF_TC_INGRESS | BPF_TC_EGRESS:
|
||||
return libbpf_err(tc_qdisc_delete(hook));
|
||||
case BPF_TC_CUSTOM:
|
||||
|
||||
@@ -71,8 +71,10 @@ CONFIG_NET_IPGRE=y
|
||||
CONFIG_NET_IPGRE_DEMUX=y
|
||||
CONFIG_NET_IPIP=y
|
||||
CONFIG_NET_MPLS_GSO=y
|
||||
CONFIG_NET_SCH_BPF=y
|
||||
CONFIG_NET_SCH_FQ=y
|
||||
CONFIG_NET_SCH_INGRESS=y
|
||||
CONFIG_NET_SCH_HTB=y
|
||||
CONFIG_NET_SCHED=y
|
||||
CONFIG_NETDEVSIM=y
|
||||
CONFIG_NETFILTER=y
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
|
||||
#include <linux/pkt_sched.h>
|
||||
#include <linux/rtnetlink.h>
|
||||
#include <test_progs.h>
|
||||
|
||||
#include "network_helpers.h"
|
||||
#include "bpf_qdisc_fifo.skel.h"
|
||||
#include "bpf_qdisc_fq.skel.h"
|
||||
|
||||
#define LO_IFINDEX 1
|
||||
|
||||
static const unsigned int total_bytes = 10 * 1024 * 1024;
|
||||
|
||||
static void do_test(char *qdisc)
|
||||
{
|
||||
DECLARE_LIBBPF_OPTS(bpf_tc_hook, hook, .ifindex = LO_IFINDEX,
|
||||
.attach_point = BPF_TC_QDISC,
|
||||
.parent = TC_H_ROOT,
|
||||
.handle = 0x8000000,
|
||||
.qdisc = qdisc);
|
||||
int srv_fd = -1, cli_fd = -1;
|
||||
int err;
|
||||
|
||||
err = bpf_tc_hook_create(&hook);
|
||||
if (!ASSERT_OK(err, "attach qdisc"))
|
||||
return;
|
||||
|
||||
srv_fd = start_server(AF_INET6, SOCK_STREAM, NULL, 0, 0);
|
||||
if (!ASSERT_OK_FD(srv_fd, "start server"))
|
||||
goto done;
|
||||
|
||||
cli_fd = connect_to_fd(srv_fd, 0);
|
||||
if (!ASSERT_OK_FD(cli_fd, "connect to client"))
|
||||
goto done;
|
||||
|
||||
err = send_recv_data(srv_fd, cli_fd, total_bytes);
|
||||
ASSERT_OK(err, "send_recv_data");
|
||||
|
||||
done:
|
||||
if (srv_fd != -1)
|
||||
close(srv_fd);
|
||||
if (cli_fd != -1)
|
||||
close(cli_fd);
|
||||
|
||||
bpf_tc_hook_destroy(&hook);
|
||||
}
|
||||
|
||||
static void test_fifo(void)
|
||||
{
|
||||
struct bpf_qdisc_fifo *fifo_skel;
|
||||
struct bpf_link *link;
|
||||
|
||||
fifo_skel = bpf_qdisc_fifo__open_and_load();
|
||||
if (!ASSERT_OK_PTR(fifo_skel, "bpf_qdisc_fifo__open_and_load"))
|
||||
return;
|
||||
|
||||
link = bpf_map__attach_struct_ops(fifo_skel->maps.fifo);
|
||||
if (!ASSERT_OK_PTR(link, "bpf_map__attach_struct_ops")) {
|
||||
bpf_qdisc_fifo__destroy(fifo_skel);
|
||||
return;
|
||||
}
|
||||
|
||||
do_test("bpf_fifo");
|
||||
|
||||
bpf_link__destroy(link);
|
||||
bpf_qdisc_fifo__destroy(fifo_skel);
|
||||
}
|
||||
|
||||
static void test_fq(void)
|
||||
{
|
||||
struct bpf_qdisc_fq *fq_skel;
|
||||
struct bpf_link *link;
|
||||
|
||||
fq_skel = bpf_qdisc_fq__open_and_load();
|
||||
if (!ASSERT_OK_PTR(fq_skel, "bpf_qdisc_fq__open_and_load"))
|
||||
return;
|
||||
|
||||
link = bpf_map__attach_struct_ops(fq_skel->maps.fq);
|
||||
if (!ASSERT_OK_PTR(link, "bpf_map__attach_struct_ops")) {
|
||||
bpf_qdisc_fq__destroy(fq_skel);
|
||||
return;
|
||||
}
|
||||
|
||||
do_test("bpf_fq");
|
||||
|
||||
bpf_link__destroy(link);
|
||||
bpf_qdisc_fq__destroy(fq_skel);
|
||||
}
|
||||
|
||||
static void test_qdisc_attach_to_mq(void)
|
||||
{
|
||||
DECLARE_LIBBPF_OPTS(bpf_tc_hook, hook,
|
||||
.attach_point = BPF_TC_QDISC,
|
||||
.parent = TC_H_MAKE(1 << 16, 1),
|
||||
.handle = 0x11 << 16,
|
||||
.qdisc = "bpf_fifo");
|
||||
struct bpf_qdisc_fifo *fifo_skel;
|
||||
struct bpf_link *link;
|
||||
int err;
|
||||
|
||||
fifo_skel = bpf_qdisc_fifo__open_and_load();
|
||||
if (!ASSERT_OK_PTR(fifo_skel, "bpf_qdisc_fifo__open_and_load"))
|
||||
return;
|
||||
|
||||
link = bpf_map__attach_struct_ops(fifo_skel->maps.fifo);
|
||||
if (!ASSERT_OK_PTR(link, "bpf_map__attach_struct_ops")) {
|
||||
bpf_qdisc_fifo__destroy(fifo_skel);
|
||||
return;
|
||||
}
|
||||
|
||||
SYS(out, "ip link add veth0 type veth peer veth1");
|
||||
hook.ifindex = if_nametoindex("veth0");
|
||||
SYS(out, "tc qdisc add dev veth0 root handle 1: mq");
|
||||
|
||||
err = bpf_tc_hook_create(&hook);
|
||||
ASSERT_OK(err, "attach qdisc");
|
||||
|
||||
bpf_tc_hook_destroy(&hook);
|
||||
|
||||
SYS(out, "tc qdisc delete dev veth0 root mq");
|
||||
out:
|
||||
bpf_link__destroy(link);
|
||||
bpf_qdisc_fifo__destroy(fifo_skel);
|
||||
}
|
||||
|
||||
static void test_qdisc_attach_to_non_root(void)
|
||||
{
|
||||
DECLARE_LIBBPF_OPTS(bpf_tc_hook, hook, .ifindex = LO_IFINDEX,
|
||||
.attach_point = BPF_TC_QDISC,
|
||||
.parent = TC_H_MAKE(1 << 16, 1),
|
||||
.handle = 0x11 << 16,
|
||||
.qdisc = "bpf_fifo");
|
||||
struct bpf_qdisc_fifo *fifo_skel;
|
||||
struct bpf_link *link;
|
||||
int err;
|
||||
|
||||
fifo_skel = bpf_qdisc_fifo__open_and_load();
|
||||
if (!ASSERT_OK_PTR(fifo_skel, "bpf_qdisc_fifo__open_and_load"))
|
||||
return;
|
||||
|
||||
link = bpf_map__attach_struct_ops(fifo_skel->maps.fifo);
|
||||
if (!ASSERT_OK_PTR(link, "bpf_map__attach_struct_ops")) {
|
||||
bpf_qdisc_fifo__destroy(fifo_skel);
|
||||
return;
|
||||
}
|
||||
|
||||
SYS(out, "tc qdisc add dev lo root handle 1: htb");
|
||||
SYS(out_del_htb, "tc class add dev lo parent 1: classid 1:1 htb rate 75Kbit");
|
||||
|
||||
err = bpf_tc_hook_create(&hook);
|
||||
if (!ASSERT_ERR(err, "attach qdisc"))
|
||||
bpf_tc_hook_destroy(&hook);
|
||||
|
||||
out_del_htb:
|
||||
SYS(out, "tc qdisc delete dev lo root htb");
|
||||
out:
|
||||
bpf_link__destroy(link);
|
||||
bpf_qdisc_fifo__destroy(fifo_skel);
|
||||
}
|
||||
|
||||
void test_bpf_qdisc(void)
|
||||
{
|
||||
struct netns_obj *netns;
|
||||
|
||||
netns = netns_new("bpf_qdisc_ns", true);
|
||||
if (!ASSERT_OK_PTR(netns, "netns_new"))
|
||||
return;
|
||||
|
||||
if (test__start_subtest("fifo"))
|
||||
test_fifo();
|
||||
if (test__start_subtest("fq"))
|
||||
test_fq();
|
||||
if (test__start_subtest("attach to mq"))
|
||||
test_qdisc_attach_to_mq();
|
||||
if (test__start_subtest("attach to non root"))
|
||||
test_qdisc_attach_to_non_root();
|
||||
|
||||
netns_free(netns);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0 */
|
||||
|
||||
#ifndef _BPF_QDISC_COMMON_H
|
||||
#define _BPF_QDISC_COMMON_H
|
||||
|
||||
#define NET_XMIT_SUCCESS 0x00
|
||||
#define NET_XMIT_DROP 0x01 /* skb dropped */
|
||||
#define NET_XMIT_CN 0x02 /* congestion notification */
|
||||
|
||||
#define TC_PRIO_CONTROL 7
|
||||
#define TC_PRIO_MAX 15
|
||||
|
||||
#define private(name) SEC(".data." #name) __hidden __attribute__((aligned(8)))
|
||||
|
||||
u32 bpf_skb_get_hash(struct sk_buff *p) __ksym;
|
||||
void bpf_kfree_skb(struct sk_buff *p) __ksym;
|
||||
void bpf_qdisc_skb_drop(struct sk_buff *p, struct bpf_sk_buff_ptr *to_free) __ksym;
|
||||
void bpf_qdisc_watchdog_schedule(struct Qdisc *sch, u64 expire, u64 delta_ns) __ksym;
|
||||
void bpf_qdisc_bstats_update(struct Qdisc *sch, const struct sk_buff *skb) __ksym;
|
||||
|
||||
static struct qdisc_skb_cb *qdisc_skb_cb(const struct sk_buff *skb)
|
||||
{
|
||||
return (struct qdisc_skb_cb *)skb->cb;
|
||||
}
|
||||
|
||||
static inline unsigned int qdisc_pkt_len(const struct sk_buff *skb)
|
||||
{
|
||||
return qdisc_skb_cb(skb)->pkt_len;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,117 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
|
||||
#include <vmlinux.h>
|
||||
#include "bpf_experimental.h"
|
||||
#include "bpf_qdisc_common.h"
|
||||
|
||||
char _license[] SEC("license") = "GPL";
|
||||
|
||||
struct skb_node {
|
||||
struct sk_buff __kptr * skb;
|
||||
struct bpf_list_node node;
|
||||
};
|
||||
|
||||
private(A) struct bpf_spin_lock q_fifo_lock;
|
||||
private(A) struct bpf_list_head q_fifo __contains(skb_node, node);
|
||||
|
||||
SEC("struct_ops/bpf_fifo_enqueue")
|
||||
int BPF_PROG(bpf_fifo_enqueue, struct sk_buff *skb, struct Qdisc *sch,
|
||||
struct bpf_sk_buff_ptr *to_free)
|
||||
{
|
||||
struct skb_node *skbn;
|
||||
u32 pkt_len;
|
||||
|
||||
if (sch->q.qlen == sch->limit)
|
||||
goto drop;
|
||||
|
||||
skbn = bpf_obj_new(typeof(*skbn));
|
||||
if (!skbn)
|
||||
goto drop;
|
||||
|
||||
pkt_len = qdisc_pkt_len(skb);
|
||||
|
||||
sch->q.qlen++;
|
||||
skb = bpf_kptr_xchg(&skbn->skb, skb);
|
||||
if (skb)
|
||||
bpf_qdisc_skb_drop(skb, to_free);
|
||||
|
||||
bpf_spin_lock(&q_fifo_lock);
|
||||
bpf_list_push_back(&q_fifo, &skbn->node);
|
||||
bpf_spin_unlock(&q_fifo_lock);
|
||||
|
||||
sch->qstats.backlog += pkt_len;
|
||||
return NET_XMIT_SUCCESS;
|
||||
drop:
|
||||
bpf_qdisc_skb_drop(skb, to_free);
|
||||
return NET_XMIT_DROP;
|
||||
}
|
||||
|
||||
SEC("struct_ops/bpf_fifo_dequeue")
|
||||
struct sk_buff *BPF_PROG(bpf_fifo_dequeue, struct Qdisc *sch)
|
||||
{
|
||||
struct bpf_list_node *node;
|
||||
struct sk_buff *skb = NULL;
|
||||
struct skb_node *skbn;
|
||||
|
||||
bpf_spin_lock(&q_fifo_lock);
|
||||
node = bpf_list_pop_front(&q_fifo);
|
||||
bpf_spin_unlock(&q_fifo_lock);
|
||||
if (!node)
|
||||
return NULL;
|
||||
|
||||
skbn = container_of(node, struct skb_node, node);
|
||||
skb = bpf_kptr_xchg(&skbn->skb, skb);
|
||||
bpf_obj_drop(skbn);
|
||||
if (!skb)
|
||||
return NULL;
|
||||
|
||||
sch->qstats.backlog -= qdisc_pkt_len(skb);
|
||||
bpf_qdisc_bstats_update(sch, skb);
|
||||
sch->q.qlen--;
|
||||
|
||||
return skb;
|
||||
}
|
||||
|
||||
SEC("struct_ops/bpf_fifo_init")
|
||||
int BPF_PROG(bpf_fifo_init, struct Qdisc *sch, struct nlattr *opt,
|
||||
struct netlink_ext_ack *extack)
|
||||
{
|
||||
sch->limit = 1000;
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC("struct_ops/bpf_fifo_reset")
|
||||
void BPF_PROG(bpf_fifo_reset, struct Qdisc *sch)
|
||||
{
|
||||
struct bpf_list_node *node;
|
||||
struct skb_node *skbn;
|
||||
int i;
|
||||
|
||||
bpf_for(i, 0, sch->q.qlen) {
|
||||
struct sk_buff *skb = NULL;
|
||||
|
||||
bpf_spin_lock(&q_fifo_lock);
|
||||
node = bpf_list_pop_front(&q_fifo);
|
||||
bpf_spin_unlock(&q_fifo_lock);
|
||||
|
||||
if (!node)
|
||||
break;
|
||||
|
||||
skbn = container_of(node, struct skb_node, node);
|
||||
skb = bpf_kptr_xchg(&skbn->skb, skb);
|
||||
if (skb)
|
||||
bpf_kfree_skb(skb);
|
||||
bpf_obj_drop(skbn);
|
||||
}
|
||||
sch->q.qlen = 0;
|
||||
}
|
||||
|
||||
SEC(".struct_ops")
|
||||
struct Qdisc_ops fifo = {
|
||||
.enqueue = (void *)bpf_fifo_enqueue,
|
||||
.dequeue = (void *)bpf_fifo_dequeue,
|
||||
.init = (void *)bpf_fifo_init,
|
||||
.reset = (void *)bpf_fifo_reset,
|
||||
.id = "bpf_fifo",
|
||||
};
|
||||
|
||||
@@ -0,0 +1,750 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
|
||||
/* bpf_fq is intended for testing the bpf qdisc infrastructure and not a direct
|
||||
* copy of sch_fq. bpf_fq implements the scheduling algorithm of sch_fq before
|
||||
* 29f834aa326e ("net_sched: sch_fq: add 3 bands and WRR scheduling") was
|
||||
* introduced. It gives each flow a fair chance to transmit packets in a
|
||||
* round-robin fashion. Note that for flow pacing, bpf_fq currently only
|
||||
* respects skb->tstamp but not skb->sk->sk_pacing_rate. In addition, if there
|
||||
* are multiple bpf_fq instances, they will have a shared view of flows and
|
||||
* configuration since some key data structure such as fq_prio_flows,
|
||||
* fq_nonprio_flows, and fq_bpf_data are global.
|
||||
*
|
||||
* To use bpf_fq alone without running selftests, use the following commands.
|
||||
*
|
||||
* 1. Register bpf_fq to the kernel
|
||||
* bpftool struct_ops register bpf_qdisc_fq.bpf.o /sys/fs/bpf
|
||||
* 2. Add bpf_fq to an interface
|
||||
* tc qdisc add dev <interface name> root handle <handle> bpf_fq
|
||||
* 3. Delete bpf_fq attached to the interface
|
||||
* tc qdisc delete dev <interface name> root
|
||||
* 4. Unregister bpf_fq
|
||||
* bpftool struct_ops unregister name fq
|
||||
*
|
||||
* The qdisc name, bpf_fq, used in tc commands is defined by Qdisc_ops.id.
|
||||
* The struct_ops_map_name, fq, used in the bpftool command is the name of the
|
||||
* Qdisc_ops.
|
||||
*
|
||||
* SEC(".struct_ops")
|
||||
* struct Qdisc_ops fq = {
|
||||
* ...
|
||||
* .id = "bpf_fq",
|
||||
* };
|
||||
*/
|
||||
|
||||
#include <vmlinux.h>
|
||||
#include <errno.h>
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include "bpf_experimental.h"
|
||||
#include "bpf_qdisc_common.h"
|
||||
|
||||
char _license[] SEC("license") = "GPL";
|
||||
|
||||
#define NSEC_PER_USEC 1000L
|
||||
#define NSEC_PER_SEC 1000000000L
|
||||
|
||||
#define NUM_QUEUE (1 << 20)
|
||||
|
||||
struct fq_bpf_data {
|
||||
u32 quantum;
|
||||
u32 initial_quantum;
|
||||
u32 flow_refill_delay;
|
||||
u32 flow_plimit;
|
||||
u64 horizon;
|
||||
u32 orphan_mask;
|
||||
u32 timer_slack;
|
||||
u64 time_next_delayed_flow;
|
||||
u64 unthrottle_latency_ns;
|
||||
u8 horizon_drop;
|
||||
u32 new_flow_cnt;
|
||||
u32 old_flow_cnt;
|
||||
u64 ktime_cache;
|
||||
};
|
||||
|
||||
enum {
|
||||
CLS_RET_PRIO = 0,
|
||||
CLS_RET_NONPRIO = 1,
|
||||
CLS_RET_ERR = 2,
|
||||
};
|
||||
|
||||
struct skb_node {
|
||||
u64 tstamp;
|
||||
struct sk_buff __kptr * skb;
|
||||
struct bpf_rb_node node;
|
||||
};
|
||||
|
||||
struct fq_flow_node {
|
||||
int credit;
|
||||
u32 qlen;
|
||||
u64 age;
|
||||
u64 time_next_packet;
|
||||
struct bpf_list_node list_node;
|
||||
struct bpf_rb_node rb_node;
|
||||
struct bpf_rb_root queue __contains(skb_node, node);
|
||||
struct bpf_spin_lock lock;
|
||||
struct bpf_refcount refcount;
|
||||
};
|
||||
|
||||
struct dequeue_nonprio_ctx {
|
||||
bool stop_iter;
|
||||
u64 expire;
|
||||
u64 now;
|
||||
};
|
||||
|
||||
struct remove_flows_ctx {
|
||||
bool gc_only;
|
||||
u32 reset_cnt;
|
||||
u32 reset_max;
|
||||
};
|
||||
|
||||
struct unset_throttled_flows_ctx {
|
||||
bool unset_all;
|
||||
u64 now;
|
||||
};
|
||||
|
||||
struct fq_stashed_flow {
|
||||
struct fq_flow_node __kptr * flow;
|
||||
};
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_HASH);
|
||||
__type(key, __u64);
|
||||
__type(value, struct fq_stashed_flow);
|
||||
__uint(max_entries, NUM_QUEUE);
|
||||
} fq_nonprio_flows SEC(".maps");
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_HASH);
|
||||
__type(key, __u64);
|
||||
__type(value, struct fq_stashed_flow);
|
||||
__uint(max_entries, 1);
|
||||
} fq_prio_flows SEC(".maps");
|
||||
|
||||
private(A) struct bpf_spin_lock fq_delayed_lock;
|
||||
private(A) struct bpf_rb_root fq_delayed __contains(fq_flow_node, rb_node);
|
||||
|
||||
private(B) struct bpf_spin_lock fq_new_flows_lock;
|
||||
private(B) struct bpf_list_head fq_new_flows __contains(fq_flow_node, list_node);
|
||||
|
||||
private(C) struct bpf_spin_lock fq_old_flows_lock;
|
||||
private(C) struct bpf_list_head fq_old_flows __contains(fq_flow_node, list_node);
|
||||
|
||||
private(D) struct fq_bpf_data q;
|
||||
|
||||
/* Wrapper for bpf_kptr_xchg that expects NULL dst */
|
||||
static void bpf_kptr_xchg_back(void *map_val, void *ptr)
|
||||
{
|
||||
void *ret;
|
||||
|
||||
ret = bpf_kptr_xchg(map_val, ptr);
|
||||
if (ret)
|
||||
bpf_obj_drop(ret);
|
||||
}
|
||||
|
||||
static bool skbn_tstamp_less(struct bpf_rb_node *a, const struct bpf_rb_node *b)
|
||||
{
|
||||
struct skb_node *skbn_a;
|
||||
struct skb_node *skbn_b;
|
||||
|
||||
skbn_a = container_of(a, struct skb_node, node);
|
||||
skbn_b = container_of(b, struct skb_node, node);
|
||||
|
||||
return skbn_a->tstamp < skbn_b->tstamp;
|
||||
}
|
||||
|
||||
static bool fn_time_next_packet_less(struct bpf_rb_node *a, const struct bpf_rb_node *b)
|
||||
{
|
||||
struct fq_flow_node *flow_a;
|
||||
struct fq_flow_node *flow_b;
|
||||
|
||||
flow_a = container_of(a, struct fq_flow_node, rb_node);
|
||||
flow_b = container_of(b, struct fq_flow_node, rb_node);
|
||||
|
||||
return flow_a->time_next_packet < flow_b->time_next_packet;
|
||||
}
|
||||
|
||||
static void
|
||||
fq_flows_add_head(struct bpf_list_head *head, struct bpf_spin_lock *lock,
|
||||
struct fq_flow_node *flow, u32 *flow_cnt)
|
||||
{
|
||||
bpf_spin_lock(lock);
|
||||
bpf_list_push_front(head, &flow->list_node);
|
||||
bpf_spin_unlock(lock);
|
||||
*flow_cnt += 1;
|
||||
}
|
||||
|
||||
static void
|
||||
fq_flows_add_tail(struct bpf_list_head *head, struct bpf_spin_lock *lock,
|
||||
struct fq_flow_node *flow, u32 *flow_cnt)
|
||||
{
|
||||
bpf_spin_lock(lock);
|
||||
bpf_list_push_back(head, &flow->list_node);
|
||||
bpf_spin_unlock(lock);
|
||||
*flow_cnt += 1;
|
||||
}
|
||||
|
||||
static void
|
||||
fq_flows_remove_front(struct bpf_list_head *head, struct bpf_spin_lock *lock,
|
||||
struct bpf_list_node **node, u32 *flow_cnt)
|
||||
{
|
||||
bpf_spin_lock(lock);
|
||||
*node = bpf_list_pop_front(head);
|
||||
bpf_spin_unlock(lock);
|
||||
*flow_cnt -= 1;
|
||||
}
|
||||
|
||||
static bool
|
||||
fq_flows_is_empty(struct bpf_list_head *head, struct bpf_spin_lock *lock)
|
||||
{
|
||||
struct bpf_list_node *node;
|
||||
|
||||
bpf_spin_lock(lock);
|
||||
node = bpf_list_pop_front(head);
|
||||
if (node) {
|
||||
bpf_list_push_front(head, node);
|
||||
bpf_spin_unlock(lock);
|
||||
return false;
|
||||
}
|
||||
bpf_spin_unlock(lock);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* flow->age is used to denote the state of the flow (not-detached, detached, throttled)
|
||||
* as well as the timestamp when the flow is detached.
|
||||
*
|
||||
* 0: not-detached
|
||||
* 1 - (~0ULL-1): detached
|
||||
* ~0ULL: throttled
|
||||
*/
|
||||
static void fq_flow_set_detached(struct fq_flow_node *flow)
|
||||
{
|
||||
flow->age = bpf_jiffies64();
|
||||
}
|
||||
|
||||
static bool fq_flow_is_detached(struct fq_flow_node *flow)
|
||||
{
|
||||
return flow->age != 0 && flow->age != ~0ULL;
|
||||
}
|
||||
|
||||
static bool sk_listener(struct sock *sk)
|
||||
{
|
||||
return (1 << sk->__sk_common.skc_state) & (TCPF_LISTEN | TCPF_NEW_SYN_RECV);
|
||||
}
|
||||
|
||||
static void fq_gc(void);
|
||||
|
||||
static int fq_new_flow(void *flow_map, struct fq_stashed_flow **sflow, u64 hash)
|
||||
{
|
||||
struct fq_stashed_flow tmp = {};
|
||||
struct fq_flow_node *flow;
|
||||
int ret;
|
||||
|
||||
flow = bpf_obj_new(typeof(*flow));
|
||||
if (!flow)
|
||||
return -ENOMEM;
|
||||
|
||||
flow->credit = q.initial_quantum,
|
||||
flow->qlen = 0,
|
||||
flow->age = 1,
|
||||
flow->time_next_packet = 0,
|
||||
|
||||
ret = bpf_map_update_elem(flow_map, &hash, &tmp, 0);
|
||||
if (ret == -ENOMEM || ret == -E2BIG) {
|
||||
fq_gc();
|
||||
bpf_map_update_elem(&fq_nonprio_flows, &hash, &tmp, 0);
|
||||
}
|
||||
|
||||
*sflow = bpf_map_lookup_elem(flow_map, &hash);
|
||||
if (!*sflow) {
|
||||
bpf_obj_drop(flow);
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
bpf_kptr_xchg_back(&(*sflow)->flow, flow);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
fq_classify(struct sk_buff *skb, struct fq_stashed_flow **sflow)
|
||||
{
|
||||
struct sock *sk = skb->sk;
|
||||
int ret = CLS_RET_NONPRIO;
|
||||
u64 hash = 0;
|
||||
|
||||
if ((skb->priority & TC_PRIO_MAX) == TC_PRIO_CONTROL) {
|
||||
*sflow = bpf_map_lookup_elem(&fq_prio_flows, &hash);
|
||||
ret = CLS_RET_PRIO;
|
||||
} else {
|
||||
if (!sk || sk_listener(sk)) {
|
||||
hash = bpf_skb_get_hash(skb) & q.orphan_mask;
|
||||
/* Avoid collision with an existing flow hash, which
|
||||
* only uses the lower 32 bits of hash, by setting the
|
||||
* upper half of hash to 1.
|
||||
*/
|
||||
hash |= (1ULL << 32);
|
||||
} else if (sk->__sk_common.skc_state == TCP_CLOSE) {
|
||||
hash = bpf_skb_get_hash(skb) & q.orphan_mask;
|
||||
hash |= (1ULL << 32);
|
||||
} else {
|
||||
hash = sk->__sk_common.skc_hash;
|
||||
}
|
||||
*sflow = bpf_map_lookup_elem(&fq_nonprio_flows, &hash);
|
||||
}
|
||||
|
||||
if (!*sflow)
|
||||
ret = fq_new_flow(&fq_nonprio_flows, sflow, hash) < 0 ?
|
||||
CLS_RET_ERR : CLS_RET_NONPRIO;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool fq_packet_beyond_horizon(struct sk_buff *skb)
|
||||
{
|
||||
return (s64)skb->tstamp > (s64)(q.ktime_cache + q.horizon);
|
||||
}
|
||||
|
||||
SEC("struct_ops/bpf_fq_enqueue")
|
||||
int BPF_PROG(bpf_fq_enqueue, struct sk_buff *skb, struct Qdisc *sch,
|
||||
struct bpf_sk_buff_ptr *to_free)
|
||||
{
|
||||
struct fq_flow_node *flow = NULL, *flow_copy;
|
||||
struct fq_stashed_flow *sflow;
|
||||
u64 time_to_send, jiffies;
|
||||
struct skb_node *skbn;
|
||||
int ret;
|
||||
|
||||
if (sch->q.qlen >= sch->limit)
|
||||
goto drop;
|
||||
|
||||
if (!skb->tstamp) {
|
||||
time_to_send = q.ktime_cache = bpf_ktime_get_ns();
|
||||
} else {
|
||||
if (fq_packet_beyond_horizon(skb)) {
|
||||
q.ktime_cache = bpf_ktime_get_ns();
|
||||
if (fq_packet_beyond_horizon(skb)) {
|
||||
if (q.horizon_drop)
|
||||
goto drop;
|
||||
|
||||
skb->tstamp = q.ktime_cache + q.horizon;
|
||||
}
|
||||
}
|
||||
time_to_send = skb->tstamp;
|
||||
}
|
||||
|
||||
ret = fq_classify(skb, &sflow);
|
||||
if (ret == CLS_RET_ERR)
|
||||
goto drop;
|
||||
|
||||
flow = bpf_kptr_xchg(&sflow->flow, flow);
|
||||
if (!flow)
|
||||
goto drop;
|
||||
|
||||
if (ret == CLS_RET_NONPRIO) {
|
||||
if (flow->qlen >= q.flow_plimit) {
|
||||
bpf_kptr_xchg_back(&sflow->flow, flow);
|
||||
goto drop;
|
||||
}
|
||||
|
||||
if (fq_flow_is_detached(flow)) {
|
||||
flow_copy = bpf_refcount_acquire(flow);
|
||||
|
||||
jiffies = bpf_jiffies64();
|
||||
if ((s64)(jiffies - (flow_copy->age + q.flow_refill_delay)) > 0) {
|
||||
if (flow_copy->credit < q.quantum)
|
||||
flow_copy->credit = q.quantum;
|
||||
}
|
||||
flow_copy->age = 0;
|
||||
fq_flows_add_tail(&fq_new_flows, &fq_new_flows_lock, flow_copy,
|
||||
&q.new_flow_cnt);
|
||||
}
|
||||
}
|
||||
|
||||
skbn = bpf_obj_new(typeof(*skbn));
|
||||
if (!skbn) {
|
||||
bpf_kptr_xchg_back(&sflow->flow, flow);
|
||||
goto drop;
|
||||
}
|
||||
|
||||
skbn->tstamp = skb->tstamp = time_to_send;
|
||||
|
||||
sch->qstats.backlog += qdisc_pkt_len(skb);
|
||||
|
||||
skb = bpf_kptr_xchg(&skbn->skb, skb);
|
||||
if (skb)
|
||||
bpf_qdisc_skb_drop(skb, to_free);
|
||||
|
||||
bpf_spin_lock(&flow->lock);
|
||||
bpf_rbtree_add(&flow->queue, &skbn->node, skbn_tstamp_less);
|
||||
bpf_spin_unlock(&flow->lock);
|
||||
|
||||
flow->qlen++;
|
||||
bpf_kptr_xchg_back(&sflow->flow, flow);
|
||||
|
||||
sch->q.qlen++;
|
||||
return NET_XMIT_SUCCESS;
|
||||
|
||||
drop:
|
||||
bpf_qdisc_skb_drop(skb, to_free);
|
||||
sch->qstats.drops++;
|
||||
return NET_XMIT_DROP;
|
||||
}
|
||||
|
||||
static int fq_unset_throttled_flows(u32 index, struct unset_throttled_flows_ctx *ctx)
|
||||
{
|
||||
struct bpf_rb_node *node = NULL;
|
||||
struct fq_flow_node *flow;
|
||||
|
||||
bpf_spin_lock(&fq_delayed_lock);
|
||||
|
||||
node = bpf_rbtree_first(&fq_delayed);
|
||||
if (!node) {
|
||||
bpf_spin_unlock(&fq_delayed_lock);
|
||||
return 1;
|
||||
}
|
||||
|
||||
flow = container_of(node, struct fq_flow_node, rb_node);
|
||||
if (!ctx->unset_all && flow->time_next_packet > ctx->now) {
|
||||
q.time_next_delayed_flow = flow->time_next_packet;
|
||||
bpf_spin_unlock(&fq_delayed_lock);
|
||||
return 1;
|
||||
}
|
||||
|
||||
node = bpf_rbtree_remove(&fq_delayed, &flow->rb_node);
|
||||
|
||||
bpf_spin_unlock(&fq_delayed_lock);
|
||||
|
||||
if (!node)
|
||||
return 1;
|
||||
|
||||
flow = container_of(node, struct fq_flow_node, rb_node);
|
||||
flow->age = 0;
|
||||
fq_flows_add_tail(&fq_old_flows, &fq_old_flows_lock, flow, &q.old_flow_cnt);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void fq_flow_set_throttled(struct fq_flow_node *flow)
|
||||
{
|
||||
flow->age = ~0ULL;
|
||||
|
||||
if (q.time_next_delayed_flow > flow->time_next_packet)
|
||||
q.time_next_delayed_flow = flow->time_next_packet;
|
||||
|
||||
bpf_spin_lock(&fq_delayed_lock);
|
||||
bpf_rbtree_add(&fq_delayed, &flow->rb_node, fn_time_next_packet_less);
|
||||
bpf_spin_unlock(&fq_delayed_lock);
|
||||
}
|
||||
|
||||
static void fq_check_throttled(u64 now)
|
||||
{
|
||||
struct unset_throttled_flows_ctx ctx = {
|
||||
.unset_all = false,
|
||||
.now = now,
|
||||
};
|
||||
unsigned long sample;
|
||||
|
||||
if (q.time_next_delayed_flow > now)
|
||||
return;
|
||||
|
||||
sample = (unsigned long)(now - q.time_next_delayed_flow);
|
||||
q.unthrottle_latency_ns -= q.unthrottle_latency_ns >> 3;
|
||||
q.unthrottle_latency_ns += sample >> 3;
|
||||
|
||||
q.time_next_delayed_flow = ~0ULL;
|
||||
bpf_loop(NUM_QUEUE, fq_unset_throttled_flows, &ctx, 0);
|
||||
}
|
||||
|
||||
static struct sk_buff*
|
||||
fq_dequeue_nonprio_flows(u32 index, struct dequeue_nonprio_ctx *ctx)
|
||||
{
|
||||
u64 time_next_packet, time_to_send;
|
||||
struct bpf_rb_node *rb_node;
|
||||
struct sk_buff *skb = NULL;
|
||||
struct bpf_list_head *head;
|
||||
struct bpf_list_node *node;
|
||||
struct bpf_spin_lock *lock;
|
||||
struct fq_flow_node *flow;
|
||||
struct skb_node *skbn;
|
||||
bool is_empty;
|
||||
u32 *cnt;
|
||||
|
||||
if (q.new_flow_cnt) {
|
||||
head = &fq_new_flows;
|
||||
lock = &fq_new_flows_lock;
|
||||
cnt = &q.new_flow_cnt;
|
||||
} else if (q.old_flow_cnt) {
|
||||
head = &fq_old_flows;
|
||||
lock = &fq_old_flows_lock;
|
||||
cnt = &q.old_flow_cnt;
|
||||
} else {
|
||||
if (q.time_next_delayed_flow != ~0ULL)
|
||||
ctx->expire = q.time_next_delayed_flow;
|
||||
goto break_loop;
|
||||
}
|
||||
|
||||
fq_flows_remove_front(head, lock, &node, cnt);
|
||||
if (!node)
|
||||
goto break_loop;
|
||||
|
||||
flow = container_of(node, struct fq_flow_node, list_node);
|
||||
if (flow->credit <= 0) {
|
||||
flow->credit += q.quantum;
|
||||
fq_flows_add_tail(&fq_old_flows, &fq_old_flows_lock, flow, &q.old_flow_cnt);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bpf_spin_lock(&flow->lock);
|
||||
rb_node = bpf_rbtree_first(&flow->queue);
|
||||
if (!rb_node) {
|
||||
bpf_spin_unlock(&flow->lock);
|
||||
is_empty = fq_flows_is_empty(&fq_old_flows, &fq_old_flows_lock);
|
||||
if (head == &fq_new_flows && !is_empty) {
|
||||
fq_flows_add_tail(&fq_old_flows, &fq_old_flows_lock, flow, &q.old_flow_cnt);
|
||||
} else {
|
||||
fq_flow_set_detached(flow);
|
||||
bpf_obj_drop(flow);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
skbn = container_of(rb_node, struct skb_node, node);
|
||||
time_to_send = skbn->tstamp;
|
||||
|
||||
time_next_packet = (time_to_send > flow->time_next_packet) ?
|
||||
time_to_send : flow->time_next_packet;
|
||||
if (ctx->now < time_next_packet) {
|
||||
bpf_spin_unlock(&flow->lock);
|
||||
flow->time_next_packet = time_next_packet;
|
||||
fq_flow_set_throttled(flow);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
rb_node = bpf_rbtree_remove(&flow->queue, rb_node);
|
||||
bpf_spin_unlock(&flow->lock);
|
||||
|
||||
if (!rb_node)
|
||||
goto add_flow_and_break;
|
||||
|
||||
skbn = container_of(rb_node, struct skb_node, node);
|
||||
skb = bpf_kptr_xchg(&skbn->skb, skb);
|
||||
bpf_obj_drop(skbn);
|
||||
|
||||
if (!skb)
|
||||
goto add_flow_and_break;
|
||||
|
||||
flow->credit -= qdisc_skb_cb(skb)->pkt_len;
|
||||
flow->qlen--;
|
||||
|
||||
add_flow_and_break:
|
||||
fq_flows_add_head(head, lock, flow, cnt);
|
||||
|
||||
break_loop:
|
||||
ctx->stop_iter = true;
|
||||
return skb;
|
||||
}
|
||||
|
||||
static struct sk_buff *fq_dequeue_prio(void)
|
||||
{
|
||||
struct fq_flow_node *flow = NULL;
|
||||
struct fq_stashed_flow *sflow;
|
||||
struct bpf_rb_node *rb_node;
|
||||
struct sk_buff *skb = NULL;
|
||||
struct skb_node *skbn;
|
||||
u64 hash = 0;
|
||||
|
||||
sflow = bpf_map_lookup_elem(&fq_prio_flows, &hash);
|
||||
if (!sflow)
|
||||
return NULL;
|
||||
|
||||
flow = bpf_kptr_xchg(&sflow->flow, flow);
|
||||
if (!flow)
|
||||
return NULL;
|
||||
|
||||
bpf_spin_lock(&flow->lock);
|
||||
rb_node = bpf_rbtree_first(&flow->queue);
|
||||
if (!rb_node) {
|
||||
bpf_spin_unlock(&flow->lock);
|
||||
goto out;
|
||||
}
|
||||
|
||||
skbn = container_of(rb_node, struct skb_node, node);
|
||||
rb_node = bpf_rbtree_remove(&flow->queue, &skbn->node);
|
||||
bpf_spin_unlock(&flow->lock);
|
||||
|
||||
if (!rb_node)
|
||||
goto out;
|
||||
|
||||
skbn = container_of(rb_node, struct skb_node, node);
|
||||
skb = bpf_kptr_xchg(&skbn->skb, skb);
|
||||
bpf_obj_drop(skbn);
|
||||
|
||||
out:
|
||||
bpf_kptr_xchg_back(&sflow->flow, flow);
|
||||
|
||||
return skb;
|
||||
}
|
||||
|
||||
SEC("struct_ops/bpf_fq_dequeue")
|
||||
struct sk_buff *BPF_PROG(bpf_fq_dequeue, struct Qdisc *sch)
|
||||
{
|
||||
struct dequeue_nonprio_ctx cb_ctx = {};
|
||||
struct sk_buff *skb = NULL;
|
||||
int i;
|
||||
|
||||
if (!sch->q.qlen)
|
||||
goto out;
|
||||
|
||||
skb = fq_dequeue_prio();
|
||||
if (skb)
|
||||
goto dequeue;
|
||||
|
||||
q.ktime_cache = cb_ctx.now = bpf_ktime_get_ns();
|
||||
fq_check_throttled(q.ktime_cache);
|
||||
bpf_for(i, 0, sch->limit) {
|
||||
skb = fq_dequeue_nonprio_flows(i, &cb_ctx);
|
||||
if (cb_ctx.stop_iter)
|
||||
break;
|
||||
};
|
||||
|
||||
if (skb) {
|
||||
dequeue:
|
||||
sch->q.qlen--;
|
||||
sch->qstats.backlog -= qdisc_pkt_len(skb);
|
||||
bpf_qdisc_bstats_update(sch, skb);
|
||||
return skb;
|
||||
}
|
||||
|
||||
if (cb_ctx.expire)
|
||||
bpf_qdisc_watchdog_schedule(sch, cb_ctx.expire, q.timer_slack);
|
||||
out:
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int fq_remove_flows_in_list(u32 index, void *ctx)
|
||||
{
|
||||
struct bpf_list_node *node;
|
||||
struct fq_flow_node *flow;
|
||||
|
||||
bpf_spin_lock(&fq_new_flows_lock);
|
||||
node = bpf_list_pop_front(&fq_new_flows);
|
||||
bpf_spin_unlock(&fq_new_flows_lock);
|
||||
if (!node) {
|
||||
bpf_spin_lock(&fq_old_flows_lock);
|
||||
node = bpf_list_pop_front(&fq_old_flows);
|
||||
bpf_spin_unlock(&fq_old_flows_lock);
|
||||
if (!node)
|
||||
return 1;
|
||||
}
|
||||
|
||||
flow = container_of(node, struct fq_flow_node, list_node);
|
||||
bpf_obj_drop(flow);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern unsigned CONFIG_HZ __kconfig;
|
||||
|
||||
/* limit number of collected flows per round */
|
||||
#define FQ_GC_MAX 8
|
||||
#define FQ_GC_AGE (3*CONFIG_HZ)
|
||||
|
||||
static bool fq_gc_candidate(struct fq_flow_node *flow)
|
||||
{
|
||||
u64 jiffies = bpf_jiffies64();
|
||||
|
||||
return fq_flow_is_detached(flow) &&
|
||||
((s64)(jiffies - (flow->age + FQ_GC_AGE)) > 0);
|
||||
}
|
||||
|
||||
static int
|
||||
fq_remove_flows(struct bpf_map *flow_map, u64 *hash,
|
||||
struct fq_stashed_flow *sflow, struct remove_flows_ctx *ctx)
|
||||
{
|
||||
if (sflow->flow &&
|
||||
(!ctx->gc_only || fq_gc_candidate(sflow->flow))) {
|
||||
bpf_map_delete_elem(flow_map, hash);
|
||||
ctx->reset_cnt++;
|
||||
}
|
||||
|
||||
return ctx->reset_cnt < ctx->reset_max ? 0 : 1;
|
||||
}
|
||||
|
||||
static void fq_gc(void)
|
||||
{
|
||||
struct remove_flows_ctx cb_ctx = {
|
||||
.gc_only = true,
|
||||
.reset_cnt = 0,
|
||||
.reset_max = FQ_GC_MAX,
|
||||
};
|
||||
|
||||
bpf_for_each_map_elem(&fq_nonprio_flows, fq_remove_flows, &cb_ctx, 0);
|
||||
}
|
||||
|
||||
SEC("struct_ops/bpf_fq_reset")
|
||||
void BPF_PROG(bpf_fq_reset, struct Qdisc *sch)
|
||||
{
|
||||
struct unset_throttled_flows_ctx utf_ctx = {
|
||||
.unset_all = true,
|
||||
};
|
||||
struct remove_flows_ctx rf_ctx = {
|
||||
.gc_only = false,
|
||||
.reset_cnt = 0,
|
||||
.reset_max = NUM_QUEUE,
|
||||
};
|
||||
struct fq_stashed_flow *sflow;
|
||||
u64 hash = 0;
|
||||
|
||||
sch->q.qlen = 0;
|
||||
sch->qstats.backlog = 0;
|
||||
|
||||
bpf_for_each_map_elem(&fq_nonprio_flows, fq_remove_flows, &rf_ctx, 0);
|
||||
|
||||
rf_ctx.reset_cnt = 0;
|
||||
bpf_for_each_map_elem(&fq_prio_flows, fq_remove_flows, &rf_ctx, 0);
|
||||
fq_new_flow(&fq_prio_flows, &sflow, hash);
|
||||
|
||||
bpf_loop(NUM_QUEUE, fq_remove_flows_in_list, NULL, 0);
|
||||
q.new_flow_cnt = 0;
|
||||
q.old_flow_cnt = 0;
|
||||
|
||||
bpf_loop(NUM_QUEUE, fq_unset_throttled_flows, &utf_ctx, 0);
|
||||
}
|
||||
|
||||
SEC("struct_ops/bpf_fq_init")
|
||||
int BPF_PROG(bpf_fq_init, struct Qdisc *sch, struct nlattr *opt,
|
||||
struct netlink_ext_ack *extack)
|
||||
{
|
||||
struct net_device *dev = sch->dev_queue->dev;
|
||||
u32 psched_mtu = dev->mtu + dev->hard_header_len;
|
||||
struct fq_stashed_flow *sflow;
|
||||
u64 hash = 0;
|
||||
|
||||
if (fq_new_flow(&fq_prio_flows, &sflow, hash) < 0)
|
||||
return -ENOMEM;
|
||||
|
||||
sch->limit = 10000;
|
||||
q.initial_quantum = 10 * psched_mtu;
|
||||
q.quantum = 2 * psched_mtu;
|
||||
q.flow_refill_delay = 40;
|
||||
q.flow_plimit = 100;
|
||||
q.horizon = 10ULL * NSEC_PER_SEC;
|
||||
q.horizon_drop = 1;
|
||||
q.orphan_mask = 1024 - 1;
|
||||
q.timer_slack = 10 * NSEC_PER_USEC;
|
||||
q.time_next_delayed_flow = ~0ULL;
|
||||
q.unthrottle_latency_ns = 0ULL;
|
||||
q.new_flow_cnt = 0;
|
||||
q.old_flow_cnt = 0;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC(".struct_ops")
|
||||
struct Qdisc_ops fq = {
|
||||
.enqueue = (void *)bpf_fq_enqueue,
|
||||
.dequeue = (void *)bpf_fq_dequeue,
|
||||
.reset = (void *)bpf_fq_reset,
|
||||
.init = (void *)bpf_fq_init,
|
||||
.id = "bpf_fq",
|
||||
};
|
||||
@@ -4,6 +4,8 @@
|
||||
#include <linux/bpf.h>
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <linux/if_ether.h>
|
||||
#include <linux/ip.h>
|
||||
#include <linux/errno.h>
|
||||
#include "xsk_xdp_common.h"
|
||||
|
||||
struct {
|
||||
@@ -14,6 +16,7 @@ struct {
|
||||
} xsk SEC(".maps");
|
||||
|
||||
static unsigned int idx;
|
||||
int adjust_value = 0;
|
||||
int count = 0;
|
||||
|
||||
SEC("xdp.frags") int xsk_def_prog(struct xdp_md *xdp)
|
||||
@@ -70,4 +73,51 @@ SEC("xdp") int xsk_xdp_shared_umem(struct xdp_md *xdp)
|
||||
return bpf_redirect_map(&xsk, idx, XDP_DROP);
|
||||
}
|
||||
|
||||
SEC("xdp.frags") int xsk_xdp_adjust_tail(struct xdp_md *xdp)
|
||||
{
|
||||
__u32 buff_len, curr_buff_len;
|
||||
int ret;
|
||||
|
||||
buff_len = bpf_xdp_get_buff_len(xdp);
|
||||
if (buff_len == 0)
|
||||
return XDP_DROP;
|
||||
|
||||
ret = bpf_xdp_adjust_tail(xdp, adjust_value);
|
||||
if (ret < 0) {
|
||||
/* Handle unsupported cases */
|
||||
if (ret == -EOPNOTSUPP) {
|
||||
/* Set adjust_value to -EOPNOTSUPP to indicate to userspace that this case
|
||||
* is unsupported
|
||||
*/
|
||||
adjust_value = -EOPNOTSUPP;
|
||||
return bpf_redirect_map(&xsk, 0, XDP_DROP);
|
||||
}
|
||||
|
||||
return XDP_DROP;
|
||||
}
|
||||
|
||||
curr_buff_len = bpf_xdp_get_buff_len(xdp);
|
||||
if (curr_buff_len != buff_len + adjust_value)
|
||||
return XDP_DROP;
|
||||
|
||||
if (curr_buff_len > buff_len) {
|
||||
__u32 *pkt_data = (void *)(long)xdp->data;
|
||||
__u32 len, words_to_end, seq_num;
|
||||
|
||||
len = curr_buff_len - PKT_HDR_ALIGN;
|
||||
words_to_end = len / sizeof(*pkt_data) - 1;
|
||||
seq_num = words_to_end;
|
||||
|
||||
/* Convert sequence number to network byte order. Store this in the last 4 bytes of
|
||||
* the packet. Use 'adjust_value' to determine the position at the end of the
|
||||
* packet for storing the sequence number.
|
||||
*/
|
||||
seq_num = __constant_htonl(words_to_end);
|
||||
bpf_xdp_store_bytes(xdp, curr_buff_len - sizeof(seq_num), &seq_num,
|
||||
sizeof(seq_num));
|
||||
}
|
||||
|
||||
return bpf_redirect_map(&xsk, 0, XDP_DROP);
|
||||
}
|
||||
|
||||
char _license[] SEC("license") = "GPL";
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#define XSK_XDP_COMMON_H_
|
||||
|
||||
#define MAX_SOCKETS 2
|
||||
#define PKT_HDR_ALIGN (sizeof(struct ethhdr) + 2) /* Just to align the data in the packet */
|
||||
|
||||
struct xdp_info {
|
||||
__u64 count;
|
||||
|
||||
@@ -524,6 +524,8 @@ static void __test_spec_init(struct test_spec *test, struct ifobject *ifobj_tx,
|
||||
test->nb_sockets = 1;
|
||||
test->fail = false;
|
||||
test->set_ring = false;
|
||||
test->adjust_tail = false;
|
||||
test->adjust_tail_support = false;
|
||||
test->mtu = MAX_ETH_PKT_SIZE;
|
||||
test->xdp_prog_rx = ifobj_rx->xdp_progs->progs.xsk_def_prog;
|
||||
test->xskmap_rx = ifobj_rx->xdp_progs->maps.xsk;
|
||||
@@ -757,14 +759,15 @@ static struct pkt_stream *pkt_stream_clone(struct pkt_stream *pkt_stream)
|
||||
return pkt_stream_generate(pkt_stream->nb_pkts, pkt_stream->pkts[0].len);
|
||||
}
|
||||
|
||||
static void pkt_stream_replace_ifobject(struct ifobject *ifobj, u32 nb_pkts, u32 pkt_len)
|
||||
{
|
||||
ifobj->xsk->pkt_stream = pkt_stream_generate(nb_pkts, pkt_len);
|
||||
}
|
||||
|
||||
static void pkt_stream_replace(struct test_spec *test, u32 nb_pkts, u32 pkt_len)
|
||||
{
|
||||
struct pkt_stream *pkt_stream;
|
||||
|
||||
pkt_stream = pkt_stream_generate(nb_pkts, pkt_len);
|
||||
test->ifobj_tx->xsk->pkt_stream = pkt_stream;
|
||||
pkt_stream = pkt_stream_generate(nb_pkts, pkt_len);
|
||||
test->ifobj_rx->xsk->pkt_stream = pkt_stream;
|
||||
pkt_stream_replace_ifobject(test->ifobj_tx, nb_pkts, pkt_len);
|
||||
pkt_stream_replace_ifobject(test->ifobj_rx, nb_pkts, pkt_len);
|
||||
}
|
||||
|
||||
static void __pkt_stream_replace_half(struct ifobject *ifobj, u32 pkt_len,
|
||||
@@ -991,6 +994,31 @@ static bool is_metadata_correct(struct pkt *pkt, void *buffer, u64 addr)
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool is_adjust_tail_supported(struct xsk_xdp_progs *skel_rx)
|
||||
{
|
||||
struct bpf_map *data_map;
|
||||
int adjust_value = 0;
|
||||
int key = 0;
|
||||
int ret;
|
||||
|
||||
data_map = bpf_object__find_map_by_name(skel_rx->obj, "xsk_xdp_.bss");
|
||||
if (!data_map || !bpf_map__is_internal(data_map)) {
|
||||
ksft_print_msg("Error: could not find bss section of XDP program\n");
|
||||
exit_with_error(errno);
|
||||
}
|
||||
|
||||
ret = bpf_map_lookup_elem(bpf_map__fd(data_map), &key, &adjust_value);
|
||||
if (ret) {
|
||||
ksft_print_msg("Error: bpf_map_lookup_elem failed with error %d\n", ret);
|
||||
exit_with_error(errno);
|
||||
}
|
||||
|
||||
/* Set the 'adjust_value' variable to -EOPNOTSUPP in the XDP program if the adjust_tail
|
||||
* helper is not supported. Skip the adjust_tail test case in this scenario.
|
||||
*/
|
||||
return adjust_value != -EOPNOTSUPP;
|
||||
}
|
||||
|
||||
static bool is_frag_valid(struct xsk_umem_info *umem, u64 addr, u32 len, u32 expected_pkt_nb,
|
||||
u32 bytes_processed)
|
||||
{
|
||||
@@ -1767,8 +1795,13 @@ static void *worker_testapp_validate_rx(void *arg)
|
||||
|
||||
if (!err && ifobject->validation_func)
|
||||
err = ifobject->validation_func(ifobject);
|
||||
if (err)
|
||||
report_failure(test);
|
||||
|
||||
if (err) {
|
||||
if (test->adjust_tail && !is_adjust_tail_supported(ifobject->xdp_progs))
|
||||
test->adjust_tail_support = false;
|
||||
else
|
||||
report_failure(test);
|
||||
}
|
||||
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
@@ -2515,6 +2548,71 @@ static int testapp_hw_sw_max_ring_size(struct test_spec *test)
|
||||
return testapp_validate_traffic(test);
|
||||
}
|
||||
|
||||
static int testapp_xdp_adjust_tail(struct test_spec *test, int adjust_value)
|
||||
{
|
||||
struct xsk_xdp_progs *skel_rx = test->ifobj_rx->xdp_progs;
|
||||
struct xsk_xdp_progs *skel_tx = test->ifobj_tx->xdp_progs;
|
||||
|
||||
test_spec_set_xdp_prog(test, skel_rx->progs.xsk_xdp_adjust_tail,
|
||||
skel_tx->progs.xsk_xdp_adjust_tail,
|
||||
skel_rx->maps.xsk, skel_tx->maps.xsk);
|
||||
|
||||
skel_rx->bss->adjust_value = adjust_value;
|
||||
|
||||
return testapp_validate_traffic(test);
|
||||
}
|
||||
|
||||
static int testapp_adjust_tail(struct test_spec *test, u32 value, u32 pkt_len)
|
||||
{
|
||||
int ret;
|
||||
|
||||
test->adjust_tail_support = true;
|
||||
test->adjust_tail = true;
|
||||
test->total_steps = 1;
|
||||
|
||||
pkt_stream_replace_ifobject(test->ifobj_tx, DEFAULT_BATCH_SIZE, pkt_len);
|
||||
pkt_stream_replace_ifobject(test->ifobj_rx, DEFAULT_BATCH_SIZE, pkt_len + value);
|
||||
|
||||
ret = testapp_xdp_adjust_tail(test, value);
|
||||
if (ret)
|
||||
return ret;
|
||||
|
||||
if (!test->adjust_tail_support) {
|
||||
ksft_test_result_skip("%s %sResize pkt with bpf_xdp_adjust_tail() not supported\n",
|
||||
mode_string(test), busy_poll_string(test));
|
||||
return TEST_SKIP;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int testapp_adjust_tail_shrink(struct test_spec *test)
|
||||
{
|
||||
/* Shrink by 4 bytes for testing purpose */
|
||||
return testapp_adjust_tail(test, -4, MIN_PKT_SIZE * 2);
|
||||
}
|
||||
|
||||
static int testapp_adjust_tail_shrink_mb(struct test_spec *test)
|
||||
{
|
||||
test->mtu = MAX_ETH_JUMBO_SIZE;
|
||||
/* Shrink by the frag size */
|
||||
return testapp_adjust_tail(test, -XSK_UMEM__MAX_FRAME_SIZE, XSK_UMEM__LARGE_FRAME_SIZE * 2);
|
||||
}
|
||||
|
||||
static int testapp_adjust_tail_grow(struct test_spec *test)
|
||||
{
|
||||
/* Grow by 4 bytes for testing purpose */
|
||||
return testapp_adjust_tail(test, 4, MIN_PKT_SIZE * 2);
|
||||
}
|
||||
|
||||
static int testapp_adjust_tail_grow_mb(struct test_spec *test)
|
||||
{
|
||||
test->mtu = MAX_ETH_JUMBO_SIZE;
|
||||
/* Grow by (frag_size - last_frag_Size) - 1 to stay inside the last fragment */
|
||||
return testapp_adjust_tail(test, (XSK_UMEM__MAX_FRAME_SIZE / 2) - 1,
|
||||
XSK_UMEM__LARGE_FRAME_SIZE * 2);
|
||||
}
|
||||
|
||||
static void run_pkt_test(struct test_spec *test)
|
||||
{
|
||||
int ret;
|
||||
@@ -2621,6 +2719,10 @@ static const struct test_spec tests[] = {
|
||||
{.name = "TOO_MANY_FRAGS", .test_func = testapp_too_many_frags},
|
||||
{.name = "HW_SW_MIN_RING_SIZE", .test_func = testapp_hw_sw_min_ring_size},
|
||||
{.name = "HW_SW_MAX_RING_SIZE", .test_func = testapp_hw_sw_max_ring_size},
|
||||
{.name = "XDP_ADJUST_TAIL_SHRINK", .test_func = testapp_adjust_tail_shrink},
|
||||
{.name = "XDP_ADJUST_TAIL_SHRINK_MULTI_BUFF", .test_func = testapp_adjust_tail_shrink_mb},
|
||||
{.name = "XDP_ADJUST_TAIL_GROW", .test_func = testapp_adjust_tail_grow},
|
||||
{.name = "XDP_ADJUST_TAIL_GROW_MULTI_BUFF", .test_func = testapp_adjust_tail_grow_mb},
|
||||
};
|
||||
|
||||
static void print_tests(void)
|
||||
|
||||
@@ -173,6 +173,8 @@ struct test_spec {
|
||||
u16 nb_sockets;
|
||||
bool fail;
|
||||
bool set_ring;
|
||||
bool adjust_tail;
|
||||
bool adjust_tail_support;
|
||||
enum test_mode mode;
|
||||
char name[MAX_TEST_NAME_SIZE];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user