Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,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
Expand Down Expand Up @@ -161,4 +167,4 @@ config LUNATIK_BYTEORDER
Byte order swapping functions.

endif

6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ 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 RCU THREAD FIB DATA PROBE SYSCALL XDP FIFO SKB NETFILTER \
LUNATIK_MODULES := DEVICE LINUX NOTIFIER SOCKET RCU THREAD FIB DATA PROBE SYSCALL XDP TC FIFO SKB NETFILTER \
COMPLETION CRYPTO CPU HID SIGNAL BYTEORDER DARKEN

$(foreach c,$(LUNATIK_MODULES),\
Expand Down Expand Up @@ -80,6 +80,7 @@ scripts_install:
${MKDIR} ${SCRIPTS_INSTALL_PATH}
${MKDIR} ${SCRIPTS_INSTALL_PATH}/lunatik
${MKDIR} ${SCRIPTS_INSTALL_PATH}/socket
${MKDIR} ${SCRIPTS_INSTALL_PATH}/skb
${MKDIR} ${SCRIPTS_INSTALL_PATH}/syscall
${MKDIR} ${SCRIPTS_INSTALL_PATH}/crypto
${MKDIR} ${SCRIPTS_INSTALL_PATH}/linux
Expand All @@ -91,6 +92,7 @@ scripts_install:
${INSTALL} -m 0644 lib/lighten.lua ${SCRIPTS_INSTALL_PATH}/
${INSTALL} -m 0644 lib/lunatik/*.lua ${SCRIPTS_INSTALL_PATH}/lunatik
${INSTALL} -m 0644 lib/socket/*.lua ${SCRIPTS_INSTALL_PATH}/socket
${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.
Expand All @@ -115,10 +117,12 @@ scripts_uninstall:

ebpf:
${MAKE} -C examples/filter
${MAKE} -C examples/sniclassify

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}/

ebpf_uninstall:
${RM} -r ${LUNATIK_EBPF_INSTALL_PATH}
Expand Down
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,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)
Expand Down
2 changes: 2 additions & 0 deletions autogen/specs.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ return {
desc = "Network device notifier event types." },
{ header = "uapi/linux/bpf.h", prefix = "XDP_", module = "xdp",
desc = "XDP verdicts and flags." },
{ 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/net.h", prefix = "SOCK_", module = "socket.sock",
Expand Down
1 change: 1 addition & 0 deletions config.ld
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ file = {
'./lib/net.lua',
'./lib/luanetfilter.c',
'./lib/luaskb.c',
'./lib/skb/attr.lua',
'./lib/luanotifier.c',
'./lib/luaprobe.c',
'./lib/luarcu.c',
Expand Down
14 changes: 14 additions & 0 deletions examples/sniclassify/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# SPDX-FileCopyrightText: (c) 2026 Ashwani Kumar Kamal <ashwanikamal.im421@gmail.com>
# 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

69 changes: 69 additions & 0 deletions examples/sniclassify/classify.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* SPDX-FileCopyrightText: (c) 2026 Ashwani Kumar Kamal <ashwanikamal.im421@gmail.com>
* SPDX-License-Identifier: MIT OR GPL-2.0-only
*/

#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

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";

85 changes: 85 additions & 0 deletions examples/sniclassify/sni.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
--
-- SPDX-FileCopyrightText: (c) 2025-2026 Ashwani Kumar Kamal <ashwanikamal.im421@gmail.com>
-- SPDX-License-Identifier: MIT OR GPL-2.0-only
--

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, 20),
["zoom%.com"] = TC_H_MAKE(1, 10),
}

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)

1 change: 1 addition & 0 deletions lib/Kbuild
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,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
Expand Down
74 changes: 65 additions & 9 deletions lib/luaskb.c
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,55 @@ static int luaskb_forward(lua_State *L)
return 0;
}

static int luaskb_getmark(lua_State *L)
{
luaskb_t *lskb = luaskb_check(L, 1);
lua_pushinteger(L, lskb->skb->mark);
return 1;
}

static int luaskb_setmark(lua_State *L)
{
luaskb_t *lskb = luaskb_check(L, 1);
lskb->skb->mark = (u32)luaL_checkinteger(L, 2);
return 0;
}

static int luaskb_getpriority(lua_State *L)
{
luaskb_t *lskb = luaskb_check(L, 1);
lua_pushinteger(L, lskb->skb->priority);
return 1;
}

static int luaskb_setpriority(lua_State *L)
{
luaskb_t *lskb = luaskb_check(L, 1);
lskb->skb->priority = (u32)luaL_checkinteger(L, 2);
return 0;
}

static int luaskb_protocol(lua_State *L)
{
luaskb_t *lskb = luaskb_check(L, 1);
lua_pushinteger(L, ntohs(lskb->skb->protocol));
return 1;
}

static int luaskb_proto(lua_State *L)
{
luaskb_t *lskb = luaskb_check(L, 1);
struct sk_buff *skb = lskb->skb;

if (skb->protocol == htons(ETH_P_IP))
lua_pushinteger(L, ip_hdr(skb)->protocol);
else if (skb->protocol == htons(ETH_P_IPV6))
lua_pushinteger(L, ipv6_hdr(skb)->nexthdr);
else
lua_pushnil(L);
return 1;
}

static int luaskb_copy(lua_State *L);

static void luaskb_release(void *private)
Expand All @@ -211,18 +260,25 @@ 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},
{"protocol", luaskb_protocol},
{"proto", luaskb_proto},
{"getmark", luaskb_getmark},
{"getpriority", luaskb_getpriority},
{"setmark", luaskb_setmark},
{"setpriority", luaskb_setpriority},
{NULL, NULL}
};


LUNATIK_OPENER(skb);
static const lunatik_class_t luaskb_class = {
.name = "skb",
Expand Down
Loading