diff --git a/.gitmodules b/.gitmodules index 8d14a926a1..f65efb66cb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "klibc"] path = klibc url = https://github.com/luainkernel/klibc.git +[submodule "examples/workload/scx"] + path = examples/workload/scx + url = https://github.com/sched-ext/scx.git diff --git a/Kconfig b/Kconfig index f540a07337..e6796dc521 100644 --- a/Kconfig +++ b/Kconfig @@ -73,6 +73,12 @@ config LUNATIK_SET Compact immutable set of strings: exact membership, plus a suffix-matched labeled flavor. +config LUNATIK_TASK + tristate "Lunatik Task Support" + default m + help + Kernel task management from Lua. + config LUNATIK_THREAD tristate "Lunatik Thread Support" default m @@ -111,6 +117,12 @@ config LUNATIK_XDP help Express Data Path (XDP) support for high-performance packet processing. +config LUNATIK_TC + tristate "Lunatik TC Support" + default m + help + Traffic Controller (TC) support for high-performance packet scheduling. + config LUNATIK_FIFO tristate "Lunatik FIFO Support" default m @@ -150,6 +162,12 @@ config LUNATIK_CPU help CPU management and information. +config LUNATIK_SCHED + tristate "Lunatik Scheduler Support" + default m + help + Extensible Linux Scheduler support for custom process scheduling. + config LUNATIK_HID tristate "Lunatik HID (Human Interface Device) Support" default m @@ -176,4 +194,4 @@ config LUNATIK_BPF Access to pinned eBPF maps from Lua scripts. endif - + diff --git a/Makefile b/Makefile index 2f35aca660..bc5281332d 100644 --- a/Makefile +++ b/Makefile @@ -32,8 +32,8 @@ CONFIG_LUNATIK_RUN ?= m # Order matters: modules are loaded left-to-right and unloaded right-to-left (rmmod). # A module must appear AFTER all modules it depends on (e.g. SKB before NETFILTER). -LUNATIK_MODULES := DEVICE LINUX NOTIFIER SOCKET NETLINK RCU SET THREAD DATA PROBE SYSCALL XDP FIFO SKB NETFILTER \ - COMPLETION CRYPTO CPU HID SIGNAL BYTEORDER DARKEN BPF +LUNATIK_MODULES := DEVICE LINUX NOTIFIER SOCKET NETLINK RCU SET TASK THREAD DATA PROBE SYSCALL XDP FIFO \ + SKB TC NETFILTER COMPLETION CRYPTO CPU HID SIGNAL BYTEORDER DARKEN BPF SCHED $(foreach c,$(LUNATIK_MODULES),\ $(eval CONFIG_LUNATIK_$(c) ?= m)) @@ -83,6 +83,7 @@ scripts_install: ${MKDIR} ${SCRIPTS_INSTALL_PATH}/netlink ${MKDIR} ${SCRIPTS_INSTALL_PATH}/netlink/rt ${MKDIR} ${SCRIPTS_INSTALL_PATH}/netlink/nl80211 + ${MKDIR} ${SCRIPTS_INSTALL_PATH}/skb ${MKDIR} ${SCRIPTS_INSTALL_PATH}/syscall ${MKDIR} ${SCRIPTS_INSTALL_PATH}/crypto ${MKDIR} ${SCRIPTS_INSTALL_PATH}/linux @@ -100,6 +101,7 @@ scripts_install: ${INSTALL} -m 0644 lib/netlink/*.lua ${SCRIPTS_INSTALL_PATH}/netlink ${INSTALL} -m 0644 lib/netlink/rt/*.lua ${SCRIPTS_INSTALL_PATH}/netlink/rt ${INSTALL} -m 0644 lib/netlink/nl80211/*.lua ${SCRIPTS_INSTALL_PATH}/netlink/nl80211 + ${INSTALL} -m 0644 lib/skb/*.lua ${SCRIPTS_INSTALL_PATH}/skb ${INSTALL} -m 0644 lib/syscall/*.lua ${SCRIPTS_INSTALL_PATH}/syscall ${INSTALL} -m 0644 lib/crypto/*.lua ${SCRIPTS_INSTALL_PATH}/crypto # NOTE: `lib/linux/` exists only as LDoc stubs (see doc-stubs); never install it. @@ -128,10 +130,16 @@ scripts_uninstall: ebpf: ${MAKE} -C examples/filter + ${MAKE} -C examples/sniclassify + ${MAKE} -C examples/workload + ${MAKE} -C examples/qos ebpf_install: ${MKDIR} ${LUNATIK_EBPF_INSTALL_PATH} ${INSTALL} -m 0644 examples/filter/https.o ${LUNATIK_EBPF_INSTALL_PATH}/ + ${INSTALL} -m 0644 examples/sniclassify/classify.o ${LUNATIK_EBPF_INSTALL_PATH}/ + ${INSTALL} -m 0644 examples/workload/scheduler.o ${LUNATIK_EBPF_INSTALL_PATH}/ + ${INSTALL} -m 0644 examples/qos/classify.o ${LUNATIK_EBPF_INSTALL_PATH}/ ebpf_uninstall: ${RM} -r ${LUNATIK_EBPF_INSTALL_PATH} diff --git a/README.md b/README.md index 50e04fd421..840d953650 100644 --- a/README.md +++ b/README.md @@ -468,6 +468,46 @@ ip netns exec tcpreject curl --connect-timeout 2 https://[2001:4860:4860::8888] sudo examples/tcpreject/cleanup.sh ``` +### sniclassify + +[sniclassify](examples/sniclassify) is a kernel extension composed by +a TC/eBPF classifier program attached on egress, +a Lua kernel script to classify [SNI](https://datatracker.ietf.org/doc/html/rfc3546#section-3.1) traffic. +This kernel extension extracts server name and assigns traffic +classes according to a Lua [policy table](examples/sniclassify/sni.lua#18). + +Install and load the classfier: + +```sh +sudo make btf_install # needed to export the 'bpf_luatc_run' kfunc +sudo make examples_install # installs examples +make ebpf # builds the TC/eBPF program +sudo make ebpf_install # installs the TC/eBPF program +sudo lunatik run examples/sniclassify/sni softirq +``` + +Configure HTB classes: +``` +sudo tc qdisc del dev docker0 root 2>/dev/null +sudo tc qdisc add dev docker0 root handle 1: htb default 30 +sudo tc class add dev docker0 parent 1: classid 1:10 htb rate 20mbit +sudo tc class add dev docker0 parent 1: classid 1:20 htb rate 10mbit +``` + +Attach the TC/eBPF classifier on egress: +``` +sudo tc filter add dev docker0 parent 1: bpf da obj examples/sniclassify/classify.o sec classifier +``` + +The classifier inspects outbound TLS ClientHello packets, extracts the SNI +field, and assigns a traffic class according to the Lua policy table. + +Verify and test: +``` +sudo tc filter show dev docker0 +sudo journalctl -ft kernel +``` + ### gesture [gesture](examples/gesture.lua) @@ -554,6 +594,37 @@ cpu_usage_idle{cpu="cpu0"} 100.0000000000000000 1764094519529162 ... ``` +### workload scheduler + +[workload](examples/workload) is a scheduler composed by +[sched_ext/scx](https://github.com/sched-ext/scx) framework and eBPF. It includes +and eBPF program which uses a eBPF map to assign queues and slices to tasks and +a Lua kernel script to set dispatch queue and slice to the tasks seen for the first time +according to a Lua [policy table](examples/workload/workload.lua#13). + +Install and load the scheduler: + +```sh +sudo make btf_install # needed to export the 'bpf_luasched_run' kfunc +sudo make examples_install # installs examples +make ebpf # builds the sched_ext/eBPF program +sudo make ebpf_install # installs the sched_ext/eBPF program +sudo lunatik run examples/workload/workload hardirq +``` + +Load and attach the struct_ops scheduler: + +```sh +sudo bpftool struct_ops register examples/workload/scheduler.o /sys/fs/bpf/luasched +``` + +Verify and test: + +```sh +sudo bpftool struct_ops show +sudo journalctl -ft kernel +``` + ## References ### Talks and Papers diff --git a/autogen.lua b/autogen.lua index affa246a2d..a57a3bdcec 100644 --- a/autogen.lua +++ b/autogen.lua @@ -375,6 +375,23 @@ local function intermediate_paths(mods) return util.sorted(needs) end +local function to_lua_number(val) + -- Negative decimal: assembler sign-extended a u64 with high bit set. + -- Convert to the equivalent hex literal so Lua parses the correct + -- bit pattern without floating point precision loss. + local neg = val:match("^%-(%d+)$") + if neg then + local n = math.tointeger(neg) + if n == nil then + -- Magnitude itself overflows: only -2^63 can do this, + -- which is math.mininteger. + return string.format("0x%016X", math.mininteger) + end + return string.format("0x%016X", -n) + end + return val +end + -- Write one sub-table block: init line (unless this is the top itself) -- followed by sorted entries. local function write_submodule(out, mod, top) @@ -382,7 +399,7 @@ local function write_submodule(out, mod, top) if mod.name ~= top then out:write(mod.name, " = {}\n") end table.sort(mod.entries, function(a, b) return a.key < b.key end) for _, e in ipairs(mod.entries) do - out:write(mod.name, '["', e.key, '"]\t= ', e.value, "\n") + out:write(mod.name, '["', e.key, '"]\t= ', to_lua_number(e.value), "\n") end end diff --git a/autogen/specs.lua b/autogen/specs.lua index 0a1421237d..a33f4c6ad0 100644 --- a/autogen/specs.lua +++ b/autogen/specs.lua @@ -40,8 +40,12 @@ return { desc = "BPF map types and update flags.", include = { "ANY", "NOEXIST", "EXIST", "MAP_TYPE_HASH", "MAP_TYPE_ARRAY", "MAP_TYPE_LRU_HASH", "MAP_TYPE_QUEUE", "MAP_TYPE_STACK" } }, + { header = "uapi/linux/pkt_cls.h", prefix = "TC_", module = "tc", + desc = "TC verdicts and flags." }, { header = "linux/sched.h", prefix = "TASK_", module = "task", desc = "Task state flags." }, + { header = "linux/sched/ext.h", prefix = "SCX_", module = "scx", + desc = "Extensible scheduler flags." }, { header = "linux/net.h", prefix = "SOCK_", module = "socket.sock", desc = "Socket types (SOCK_STREAM, SOCK_DGRAM, ...)." }, { header = "linux/socket.h", prefix = "AF_", module = "socket.af", diff --git a/config.ld b/config.ld index 4f7eca371f..6571ac300b 100644 --- a/config.ld +++ b/config.ld @@ -54,6 +54,7 @@ file = { './lib/luanetfilter.c', './lib/luanetlink.c', './lib/luaskb.c', + './lib/skb/attr.lua', './lib/luanotifier.c', './lib/luaprobe.c', './lib/luarcu.c', diff --git a/examples/filter/sni.lua b/examples/filter/sni.lua index b34dc7a5e0..60127042c9 100644 --- a/examples/filter/sni.lua +++ b/examples/filter/sni.lua @@ -48,11 +48,14 @@ local server_name = 0x00 local session = 43 local max_extensions = 17 -local function filter_sni(packet, argument) +local function filter_sni(ctx) + local packet = ctx:packet() + local argument = ctx:argument() local byte, short, str = unpacker(packet, offset(argument)) if byte(0) ~= handshake or byte(5) ~= client_hello then - return action.PASS + ctx:action(action.PASS) + return end local cipher = (session + 1) + byte(session) @@ -67,12 +70,13 @@ local function filter_sni(packet, argument) verdict = blacklist[sni] and "DROP" or "PASS" log(sni, verdict) - return action[verdict] + ctx:action(action[verdict]) + return end extension = data + short(extension + 2) end - return action.PASS + ctx:action(action.PASS) end xdp.attach(filter_sni) diff --git a/examples/qos/Makefile b/examples/qos/Makefile new file mode 100644 index 0000000000..373ad3601e --- /dev/null +++ b/examples/qos/Makefile @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: (c) 2026 Ashwani Kumar Kamal +# SPDX-License-Identifier: MIT OR GPL-2.0-only + +all: vmlinux classify.o + +vmlinux: + bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h + +classify.o: classify.c + clang -target bpf -Wall -O2 -c -g $< + +clean: + rm -f vmlinux.h classify.o + diff --git a/examples/qos/classify.c b/examples/qos/classify.c new file mode 100644 index 0000000000..0e690cbe83 --- /dev/null +++ b/examples/qos/classify.c @@ -0,0 +1,24 @@ +/* +* SPDX-FileCopyrightText: (c) 2026 Ashwani Kumar Kamal +* SPDX-License-Identifier: MIT OR GPL-2.0-only +*/ + +#include "vmlinux.h" +#include +#include + +extern int bpf_luatc_run(char *key, size_t key__sz, struct __sk_buff *skb, void *arg, size_t arg__sz) __ksym; + +static char runtime[] = "examples/qos/tc"; + +int const TC_ACT_OK = 0; + +SEC("classifier") +int classify(struct __sk_buff *skb) +{ + int action = bpf_luatc_run(runtime, sizeof(runtime), skb, NULL, 0); + return action < 0 ? TC_ACT_OK : action; +} + +char _license[] SEC("license") = "Dual MIT/GPL"; + diff --git a/examples/qos/tc.lua b/examples/qos/tc.lua new file mode 100644 index 0000000000..733e0ce169 --- /dev/null +++ b/examples/qos/tc.lua @@ -0,0 +1,60 @@ +-- +-- SPDX-FileCopyrightText: (c) 2026 Ashwani Kumar Kamal +-- SPDX-License-Identifier: MIT OR GPL-2.0-only +-- + +local tc = require("tc") +local action = require("linux.tc") +local skbattr = require("skb.attr") +local map = require("ebpf.map") + +local TC_H_MAKE = function(maj, min) return (maj << 16) | min end + +local stats = map.open("/sys/fs/bpf/flow_stats") + +-- struct flow_stats { +-- u64 packets; +-- u32 avg_pkt_size; +-- }; + +local KEY_FMT = " +# SPDX-License-Identifier: MIT OR GPL-2.0-only + +all: vmlinux classify.o + +vmlinux: + bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h + +classify.o: classify.c + clang -target bpf -Wall -O2 -c -g $< + +clean: + rm -f vmlinux.h classify.o + diff --git a/examples/sniclassify/classify.c b/examples/sniclassify/classify.c new file mode 100644 index 0000000000..3ece7213b9 --- /dev/null +++ b/examples/sniclassify/classify.c @@ -0,0 +1,69 @@ +/* +* SPDX-FileCopyrightText: (c) 2026 Ashwani Kumar Kamal +* SPDX-License-Identifier: MIT OR GPL-2.0-only +*/ + +#include "vmlinux.h" +#include +#include + +extern int bpf_luatc_run(char *key, size_t key__sz, struct __sk_buff *skb, void *arg, size_t arg__sz) __ksym; + +static char runtime[] = "examples/sniclassify/sni"; + +int const TC_ACT_OK = 0; + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 65536); + __type(key, __u32); + __type(value, __u32); +} flow_cache SEC(".maps"); + +struct bpf_luatc_arg { + __u16 offset; +} __attribute__((packed)); + +SEC("classifier") +int classify(struct __sk_buff *skb) +{ + __u32 key = skb->hash; + + __u32 *priority= bpf_map_lookup_elem(&flow_cache, &key); + if (priority) { + skb->priority = *priority; + return TC_ACT_OK; + } + + struct bpf_luatc_arg arg; + void *data_end = (void *)(long)skb->data_end; + void *data = (void *)(long)skb->data; + struct iphdr *ip = data + sizeof(struct ethhdr); + + if (ip + 1 > (struct iphdr *)data_end) + goto pass; + + if (ip->protocol != IPPROTO_TCP) + goto pass; + + struct tcphdr *tcp = (void *)ip + (ip->ihl * 4); + if (tcp + 1 > (struct tcphdr *)data_end) + goto pass; + + if (bpf_ntohs(tcp->dest) != 443 || !tcp->psh) + goto pass; + + void *payload = (void *)tcp + (tcp->doff * 4); + if (payload > data_end) + goto pass; + + arg.offset = bpf_htons((__u16)(payload - data)); + + int action = bpf_luatc_run(runtime, sizeof(runtime), skb, &arg, sizeof(arg)); + return action < 0 ? TC_ACT_OK : action; +pass: + return TC_ACT_OK; +} + +char _license[] SEC("license") = "Dual MIT/GPL"; + diff --git a/examples/sniclassify/sni.lua b/examples/sniclassify/sni.lua new file mode 100644 index 0000000000..f897d4fbaf --- /dev/null +++ b/examples/sniclassify/sni.lua @@ -0,0 +1,87 @@ +-- +-- SPDX-FileCopyrightText: (c) 2026 Ashwani Kumar Kamal +-- SPDX-FileCopyrightText: (c) 2024-2026 Ring Zero Desenvolvimento de Software LTDA +-- SPDX-License-Identifier: MIT OR GPL-2.0-only +-- +-- Based on https://github.com/luainkernel/lunatik/blob/master/examples/filter/sni.lua + +local tc = require("tc") +local action = require("linux.tc") +local skbattr = require("skb.attr") + +local TC_H_MAKE = function(maj, min) return (maj << 16) | min end + +local client_hello = 0x01 +local handshake = 0x16 +local server_name = 0x0000 + +local session = 43 +local max_extensions = 17 + +local policy = { + ["netflix%.com"] = TC_H_MAKE(1, 0x20), + ["zoom%.com"] = TC_H_MAKE(1, 0x10), +} + +local function log(sni, priority) + print(string.format("sniclassify: %s %s", sni, priority)) +end + +local function unpacker(packet, base) + local byte = function (offset) + return packet:getbyte(base + offset) + end + + local short = function (offset) + local offset = base + offset + return packet:getbyte(offset) << 8 | packet:getbyte(offset + 1) + end + + local str = function (offset, length) + return packet:getstring(base + offset, length) + end + + return byte, short, str +end + +local function offset(argument) + return select(2, unpacker(argument, 0))(0) +end + +local function sniclassify(ctx) + local argument = ctx:argument() + local skb = skbattr(ctx:skb()) + local data = skb:data() + local byte, short, str = unpacker(data, offset(argument)) + + if byte(0) ~= handshake or byte(5) ~= client_hello then + ctx:action(action.ACT_OK) + return + end + + local cipher = (session + 1) + byte(session) + local compression = cipher + 2 + short(cipher) + local extension = compression + 3 + byte(compression) + + for _ = 1, max_extensions do + local data_off = extension + 4 + if short(extension) == server_name then + local sni = str(data_off + 5, short(data_off + 3)) + for pattern, classid in pairs(policy) do + if sni:match(pattern) then + log(sni, classid) + skb.priority = classid + break + end + end + ctx:action(action.ACT_OK) + return + end + extension = data_off + short(extension + 2) + end + + ctx:action(action.ACT_OK) +end + +tc.attach(sniclassify) + diff --git a/examples/workload/Makefile b/examples/workload/Makefile new file mode 100644 index 0000000000..9678132fed --- /dev/null +++ b/examples/workload/Makefile @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: (c) 2026 Ashwani Kumar Kamal +# SPDX-License-Identifier: MIT OR GPL-2.0-only +# + +all: vmlinux scheduler.o + +vmlinux: + bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h + +scheduler.o: scheduler.c + clang -target bpf -Wall -O2 \ + -D__SCX_COMPAT_BPF_H \ + -Wno-visibility \ + -I. -I./scx/scheds/include -c -g $< + +clean: + rm -f vmlinux.h scheduler.o + diff --git a/examples/workload/scheduler.c b/examples/workload/scheduler.c new file mode 100644 index 0000000000..ad9d880366 --- /dev/null +++ b/examples/workload/scheduler.c @@ -0,0 +1,89 @@ +/* +* SPDX-FileCopyrightText: (c) 2026 Ashwani Kumar Kamal +* SPDX-License-Identifier: MIT OR GPL-2.0-only +*/ + +#include "vmlinux.h" +#include +#include +#include +#include + +#define DSQ_REALTIME 0 +#define DSQ_BATCH 1 +#define DSQ_DEFAULT 2 + +struct task_class { + u64 dsq; + u64 slice_ns; +}; + +static char runtime[] = "examples/workload/workload"; + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 10240); + __type(key, pid_t); + __type(value, struct task_class); +} task_classes SEC(".maps"); + +extern int bpf_luasched_run(const char *key, size_t key__sz, struct task_struct *task, struct task_class *cls) __ksym; + +s32 BPF_STRUCT_OPS_SLEEPABLE(luasched_init) +{ + scx_bpf_create_dsq(DSQ_REALTIME, -1); + scx_bpf_create_dsq(DSQ_BATCH, -1); + scx_bpf_create_dsq(DSQ_DEFAULT, -1); + return 0; +} + +void BPF_STRUCT_OPS(luasched_dispatch, s32 cpu, struct task_struct *prev) +{ + if (!scx_bpf_dsq_move_to_local(DSQ_REALTIME)) { + if (!scx_bpf_dsq_move_to_local(DSQ_BATCH)) { + scx_bpf_dsq_move_to_local(DSQ_DEFAULT); + } + } +} + +void BPF_STRUCT_OPS(luasched_enqueue, struct task_struct *p, u64 enq_flags) +{ + pid_t pid = p->pid; + struct task_class *cls; + + cls = bpf_map_lookup_elem(&task_classes, &pid); + if (cls) { + scx_bpf_dsq_insert(p, cls->dsq, cls->slice_ns, enq_flags); + return; + } + + struct task_class received_cls = { .dsq = DSQ_DEFAULT, .slice_ns = SCX_SLICE_DFL }; + + int ret = bpf_luasched_run(runtime, sizeof(runtime), p, &received_cls); + + if (ret) { + received_cls.dsq = DSQ_DEFAULT; + received_cls.slice_ns = SCX_SLICE_DFL; + } + + bpf_map_update_elem(&task_classes, &pid, &received_cls, BPF_ANY); + scx_bpf_dsq_insert(p, received_cls.dsq, received_cls.slice_ns, enq_flags); +} + +void BPF_STRUCT_OPS(luasched_exit_task, struct task_struct *p, struct scx_exit_task_args *args) +{ + pid_t pid = p->pid; + bpf_map_delete_elem(&task_classes, &pid); +} + +SEC(".struct_ops") +struct sched_ext_ops luasched_ops = { + .init = (void *)luasched_init, + .dispatch = (void *)luasched_dispatch, + .enqueue = (void *)luasched_enqueue, + .exit_task = (void *)luasched_exit_task, + .name = "luasched", +}; + +char _license[] SEC("license") = "GPL"; + diff --git a/examples/workload/scx b/examples/workload/scx new file mode 160000 index 0000000000..95d7b9856a --- /dev/null +++ b/examples/workload/scx @@ -0,0 +1 @@ +Subproject commit 95d7b9856a0da604902e3aabb4e5d41080bc1cf6 diff --git a/examples/workload/workload.lua b/examples/workload/workload.lua new file mode 100644 index 0000000000..33e67bd39b --- /dev/null +++ b/examples/workload/workload.lua @@ -0,0 +1,37 @@ +-- +-- SPDX-FileCopyrightText: (c) 2026 Ashwani Kumar Kamal +-- SPDX-License-Identifier: MIT OR GPL-2.0-only +-- + +local sched = require("sched") +local scx = require("linux.scx") + +local REALTIME = 0 +local BATCH = 1 +local DEFAULT = 2 + +local policy = { + { pattern = "^nginx", dsq = REALTIME, slice = 1000000 }, -- 1ms + { pattern = "^firefox", dsq = BATCH, slice = 10000000 }, -- 10ms +} + +local function log(command, dsq, slice) + print(string.format("workload: [%s]: %d %d", command, dsq, slice)) +end + +local function workload(ctx) + local task = ctx:task() + for _, rule in ipairs(policy) do + if task:comm():match(rule.pattern) then + ctx:dsq(rule.dsq) + ctx:slice(rule.slice) + log(task:comm(), rule.dsq, rule.slice) + return + end + end + ctx:dsq(DEFAULT) + ctx:slice(scx.SLICE_DFL) +end + +sched.attach(workload) + diff --git a/lib/Kbuild b/lib/Kbuild index 9fb71b5ff6..ef04702b68 100644 --- a/lib/Kbuild +++ b/lib/Kbuild @@ -13,6 +13,7 @@ obj-$(CONFIG_LUNATIK_DATA) += luadata.o obj-$(CONFIG_LUNATIK_PROBE) += luaprobe.o obj-$(CONFIG_LUNATIK_SYSCALL) += luasyscall.o obj-$(CONFIG_LUNATIK_XDP) += luaxdp.o +obj-$(CONFIG_LUNATIK_TC) += luatc.o obj-$(CONFIG_LUNATIK_FIFO) += luafifo.o obj-$(CONFIG_LUNATIK_NETFILTER) += luanetfilter.o obj-$(CONFIG_LUNATIK_COMPLETION) += luacompletion.o @@ -26,4 +27,6 @@ obj-$(CONFIG_LUNATIK_BYTEORDER) += luabyteorder.o obj-$(CONFIG_LUNATIK_DARKEN) += luadarken.o obj-$(CONFIG_LUNATIK_SKB) += luaskb.o obj-$(CONFIG_LUNATIK_BPF) += luabpf.o +obj-$(CONFIG_LUNATIK_TASK) += luatask.o +obj-$(CONFIG_LUNATIK_SCHED) += luasched.o diff --git a/lib/luasched.c b/lib/luasched.c new file mode 100644 index 0000000000..f1b12ba4cf --- /dev/null +++ b/lib/luasched.c @@ -0,0 +1,301 @@ +/* +* SPDX-FileCopyrightText: (c) 2026 Ashwani Kumar Kamal +* SPDX-License-Identifier: MIT OR GPL-2.0-only +*/ + +/*** +* Linux Exensible Scheduler (sched_ext) integration. +* This library allows Lua scripts to interact with the kernel's sched_ext subsystem. +* It enables sched_ext/eBPF programs to call Lua functions for task scheduling, +* providing a flexible way to implement custom scheduling logic in Lua. +* +* The primary mechanism involves an sched_ext program calling the `bpf_luasched_run` +* kfunc, which in turn invokes a Lua callback function previously registered +* using `sched.attach()`. +* @module sched +*/ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt +#include +#include + +#include +#include + +#include "luarcu.h" +#include "luatask.h" + +#if (LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)) +#include +#include +#include + +LUNATIK_EBPF_START(); + +static char luasched_env_key; + +static lunatik_object_t *luasched_runtimes = NULL; + +typedef struct luasched_ctx_s { + struct task_struct *task; + u64 *dsq; + u64 *slice_ns; + int callback_ref; +} luasched_ctx_t; + +LUNATIK_PRIVATECHECKER(luasched_ctx_check, luasched_ctx_t *, + luaL_argcheck(L, private->task != NULL, ix, "ctx is not set"); +); + +/*** +* Returns the object for the current task. +* @function task +* @treturn task +*/ +static int luasched_task(lua_State *L) +{ + luasched_ctx_t *ctx = luasched_ctx_check(L, 1); + lunatik_object_t *task = luatask_new(L, ctx->task); + lunatik_pushobject(L, task); + return 1; +} + +/*** +* Sets the sched_ext dispatch queue for this task. +* @function dsq +* @tparam integer dispatch queue to set for the task +*/ +static int luasched_dsq(lua_State *L) +{ + luasched_ctx_t *ctx = luasched_ctx_check(L, 1); + *ctx->dsq = luaL_checkinteger(L, 2); + return 0; +} + +/*** +* Sets the sched_ext slice in nanoseconds for this task. +* @function dsq +* @tparam integer slice in ns to set for the task +*/ +static int luasched_slice_ns(lua_State *L) +{ + luasched_ctx_t *ctx = luasched_ctx_check(L, 1); + *ctx->slice_ns = luaL_checkinteger(L, 2); + return 0; +} + +static const luaL_Reg luasched_mt[] = { + {"__gc", lunatik_deleteobject}, + {"task", luasched_task}, + {"dsq", luasched_dsq}, + {"slice", luasched_slice_ns}, + {NULL, NULL} +}; + +static void luasched_release(void *private) +{ + luasched_ctx_t *lctx = (luasched_ctx_t *)private; + if (lctx->task != NULL) { + put_task_struct(lctx->task); + lctx->task = NULL; + } +} + +LUNATIK_OPENER(sched); +static const lunatik_class_t luasched_class = { + .name = "sched.ctx", + .methods = luasched_mt, + .release = luasched_release, + .opener = luaopen_sched, + .opt = LUNATIK_OPT_HARDIRQ, +}; + +static void luasched_handler_cleanup(luasched_ctx_t *lctx) +{ + put_task_struct(lctx->task); + lctx->task = NULL; + lctx->dsq = NULL; + lctx->slice_ns = NULL; +} + +static int luasched_handler(lua_State *L, luasched_ctx_t *ctx) +{ + luasched_ctx_t *lctx; + lunatik_bpf_get_env(L, &luasched_env_key, lctx); + get_task_struct(ctx->task); + + lctx->task = ctx->task; + lctx->dsq = ctx->dsq; + lctx->slice_ns = ctx->slice_ns; + + lua_rawgeti(L, LUA_REGISTRYINDEX, lctx->callback_ref); + if (!lua_isfunction(L, -1)) { + lua_pop(L, 2); + pr_err("callback_ref is not a valid function\n"); + luasched_handler_cleanup(lctx); + return -1; + } + + lua_insert(L, -2); + if (lua_pcall(L, 1, 0, 0) != LUA_OK) { + pr_err("%s\n", lua_tostring(L, -1)); + lua_pop(L, 1); + luasched_handler_cleanup(lctx); + return -1; + } + + luasched_handler_cleanup(lctx); + return 0; +} + +struct task_class { + u64 dsq; + u64 slice_ns; +}; + +__bpf_kfunc int bpf_luasched_run(char *key, size_t key__sz, struct task_struct *task, struct task_class *cls) +{ + u64 dsq = SCX_DSQ_GLOBAL; + u64 slice_ns = SCX_SLICE_DFL; + int ret; + + if (!cls) + return -EINVAL; + + lunatik_object_t *runtime = lunatik_ebpf_lookup(luasched_runtimes, key, key__sz); + if (runtime == NULL) + return -ENOENT; + + luasched_ctx_t ctx = { + .task = task, + .dsq = &dsq, + .slice_ns = &slice_ns, + }; + + lunatik_run(runtime, luasched_handler, ret, &ctx); + lunatik_putobject(runtime); + cls->dsq = dsq; + cls->slice_ns = slice_ns; + return ret; +} + +LUNATIK_EBPF_END(); + +LUNATIK_EBPF_KFUNC_DEFINE_SET(sched, bpf_luasched_run); + +/*** +* Unregisters the Lua callback function associated with the current Lunatik runtime. +* After calling this, `bpf_luasched_run` calls targeting this runtime will no longer +* invoke a Lua function (they will likely return an error or default action). +* @function detach +* @treturn nil +* @usage +* sched.detach() +* @within sched +*/ +static int luasched_detach(lua_State *L) +{ + luasched_ctx_t *lctx; + lunatik_bpf_get_env(L, &luasched_env_key, lctx); + luaL_unref(L, LUA_REGISTRYINDEX, lctx->callback_ref); + lctx->callback_ref = LUA_NOREF; + lua_pop(L, 1); + lunatik_unregister(L, &luasched_env_key); + return 0; +} + +/*** +* Registers a Lua callback function to be invoked by a sched_ext eBPF program. +* When an XDP program calls the `bpf_luasched_run` kfunc, Lunatik will execute +* the registered Lua `callback` associated with the current Lunatik runtime. +* The runtime invoking this function must be non-sleepable. +* +* The `bpf_luasched_run` kfunc is called from an eBPF program with the following signature: +* `int bpf_luasched_run(const char *key, size_t key__sz, struct task_struct *task, struct task_class *cls)` +* +* - `key`: A string identifying the Lunatik runtime (e.g., the script name like "examples/workload/workload"). +* This key is used to look up the runtime in Lunatik's internal table of active runtimes. +* - `key_sz`: Length of the key string (including the null terminator). +* - `task`: The task context (`struct task_struct *`). +* - `cls`: The scheduling decision (dsq and slice_ns). +* +* @function attach +* @tparam function callback Lua function to call. It receives one argument: +* +* 1. `ctx` (sched.ctx userdata): A context object used to inspect the task +* and control the sched_ext dsq via `ctx:dsq()`. +* +* The callback **must not return a value**. If no dsq is set, the default +* is SCX_DSQ_GLOBAL. +* @treturn nil +* @raise Error if the current runtime is sleepable or if internal setup fails. +* @usage +* -- Lua script (e.g., "my_sched_handler.lua" which is run via `lunatik run my_sched_handler.lua`) +* local sched = require("sched") +* local scx = require("linux.scx") +* +* local function my_task_processor(ctx) +* local task = ctx:task() +* if task:comm() == "bash" then +* ctx:dsq(scx.DSQ_LOCAL) +* ctx:slice(scx.SLICE_DFL) +* end +* return +* end +* sched.attach(my_task_processor) +* +* -- In eBPF C code, to call the above Lua function: +* -- char rt_key[] = "my_sched_handler.lua"; // Key matches the script name +* -- int ret = bpf_luasched_run(rt_key, sizeof(rt_key), p, cls); +* @see data +* @within sched +*/ +static int luasched_attach(lua_State *L) +{ + lunatik_checkruntime(L, LUNATIK_OPT_HARDIRQ); + luaL_checktype(L, 1, LUA_TFUNCTION); /* callback */ + + lunatik_object_t *object = lunatik_newobject(L, &luasched_class, sizeof(luasched_ctx_t), LUNATIK_OPT_NONE); + luasched_ctx_t *ctx = (luasched_ctx_t *)object->private; + + lua_pushvalue(L, 1); + ctx->callback_ref = luaL_ref(L, LUA_REGISTRYINDEX); + + lua_pushvalue(L, -1); + + lunatik_register(L, -1, &luasched_env_key); + lua_pop(L, 1); + + return 0; +} +#endif + +static const luaL_Reg luasched_lib[] = { +#if (LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)) + {"attach", luasched_attach}, + {"detach", luasched_detach}, +#endif + {NULL, NULL} +}; + +LUNATIK_CLASSES(sched, &luasched_class); +LUNATIK_NEWLIB(sched, luasched_lib, luasched_classes); + +static int __init luasched_init(void) +{ + LUNATIK_EBPF_KFUNC_INIT(sched, BPF_PROG_TYPE_STRUCT_OPS); +} + +static void __exit luasched_exit(void) +{ +#if (LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)) + if (luasched_runtimes != NULL) + lunatik_putobject(luasched_runtimes); +#endif +} + +module_init(luasched_init); +module_exit(luasched_exit); +MODULE_LICENSE("Dual MIT/GPL"); +MODULE_AUTHOR("Ashwani Kumar Kamal "); + diff --git a/lib/luaskb.c b/lib/luaskb.c index 9e3dff2007..1727641efe 100644 --- a/lib/luaskb.c +++ b/lib/luaskb.c @@ -228,6 +228,57 @@ static int luaskb_connmark(lua_State *L) } #endif /* CONFIG_NF_CONNTRACK_MARK */ +#define luaskb_getinteger(name, field) \ +static int luaskb_get##name(lua_State *l) \ +{ \ + luaskb_t *lskb = luaskb_check(l, 1); \ + lua_pushinteger(l, lskb->skb->field); \ + return 1; \ +} + +#define luaskb_setinteger(name, field) \ +static int luaskb_set##name(lua_State *l) \ +{ \ + luaskb_t *lskb = luaskb_check(l, 1); \ + lskb->skb->field = (u32)luaL_checkinteger(l, 2); \ + return 0; \ +} + +/*** +* Gets the packet mark. +* @function getmark +* @return skb->mark +*/ +luaskb_getinteger(mark, mark); + +/*** +* Sets the packet mark. +* @function setmark +* @param mark New packet mark value +*/ +luaskb_setinteger(mark, mark); + +/*** +* Gets the packet priority. +* @function getpriority +* @return skb->priority +*/ +luaskb_getinteger(priority, priority); + +/*** +* Sets the packet priority. +* @function setpriority +* @param priority New packet priority value +*/ +luaskb_setinteger(priority, priority); + +/*** +* Gets the packet hash. +* @function gethash +* @return skb->hash +*/ +luaskb_getinteger(hash, hash); + static int luaskb_copy(lua_State *L); static void luaskb_release(void *private) @@ -244,21 +295,27 @@ static const luaL_Reg luaskb_lib[] = { }; static const luaL_Reg luaskb_mt[] = { - {"__gc", lunatik_deleteobject}, - {"__len", luaskb_len}, - {"ifindex", luaskb_ifindex}, - {"vlan", luaskb_vlan}, - {"data", luaskb_data}, - {"resize", luaskb_resize}, - {"checksum", luaskb_checksum}, - {"forward", luaskb_forward}, - {"copy", luaskb_copy}, + {"__gc", lunatik_deleteobject}, + {"__len", luaskb_len}, + {"ifindex", luaskb_ifindex}, + {"vlan", luaskb_vlan}, + {"data", luaskb_data}, + {"resize", luaskb_resize}, + {"checksum", luaskb_checksum}, + {"forward", luaskb_forward}, + {"copy", luaskb_copy}, #if defined(CONFIG_NF_CONNTRACK_MARK) - {"connmark", luaskb_connmark}, + {"connmark", luaskb_connmark}, #endif + {"getmark", luaskb_getmark}, + {"getpriority", luaskb_getpriority}, + {"setmark", luaskb_setmark}, + {"setpriority", luaskb_setpriority}, + {"gethash", luaskb_gethash}, {NULL, NULL} }; + LUNATIK_OPENER(skb); static const lunatik_class_t luaskb_class = { .name = "skb", diff --git a/lib/luatask.c b/lib/luatask.c new file mode 100644 index 0000000000..d832c4ca6b --- /dev/null +++ b/lib/luatask.c @@ -0,0 +1,196 @@ +/* +* SPDX-FileCopyrightText: (c) 2026 Ashwani Kumar Kamal +* SPDX-License-Identifier: MIT OR GPL-2.0-only +*/ + +/*** +* Linux task interface. +* @module task +*/ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt +#include +#include +#include + +#include "luatask.h" + +LUNATIK_PRIVATECHECKER(luatask_check, struct task_struct *, + luaL_argcheck(L, private != NULL, ix, "task is not set"); +); + +/*** +* Returns the command name (comm) of the task, i.e., the executable. +* This is truncated to TASK_COMM_LEN (16) characters by the kernel. +* @function comm +* @treturn string command name of the task +*/ +static int luatask_comm(lua_State *L) +{ + struct task_struct *task = luatask_check(L, 1); + lua_pushstring(L, task->comm); + return 1; +} + +/*** +* Returns the process ID (PID) of the task. +* For threads, this is the thread ID (TID) unique to each thread. +* @function pid +* @treturn integer pid of the task +*/ +static int luatask_pid(lua_State *L) +{ + struct task_struct *task = luatask_check(L, 1); + lua_pushinteger(L, task->pid); + return 1; +} + +/*** +* Returns the thread group ID (TGID) of the task. +* For the main thread, TGID equals PID. For other threads in the group, +* TGID is the PID of the main thread. +* @function tgid +* @treturn integer tgid of the task +*/ +static int luatask_tgid(lua_State *L) +{ + struct task_struct *task = luatask_check(L, 1); + lua_pushinteger(L, task->tgid); + return 1; +} + +/*** +* Returns the dynamic priority of the task. +* Ranges from 0 (highest) to 139 (lowest); normal tasks are 100-139, +* real-time tasks are 0-99. +* @function prio +* @treturn integer priority of the task +*/ +static int luatask_prio(lua_State *L) +{ + struct task_struct *task = luatask_check(L, 1); + lua_pushinteger(L, task->prio); + return 1; +} + +/*** +* Returns whether the task is currently running on a CPU. +* Only available on SMP systems. +* @function cpu +* @treturn integer 1 if a task is actively occupying a CPU core, else 0. +*/ +#ifdef CONFIG_SMP +static int luatask_cpu(lua_State *L) +{ + struct task_struct *task = luatask_check(L, 1); + lua_pushinteger(L, task->on_cpu); + return 1; +} +#endif + +/*** +* Gets an object representing the current kernel task. +* @function current +* @treturn task task object for the current task. +* @usage +* local task = require("task") +* local t = task.current() +* print(t:pid(), t:comm()) +*/ +static int luatask_current(lua_State *L) +{ + lunatik_object_t *object = luatask_new(L, current); + lunatik_pushobject(L, object); + return 1; +} + +static void luatask_release(void *private) +{ + struct task_struct *task = (struct task_struct *)private; + if (task) { + put_task_struct(task); + task = NULL; + } +} + +static const luaL_Reg luatask_lib[] = { + {"current", luatask_current}, + {NULL, NULL} +}; + +static const luaL_Reg luatask_mt[] = { + {"__gc", lunatik_deleteobject}, + {"comm", luatask_comm}, + {"pid", luatask_pid}, + {"tgid", luatask_tgid}, + {"prio", luatask_prio}, +#ifdef CONFIG_SMP + {"cpu", luatask_cpu}, +#endif + {NULL, NULL} +}; + +LUNATIK_OPENER(task); +static const lunatik_class_t luatask_class = { + .name = "task", + .methods = luatask_mt, + .release = luatask_release, + .opener = luaopen_task, + .opt = LUNATIK_OPT_SOFTIRQ, +}; + +lunatik_object_t *luatask_new(lua_State *L, struct task_struct *task) +{ + lunatik_require(L, &luatask_class); + if (!task) + return NULL; + lunatik_object_t *object = lunatik_newobject(L, &luatask_class, sizeof(struct task_struct *), LUNATIK_OPT_NONE); + get_task_struct(task); + object->private = task; + lunatik_getobject(object); + lua_pop(L, 1); + return object; +} +EXPORT_SYMBOL(luatask_new); + +int luatask_stop(lunatik_object_t *object) +{ + struct task_struct *task; + if (!object) + return -ESRCH; + task = (struct task_struct *)object->private; + if (!task) + return -ESRCH; + return kthread_stop(task); +} +EXPORT_SYMBOL(luatask_stop); + +lunatik_object_t *luatask_run(lua_State *L, int (*threadfn)(void *data), void *data, const char *name) +{ + struct task_struct *task; + + task = kthread_run(threadfn, data, name); + if (IS_ERR(task)) + return ERR_CAST(task); + + return luatask_new(L, task); +} +EXPORT_SYMBOL(luatask_run); + +LUNATIK_CLASSES(task, &luatask_class); +LUNATIK_NEWLIB(task, luatask_lib, luatask_classes); + +static int __init luatask_init(void) +{ + return 0; +} + +static void __exit luatask_exit(void) +{ +} + +module_init(luatask_init); +module_exit(luatask_exit); +MODULE_LICENSE("Dual MIT/GPL"); +MODULE_AUTHOR("Ashwani Kumar Kamal "); + diff --git a/lib/luatask.h b/lib/luatask.h new file mode 100644 index 0000000000..7f4747fd61 --- /dev/null +++ b/lib/luatask.h @@ -0,0 +1,16 @@ +/* +* SPDX-FileCopyrightText: (c) 2026 Ashwani Kumar Kamal +* SPDX-License-Identifier: MIT OR GPL-2.0-only +*/ + +#ifndef luatask_h +#define luatask_h + +#include + +lunatik_object_t *luatask_new(lua_State *L, struct task_struct *task); +int luatask_stop(lunatik_object_t *object); +lunatik_object_t *luatask_run(lua_State *L, int (*threadfn)(void *data), void *data, const char *name); + +#endif + diff --git a/lib/luatc.c b/lib/luatc.c new file mode 100644 index 0000000000..043b55ba4f --- /dev/null +++ b/lib/luatc.c @@ -0,0 +1,307 @@ +/* +* SPDX-FileCopyrightText: (c) 2026 Ashwani Kumar Kamal +* SPDX-License-Identifier: MIT OR GPL-2.0-only +*/ + +/*** +* Linux Traffic Controller (TC) integration. +* This library allows Lua scripts to interact with the kernel's TC subsystem. +* It enables TC/eBPF programs to call Lua functions for packet processing, +* traffic shaping, filtering, and policy enforcement, providing a flexible +* way to implement custom networking logic in Lua at the ingress and egress +* layers of network stack. +* +* The primary mechanism involves an TC program calling the `bpf_luatc_run` +* kfunc, which in turn invokes a Lua callback function previously registered +* using `tc.attach()`. +* @module tc +*/ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt +#include + +#include +#include + +#include "luarcu.h" +#include "luadata.h" +#include "luaskb.h" + +#if (LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)) +#include +#include +#include + +LUNATIK_EBPF_START(); + +static char luatc_env_key; + +static lunatik_object_t *luatc_runtimes = NULL; + +typedef struct luatc_ctx_s { + struct __sk_buff *skb; + void *arg; + size_t arg__sz; + int *action; + lunatik_object_t *argument; + lunatik_object_t *skb_obj; + int callback_ref; +} luatc_ctx_t; + +LUNATIK_PRIVATECHECKER(luatc_ctx_check, luatc_ctx_t *, + luaL_argcheck(L, private->skb != NULL, ix, "ctx is not set"); +); + +/*** +* Returns the packet object for the current TC context. +* @function skb +* @treturn data +*/ +static int luatc_skb(lua_State *L) +{ + luatc_ctx_t *ctx = luatc_ctx_check(L, 1); + lunatik_getregistry(L, ctx->skb_obj); + return 1; +} + +/*** +* Returns the argument data buffer passed from eBPF. +* @function argument +* @treturn data +*/ +static int luatc_arg(lua_State *L) +{ + luatc_ctx_t *ctx = luatc_ctx_check(L, 1); + lunatik_getregistry(L, ctx->argument); + return 1; +} + +/*** +* Sets the TC verdict action for this packet. +* @function action +* @tparam integer action TC action constant (e.g. TC_ACT_OK, TC_ACT_SHOT, ...) +*/ +static int luatc_action(lua_State *L) +{ + luatc_ctx_t *ctx = luatc_ctx_check(L, 1); + *ctx->action = luaL_checkinteger(L, 2); + return 0; +} + +static const luaL_Reg luatc_mt[] = { + {"__gc", lunatik_deleteobject}, + {"skb", luatc_skb}, + {"argument", luatc_arg}, + {"action", luatc_action}, + {NULL, NULL} +}; + +static void luatc_release(void *private) +{ + luatc_ctx_t *lctx = (luatc_ctx_t *)private; + if (lctx->skb_obj) + luaskb_clear(lctx->skb_obj); + if (lctx->argument) + luadata_close(lctx->argument); +} + +LUNATIK_OPENER(tc); +static const lunatik_class_t luatc_class = { + .name = "tc.ctx", + .methods = luatc_mt, + .release = luatc_release, + .opener = luaopen_tc, + .opt = LUNATIK_OPT_SOFTIRQ | LUNATIK_OPT_SINGLE, +}; + +static void luatc_handler_cleanup(luatc_ctx_t *lctx) +{ + luaskb_t *lskb = (luaskb_t *)lctx->skb_obj->private; + luadata_clear(lctx->argument); + lskb->skb = NULL; + lctx->action = NULL; +} + +static int luatc_handler(lua_State *L, luatc_ctx_t *ctx) +{ + luatc_ctx_t *lctx; + lunatik_bpf_get_env(L, &luatc_env_key, lctx); + luaskb_t *lskb = (luaskb_t *)lctx->skb_obj->private; + + lskb->skb = (struct sk_buff *)ctx->skb; + + lctx->skb = ctx->skb; + lctx->arg = ctx->arg; + lctx->arg__sz = ctx->arg__sz; + lctx->action = ctx->action; + + luadata_reset(lctx->argument, lctx->arg, lctx->arg__sz, LUADATA_OPT_KEEP); + + lua_rawgeti(L, LUA_REGISTRYINDEX, lctx->callback_ref); + if (!lua_isfunction(L, -1)) { + lua_pop(L, 2); + pr_err("callback_ref is not a function\n"); + luatc_handler_cleanup(lctx); + return -1; + } + + lua_insert(L, -2); + if (lua_pcall(L, 1, 0, 0) != LUA_OK) { + pr_err("%s\n", lua_tostring(L, -1)); + lua_pop(L, 1); + luatc_handler_cleanup(lctx); + return -1; + } + + luatc_handler_cleanup(lctx); + return 0; +} + +__bpf_kfunc int bpf_luatc_run(char *key, size_t key__sz, struct __sk_buff *skb, void *arg, size_t arg__sz) +{ + int action = -1; + + lunatik_object_t *runtime = lunatik_ebpf_lookup(luatc_runtimes, key, key__sz); + if (runtime == NULL) + goto out; + + luatc_ctx_t ctx = { + .skb = skb, + .arg = arg, + .arg__sz = arg__sz, + .action = &action, + }; + + lunatik_run(runtime, luatc_handler, action, &ctx); + lunatik_putobject(runtime); +out: + return action; +} + +LUNATIK_EBPF_END(); + +LUNATIK_EBPF_KFUNC_DEFINE_SET(tc, bpf_luatc_run); + +/*** +* Unregisters the Lua callback function associated with the current Lunatik runtime. +* After calling this, `bpf_luatc_run` calls targeting this runtime will no longer +* invoke a Lua function (they will likely return an error or default action). +* @function detach +* @treturn nil +* @usage +* tc.detach() +* @within tc +*/ +static int luatc_detach(lua_State *L) +{ + luatc_ctx_t *lctx; + lunatik_bpf_get_env(L, &luatc_env_key, lctx); + luaL_unref(L, LUA_REGISTRYINDEX, lctx->callback_ref); + lctx->callback_ref = LUA_NOREF; + lua_pop(L, 1); + lunatik_unregister(L, &luatc_env_key); + return 0; +} + +/*** +* Registers a Lua callback function to be invoked by an TC/eBPF program. +* When an TC program calls the `bpf_luatc_run` kfunc, Lunatik will execute +* the registered Lua `callback` associated with the current Lunatik runtime. +* The runtime invoking this function must be non-sleepable. +* +* The `bpf_luatc_run` kfunc is called from an eBPF program with the following signature: +* `int bpf_luatc_run(char *key, size_t key__sz, struct __sk_buff *skb, void *arg, size_t arg__sz)` +* +* - `key`: A string identifying the Lunatik runtime (e.g., the script name like "examples/sniclassify/sni"). +* This key is used to look up the runtime in Lunatik's internal table of active runtimes. +* - `key_sz`: Length of the key string (including the null terminator). +* - `skb`: The TC metadata context (`struct __sk_buff *`). +* - `arg`: A pointer to arbitrary data passed from eBPF to Lua. +* - `arg_sz`: The size of the `arg` data. +* +* @function attach +* @tparam function callback Lua function to call. It receives one argument: +* +* 1. `ctx` (tc.ctx userdata): A context object used to inspect the packet +* and control the TC verdict via `ctx:action()`. +* +* The callback **must not return a value**. If no action is set, the default +* is `action.ACT_OK`. +* @treturn nil +* @raise Error if the current runtime is sleepable or if internal setup fails. +* @usage +* -- Lua script (e.g., "my_tc_handler.lua" which is run via `lunatik run my_tc_handler.lua`) +* local tc = require("tc") +* local action = require("linux.tc") +* +* local function my_packet_processor(ctx) +* local skb = ctx:packet() +* print("Packet received, size:", #skb) +* ctx:action(action.ACT_OK) +* return +* end +* tc.attach(my_packet_processor) +* +* -- In eBPF C code, to call the above Lua function: +* -- char rt_key[] = "my_tc_handler.lua"; // Key matches the script name +* -- int verdict = bpf_luatc_run(rt_key, sizeof(rt_key), ctx, NULL, 0); +* @see data +* @within tc +*/ +static int luatc_attach(lua_State *L) +{ + lunatik_checkruntime(L, LUNATIK_OPT_SOFTIRQ); + luaL_checktype(L, 1, LUA_TFUNCTION); /* callback */ + + lunatik_object_t *object = lunatik_newobject(L, &luatc_class, sizeof(luatc_ctx_t), LUNATIK_OPT_NONE); + luatc_ctx_t *ctx = (luatc_ctx_t *)object->private; + + ctx->skb_obj = luaskb_new(L); + lunatik_getobject(ctx->skb_obj); + lunatik_register(L, -1, ctx->skb_obj); + lua_pop(L, 1); + + ctx->argument = luadata_new(L, LUNATIK_OPT_SINGLE); + lunatik_getobject(ctx->argument); + lunatik_register(L, -1, ctx->argument); + lua_pop(L, 1); + + lua_pushvalue(L, 1); + ctx->callback_ref = luaL_ref(L, LUA_REGISTRYINDEX); + + lunatik_register(L, -1, &luatc_env_key); + lua_pop(L, 1); + + return 0; +} +#endif + +static const luaL_Reg luatc_lib[] = { +#if (LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)) + {"attach", luatc_attach}, + {"detach", luatc_detach}, +#endif + {NULL, NULL} +}; + +LUNATIK_CLASSES(tc, &luatc_class); +LUNATIK_NEWLIB(tc, luatc_lib, luatc_classes); + +static int __init luatc_init(void) +{ + LUNATIK_EBPF_KFUNC_INIT(tc, BPF_PROG_TYPE_SCHED_CLS); +} + +static void __exit luatc_exit(void) +{ +#if (LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)) + if (luatc_runtimes != NULL) + lunatik_putobject(luatc_runtimes); +#endif +} + +module_init(luatc_init); +module_exit(luatc_exit); +MODULE_LICENSE("Dual MIT/GPL"); +MODULE_AUTHOR("Ashwani Kumar Kamal "); + diff --git a/lib/luaxdp.c b/lib/luaxdp.c index 236120584d..54e9e056b0 100644 --- a/lib/luaxdp.c +++ b/lib/luaxdp.c @@ -20,6 +20,7 @@ #include #include +#include #include "luarcu.h" #include "luadata.h" @@ -29,124 +30,151 @@ #include #include -#if (LINUX_VERSION_CODE >= KERNEL_VERSION(6, 7, 0)) -__bpf_kfunc_start_defs(); -#else -__diag_push(); -__diag_ignore_all("-Wmissing-prototypes", - "Global kfuncs as their definitions will be in BTF"); -#endif +LUNATIK_EBPF_START(); + +static char luaxdp_env_key; static lunatik_object_t *luaxdp_runtimes = NULL; -static inline lunatik_object_t *luaxdp_pushdata(lua_State *L, int upvalue, void *ptr, size_t size) -{ - lunatik_object_t *data; +typedef struct luaxdp_ctx_s { + struct xdp_buff *xdp; + void *arg; + size_t arg__sz; + int *action; + lunatik_object_t *packet; + lunatik_object_t *argument; + int callback_ref; +} luaxdp_ctx_t; - lua_pushvalue(L, lua_upvalueindex(upvalue)); - data = (lunatik_object_t *)lunatik_toobject(L, -1); - luadata_reset(data, ptr, size, LUADATA_OPT_KEEP); - return data; -} +LUNATIK_PRIVATECHECKER(luaxdp_ctx_check, luaxdp_ctx_t *, + luaL_argcheck(L, private->xdp != NULL, ix, "ctx is not set"); +); -static int luaxdp_callback(lua_State *L) +/*** +* Returns the packet data buffer for the current XDP context. +* @function packet +* @treturn data +*/ +static int luaxdp_packet(lua_State *L) { - lunatik_object_t *buffer, *argument; - struct xdp_buff *ctx = (struct xdp_buff *)lua_touserdata(L, 1); - void *arg = lua_touserdata(L, 2); - size_t arg__sz = (size_t)lua_tointeger(L, 3); - - lua_pushvalue(L, lua_upvalueindex(1)); /* callback */ - buffer = luaxdp_pushdata(L, 2, ctx->data, ctx->data_end - ctx->data); - argument = luaxdp_pushdata(L, 3, arg, arg__sz); - - if (lua_pcall(L, 2, 1, 0) != LUA_OK) { - luadata_clear(buffer); - luadata_clear(argument); - return lua_error(L); - } + luaxdp_ctx_t *ctx = luaxdp_ctx_check(L, 1); + lunatik_getregistry(L, ctx->packet); + return 1; +} - luadata_clear(buffer); - luadata_clear(argument); +/*** +* Returns the argument data buffer passed from eBPF. +* @function argument +* @treturn data +*/ +static int luaxdp_arg(lua_State *L) +{ + luaxdp_ctx_t *ctx = luaxdp_ctx_check(L, 1); + lunatik_getregistry(L, ctx->argument); return 1; } -static int luaxdp_handler(lua_State *L, struct xdp_buff *ctx, void *arg, size_t arg__sz) +/*** +* Sets the XDP verdict action for this packet. +* @function set_action +* @tparam integer action XDP action constant (e.g. XDP_PASS, XDP_DROP, ...) +*/ +static int luaxdp_action(lua_State *L) { - int action = -1; - int status; + luaxdp_ctx_t *ctx = luaxdp_ctx_check(L, 1); + *ctx->action = luaL_checkinteger(L, 2); + return 0; +} - if (lunatik_getregistry(L, luaxdp_callback) != LUA_TFUNCTION) { - pr_err("couldn't find callback"); - goto out; - } +static const luaL_Reg luaxdp_mt[] = { + {"__gc", lunatik_deleteobject}, + {"packet", luaxdp_packet}, + {"argument", luaxdp_arg}, + {"action", luaxdp_action}, + {NULL, NULL} +}; - lua_pushlightuserdata(L, ctx); - lua_pushlightuserdata(L, arg); - lua_pushinteger(L, (lua_Integer)arg__sz); - if ((status = lua_pcall(L, 3, 1, 0)) != LUA_OK) { - pr_err("%s\n", lua_tostring(L, -1)); - goto out; - } +static void luaxdp_release(void *private) +{ + luaxdp_ctx_t *lctx = (luaxdp_ctx_t *)private; + if (lctx->packet) + luadata_close(lctx->packet); + if (lctx->argument) + luadata_close(lctx->argument); +} - action = lua_tointeger(L, -1); -out: - return action; +LUNATIK_OPENER(xdp); +static const lunatik_class_t luaxdp_class = { + .name = "xdp.ctx", + .methods = luaxdp_mt, + .release = luaxdp_release, + .opener = luaopen_xdp, + .opt = LUNATIK_OPT_SOFTIRQ | LUNATIK_OPT_SINGLE, +}; + +static void luaxdp_handler_cleanup(luaxdp_ctx_t *lctx) { + luadata_clear(lctx->packet); + luadata_clear(lctx->argument); + lctx->xdp = NULL; } -static inline int luaxdp_checkruntimes(void) +static int luaxdp_handler(lua_State *L, luaxdp_ctx_t *ctx) { - const char *key = "runtimes"; - if (luaxdp_runtimes == NULL && - (luaxdp_runtimes = luarcu_getobject(lunatik_env, key, sizeof(key))) == NULL) + luaxdp_ctx_t *lctx; + lunatik_bpf_get_env(L, &luaxdp_env_key, lctx); + + lctx->xdp = ctx->xdp; + lctx->arg = ctx->arg; + lctx->arg__sz = ctx->arg__sz; + lctx->action = ctx->action; + + luadata_reset(lctx->packet, lctx->xdp->data, lctx->xdp->data_end - lctx->xdp->data, LUADATA_OPT_KEEP); + luadata_reset(lctx->argument, lctx->arg, lctx->arg__sz, LUADATA_OPT_KEEP); + + lua_rawgeti(L, LUA_REGISTRYINDEX, lctx->callback_ref); + if (!lua_isfunction(L, -1)) { + lua_pop(L, 2); + pr_err("callback_ref is not a function\n"); + luaxdp_handler_cleanup(lctx); return -1; + } + + lua_insert(L, -2); + if (lua_pcall(L, 1, 0, 0) != LUA_OK) { + pr_err("%s\n", lua_tostring(L, -1)); + lua_pop(L, 1); + luaxdp_handler_cleanup(lctx); + return -1; + } + + luaxdp_handler_cleanup(lctx); return 0; } __bpf_kfunc int bpf_luaxdp_run(char *key, size_t key__sz, struct xdp_md *xdp_ctx, void *arg, size_t arg__sz) { - lunatik_object_t *runtime; - struct xdp_buff *ctx = (struct xdp_buff *)xdp_ctx; int action = -1; - size_t keylen = key__sz - 1; - if (unlikely(luaxdp_checkruntimes() != 0)) { - pr_err("couldn't find _ENV.runtimes\n"); + lunatik_object_t *runtime = lunatik_ebpf_lookup(luaxdp_runtimes, key, key__sz); + if (runtime == NULL) goto out; - } - key[keylen] = '\0'; - if ((runtime = luarcu_getobject(luaxdp_runtimes, key, keylen)) == NULL) { - pr_err("couldn't find runtime '%s'\n", key); - goto out; - } + luaxdp_ctx_t ctx = { + .xdp = (struct xdp_buff *)xdp_ctx, + .arg = arg, + .arg__sz = arg__sz, + .action = &action, + }; - lunatik_run(runtime, luaxdp_handler, action, ctx, arg, arg__sz); + lunatik_run(runtime, luaxdp_handler, action, &ctx); lunatik_putobject(runtime); out: return action; } -#if (LINUX_VERSION_CODE >= KERNEL_VERSION(6, 7, 0)) -__bpf_kfunc_end_defs(); -#else -__diag_pop(); -#endif - -#if (LINUX_VERSION_CODE >= KERNEL_VERSION(6, 9, 0)) -BTF_KFUNCS_START(bpf_luaxdp_set) -BTF_ID_FLAGS(func, bpf_luaxdp_run) -BTF_KFUNCS_END(bpf_luaxdp_set) -#else -BTF_SET8_START(bpf_luaxdp_set) -BTF_ID_FLAGS(func, bpf_luaxdp_run) -BTF_SET8_END(bpf_luaxdp_set) -#endif +LUNATIK_EBPF_END(); -static const struct btf_kfunc_id_set bpf_luaxdp_kfunc_set = { - .owner = THIS_MODULE, - .set = &bpf_luaxdp_set, -}; +LUNATIK_EBPF_KFUNC_DEFINE_SET(xdp, bpf_luaxdp_run); /*** * Unregisters the Lua callback function associated with the current Lunatik runtime. @@ -160,7 +188,12 @@ static const struct btf_kfunc_id_set bpf_luaxdp_kfunc_set = { */ static int luaxdp_detach(lua_State *L) { - lunatik_unregister(L, luaxdp_callback); + luaxdp_ctx_t *lctx; + lunatik_bpf_get_env(L, &luaxdp_env_key, lctx); + luaL_unref(L, LUA_REGISTRYINDEX, lctx->callback_ref); + lctx->callback_ref = LUA_NOREF; + lua_pop(L, 1); + lunatik_unregister(L, &luaxdp_env_key); return 0; } @@ -181,15 +214,13 @@ static int luaxdp_detach(lua_State *L) * - `arg_sz`: The size of the `arg` data. * * @function attach -* @tparam function callback Lua function to call. It receives two arguments: +* @tparam function callback Lua function to call. It receives one argument: * -* 1. `buffer` (data): A `data` object representing the network packet buffer (`xdp_md`). -* The `data` object points to `xdp_ctx->data` and its size is `xdp_ctx->data_end - xdp_ctx->data`. -* 2. `argument` (data): A `data` object representing the `arg` passed from the eBPF program. -* Its size is `arg_sz`. +* 1. `ctx` (xdp.ctx userdata): A context object used to inspect the packet +* and control the XDP verdict via `ctx:action()`. * -* The callback function should return an integer verdict, typically one of the values -* from `linux.xdp` (e.g., `action.PASS`, `action.DROP`). +* The callback **must not return a value**. If no action is set, the default +* is `action.PASS`. * @treturn nil * @raise Error if the current runtime is sleepable or if internal setup fails. * @usage @@ -197,9 +228,11 @@ static int luaxdp_detach(lua_State *L) * local xdp = require("xdp") * local action = require("linux.xdp") * -* local function my_packet_processor(packet_buffer, custom_arg) +* local function my_packet_processor(ctx) +* local packet_buffer = ctx:packet() * print("Packet received, size:", #packet_buffer) -* return action.PASS +* ctx:action(action.PASS) +* return nil * end * xdp.attach(my_packet_processor) * @@ -214,11 +247,25 @@ static int luaxdp_attach(lua_State *L) lunatik_checkruntime(L, LUNATIK_OPT_SOFTIRQ); luaL_checktype(L, 1, LUA_TFUNCTION); /* callback */ - luadata_new(L, LUNATIK_OPT_SINGLE); /* buffer */ - luadata_new(L, LUNATIK_OPT_SINGLE); /* argument */ + lunatik_object_t *object = lunatik_newobject(L, &luaxdp_class, sizeof(luaxdp_ctx_t), LUNATIK_OPT_NONE); + luaxdp_ctx_t *ctx = (luaxdp_ctx_t *)object->private; + + ctx->packet = luadata_new(L, LUNATIK_OPT_SINGLE); + lunatik_getobject(ctx->packet); + lunatik_register(L, -1, ctx->packet); + lua_pop(L, 1); + + ctx->argument = luadata_new(L, LUNATIK_OPT_SINGLE); + lunatik_getobject(ctx->argument); + lunatik_register(L, -1, ctx->argument); + lua_pop(L, 1); + + lua_pushvalue(L, 1); + ctx->callback_ref = luaL_ref(L, LUA_REGISTRYINDEX); + + lunatik_register(L, -1, &luaxdp_env_key); + lua_pop(L, 1); - lua_pushcclosure(L, luaxdp_callback, 3); - lunatik_register(L, -1, luaxdp_callback); return 0; } #endif @@ -231,15 +278,12 @@ static const luaL_Reg luaxdp_lib[] = { {NULL, NULL} }; -LUNATIK_NEWLIB(xdp, luaxdp_lib, NULL); +LUNATIK_CLASSES(xdp, &luaxdp_class); +LUNATIK_NEWLIB(xdp, luaxdp_lib, luaxdp_classes); static int __init luaxdp_init(void) { -#if (LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)) - return register_btf_kfunc_id_set(BPF_PROG_TYPE_XDP, &bpf_luaxdp_kfunc_set); -#else - return 0; -#endif + LUNATIK_EBPF_KFUNC_INIT(xdp, BPF_PROG_TYPE_XDP); } static void __exit luaxdp_exit(void) diff --git a/lib/skb/attr.lua b/lib/skb/attr.lua new file mode 100644 index 0000000000..b738cc8888 --- /dev/null +++ b/lib/skb/attr.lua @@ -0,0 +1,46 @@ +-- +-- SPDX-FileCopyrightText: (c) 2026 Ashwani Kumar Kamal +-- SPDX-License-Identifier: MIT OR GPL-2.0-only +-- + +--- +-- Socket buffer operations. +-- This module provides a higher-level abstraction over the `skb` module. +-- +-- @module skb.attr +-- @see skb +-- + +local mt = { + __len = function(self) + return #self._skb + end, + + __index = function(self, key) + local getter = self._skb["get" .. key] + if type(getter) == "function" then + return getter(self._skb) + end + + local method = self._skb[key] + if type(method) == "function" then + return function(_, ...) + return method(self._skb, ...) + end + end + end, + + __newindex = function(self, key, val) + local setter = self._skb["set" .. key] + if type(setter) == "function" then + return setter(self._skb, val) + end + + error("skb field '" .. key .. "' is read-only or does not exist") + end, +} + +return function(raw_skb) + return setmetatable({ _skb = raw_skb }, mt) +end + diff --git a/lunatik_ebpf.h b/lunatik_ebpf.h new file mode 100644 index 0000000000..edf3444ed8 --- /dev/null +++ b/lunatik_ebpf.h @@ -0,0 +1,84 @@ +/* +* SPDX-FileCopyrightText: (c) 2026 Ashwani Kumar Kamal +* SPDX-License-Identifier: MIT OR GPL-2.0-only +*/ + +#ifndef LUNATIK_EBPF_H +#define LUNATIK_EBPF_H + +#include "lunatik.h" + +#define lunatik_ebpf_checkruntimes(runtimes) \ +({ \ + const char *key = "runtimes"; \ + if ((runtimes) == NULL) \ + (runtimes) = luarcu_getobject(lunatik_env, key, sizeof(key)); \ + (runtimes) != NULL ? 0 : -1; \ +}) + +#define lunatik_ebpf_lookup(runtimes, key, key_sz) \ +({ \ + lunatik_object_t *runtime = NULL; \ + size_t keylen = key_sz - 1; \ + key[keylen] = '\0'; \ + if (unlikely(lunatik_ebpf_checkruntimes(runtimes) != 0)) \ + pr_err("couldn't find _ENV.runtimes\n"); \ + else { \ + runtime = luarcu_getobject((runtimes), (key), keylen); \ + if (runtime == NULL) \ + pr_err("couldn't find runtime '%s'\n", (key)); \ + } \ + runtime; \ +}) + +/** + * Fetches the environment context from runtime registry + * Sets out_ptr to the objects private pointer + */ +#define lunatik_bpf_get_env(L, env_key, out_ptr) do { \ + if (lunatik_getregistry((L), (env_key)) != LUA_TUSERDATA) { \ + lua_pop((L), 1); \ + pr_err("couldn't find the context object\n"); \ + return -1; \ + } \ + lunatik_object_t *obj = (lunatik_object_t *)lunatik_toobject((L), -1); \ + (out_ptr) = obj->private; \ +} while (0) + +#if (LINUX_VERSION_CODE >= KERNEL_VERSION(6, 7, 0)) +#define LUNATIK_EBPF_START() __bpf_kfunc_start_defs() +#define LUNATIK_EBPF_END() __bpf_kfunc_end_defs() +#else +#define LUNATIK_EBPF_START() \ + __diag_push(); \ + __diag_ignore_all("-Wmissing-prototypes", \ + "Global kfuncs as their definitions will be in BTF") +#define LUNATIK_EBPF_END() __diag_pop() +#endif + +#if (LINUX_VERSION_CODE >= KERNEL_VERSION(6, 9, 0)) +#define LUNATIK_BTF_SET_START(name) BTF_KFUNCS_START(name) +#define LUNATIK_BTF_SET_END(name) BTF_KFUNCS_END(name) +#else +#define LUNATIK_BTF_SET_START(name) BTF_SET8_START(name) +#define LUNATIK_BTF_SET_END(name) BTF_SET8_END(name) +#endif + +#define LUNATIK_EBPF_KFUNC_DEFINE_SET(subsys, kfunc) \ + LUNATIK_BTF_SET_START(bpf_lua##subsys##_set) \ + BTF_ID_FLAGS(func, kfunc) \ + LUNATIK_BTF_SET_END(bpf_lua##subsys##_set) \ + static const struct btf_kfunc_id_set bpf_lua##subsys##_kfunc_set = { \ + .owner = THIS_MODULE, \ + .set = &bpf_lua##subsys##_set, \ + }; + +#if (LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)) +#define LUNATIK_EBPF_KFUNC_INIT(subsys, prog_type) \ + return register_btf_kfunc_id_set(prog_type, &bpf_lua##subsys##_kfunc_set); +#else +#define LUNATIK_EBPF_KFUNC_INIT(subsys, prog_type) return 0; +#endif + +#endif + diff --git a/tc.sh b/tc.sh new file mode 100755 index 0000000000..c7baeb1561 --- /dev/null +++ b/tc.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +TC="tc" +IF="wlp3s0" +BPF_OBJ="examples/qos/classify.o" +SEC_NAME="classifier" + +# 1. cleanup +echo "Cleaning up existing qdiscs and filters..." +$TC filter del dev $IF egress 2>/dev/null +$TC qdisc del dev $IF clsact 2>/dev/null +$TC qdisc del dev $IF root 2>/dev/null + +# 2. create htb scheduler +echo "Setting up HTB scheduler..." +$TC qdisc add dev $IF root handle 1: htb default 20 + +$TC class add dev $IF parent 1: classid 1:1 htb rate 100mbit ceil 100mbit + +# Band 1: Interactive / Tiny Packets (<256B) - High Priority +$TC class add dev $IF parent 1:1 classid 1:10 htb rate 50mbit ceil 100mbit prio 1 + +# Band 2: Normal / Standard flows (<800B) - Medium Priority +$TC class add dev $IF parent 1:1 classid 1:20 htb rate 30mbit ceil 100mbit prio 2 + +# Band 3: Bulk / Large transfers (>800B) - Low Priority +$TC class add dev $IF parent 1:1 classid 1:30 htb rate 20mbit ceil 100mbit prio 3 + +# 3. create clsact hook +echo "Creating clsact hook..." +$TC qdisc add dev $IF clsact + +# 4. load and attach ebpf classifier +echo "Loading eBPF classifier onto egress..." +$TC filter add dev $IF egress bpf da obj $BPF_OBJ sec $SEC_NAME + +echo "Setup complete! Verifying status:" +echo "-----------------------------------" +$TC qdisc show dev $IF +echo "-----------------------------------" +$TC class show dev $IF +