Skip to content

Commit cb1a607

Browse files
committed
examples: add sniclassify tc/qos
Signed-off-by: Ashwani Kumar Kamal <ashwanikamal.im421@gmail.com>
1 parent b24be6e commit cb1a607

5 files changed

Lines changed: 210 additions & 0 deletions

File tree

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,12 @@ scripts_uninstall:
117117

118118
ebpf:
119119
${MAKE} -C examples/filter
120+
${MAKE} -C examples/sniclassify
120121

121122
ebpf_install:
122123
${MKDIR} ${LUNATIK_EBPF_INSTALL_PATH}
123124
${INSTALL} -m 0644 examples/filter/https.o ${LUNATIK_EBPF_INSTALL_PATH}/
125+
${INSTALL} -m 0644 examples/sniclassify/classify.o ${LUNATIK_EBPF_INSTALL_PATH}/
124126

125127
ebpf_uninstall:
126128
${RM} -r ${LUNATIK_EBPF_INSTALL_PATH}

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,46 @@ ip netns exec tcpreject curl --connect-timeout 2 https://[2001:4860:4860::8888]
465465
sudo examples/tcpreject/cleanup.sh
466466
```
467467

468+
### sniclassify
469+
470+
[sniclassify](examples/sniclassify) is a kernel extension composed by
471+
a TC/eBPF classifier program attached on egress,
472+
a Lua kernel script to classify [SNI](https://datatracker.ietf.org/doc/html/rfc3546#section-3.1) traffic.
473+
This kernel extension extracts server name and assigns traffic
474+
classes according to a Lua [policy table](examples/sniclassify/sni.lua#18).
475+
476+
Install and load the classfier:
477+
478+
```sh
479+
sudo make btf_install # needed to export the 'bpf_luatc_run' kfunc
480+
sudo make examples_install # installs examples
481+
make ebpf # builds the TC/eBPF program
482+
sudo make ebpf_install # installs the TC/eBPF program
483+
sudo lunatik run examples/sniclassify/sni softirq
484+
```
485+
486+
Configure HTB classes:
487+
```
488+
sudo tc qdisc del dev docker0 root 2>/dev/null
489+
sudo tc qdisc add dev docker0 root handle 1: htb default 30
490+
sudo tc class add dev docker0 parent 1: classid 1:10 htb rate 20mbit
491+
sudo tc class add dev docker0 parent 1: classid 1:20 htb rate 10mbit
492+
```
493+
494+
Attach the TC/eBPF classifier on egress:
495+
```
496+
sudo tc filter add dev docker0 parent 1: bpf da obj examples/sniclassify/classify.o sec classifier
497+
```
498+
499+
The classifier inspects outbound TLS ClientHello packets, extracts the SNI
500+
field, and assigns a traffic class according to the Lua policy table.
501+
502+
Verify and test:
503+
```
504+
sudo tc filter show dev docker0
505+
sudo journalctl -ft kernel
506+
```
507+
468508
### gesture
469509

470510
[gesture](examples/gesture.lua)

examples/sniclassify/Makefile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# SPDX-FileCopyrightText: (c) 2026 Ashwani Kumar Kamal <ashwanikamal.im421@gmail.com>
2+
# SPDX-License-Identifier: MIT OR GPL-2.0-only
3+
4+
all: vmlinux classify.o
5+
6+
vmlinux:
7+
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
8+
9+
classify.o: classify.c
10+
clang -target bpf -Wall -O2 -c -g $<
11+
12+
clean:
13+
rm -f vmlinux.h classify.o
14+

examples/sniclassify/classify.c

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* SPDX-FileCopyrightText: (c) 2026 Ashwani Kumar Kamal <ashwanikamal.im421@gmail.com>
3+
* SPDX-License-Identifier: MIT OR GPL-2.0-only
4+
*/
5+
6+
#include "vmlinux.h"
7+
#include <bpf/bpf_helpers.h>
8+
#include <bpf/bpf_endian.h>
9+
10+
extern int bpf_luatc_run(char *key, size_t key__sz, struct __sk_buff *skb, void *arg, size_t arg__sz) __ksym;
11+
12+
static char runtime[] = "examples/sniclassify/sni";
13+
14+
int const TC_ACT_OK = 0;
15+
16+
struct {
17+
__uint(type, BPF_MAP_TYPE_HASH);
18+
__uint(max_entries, 65536);
19+
__type(key, __u32);
20+
__type(value, __u32);
21+
} flow_cache SEC(".maps");
22+
23+
struct bpf_luatc_arg {
24+
__u16 offset;
25+
} __attribute__((packed));
26+
27+
SEC("classifier")
28+
int classify(struct __sk_buff *skb)
29+
{
30+
__u32 key = skb->hash;
31+
32+
__u32 *priority= bpf_map_lookup_elem(&flow_cache, &key);
33+
if (priority) {
34+
skb->priority = *priority;
35+
return TC_ACT_OK;
36+
}
37+
38+
struct bpf_luatc_arg arg;
39+
void *data_end = (void *)(long)skb->data_end;
40+
void *data = (void *)(long)skb->data;
41+
struct iphdr *ip = data + sizeof(struct ethhdr);
42+
43+
if (ip + 1 > (struct iphdr *)data_end)
44+
goto pass;
45+
46+
if (ip->protocol != IPPROTO_TCP)
47+
goto pass;
48+
49+
struct tcphdr *tcp = (void *)ip + (ip->ihl * 4);
50+
if (tcp + 1 > (struct tcphdr *)data_end)
51+
goto pass;
52+
53+
if (bpf_ntohs(tcp->dest) != 443 || !tcp->psh)
54+
goto pass;
55+
56+
void *payload = (void *)tcp + (tcp->doff * 4);
57+
if (payload > data_end)
58+
goto pass;
59+
60+
arg.offset = bpf_htons((__u16)(payload - data));
61+
62+
int action = bpf_luatc_run(runtime, sizeof(runtime), skb, &arg, sizeof(arg));
63+
return action < 0 ? TC_ACT_OK : action;
64+
pass:
65+
return TC_ACT_OK;
66+
}
67+
68+
char _license[] SEC("license") = "Dual MIT/GPL";
69+

examples/sniclassify/sni.lua

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
--
2+
-- SPDX-FileCopyrightText: (c) 2025-2026 Ashwani Kumar Kamal <ashwanikamal.im421@gmail.com>
3+
-- SPDX-License-Identifier: MIT OR GPL-2.0-only
4+
--
5+
6+
local tc = require("tc")
7+
local action = require("linux.tc")
8+
local skbattr = require("skb.attr")
9+
10+
local TC_H_MAKE = function(maj, min) return (maj << 16) | min end
11+
12+
local client_hello = 0x01
13+
local handshake = 0x16
14+
local server_name = 0x0000
15+
16+
local session = 43
17+
local max_extensions = 17
18+
19+
local policy = {
20+
["netflix%.com"] = TC_H_MAKE(1, 20),
21+
["zoom%.com"] = TC_H_MAKE(1, 10),
22+
}
23+
24+
local function log(sni, priority)
25+
print(string.format("sniclassify: %s %s", sni, priority))
26+
end
27+
28+
local function unpacker(packet, base)
29+
local byte = function (offset)
30+
return packet:getbyte(base + offset)
31+
end
32+
33+
local short = function (offset)
34+
local offset = base + offset
35+
return packet:getbyte(offset) << 8 | packet:getbyte(offset + 1)
36+
end
37+
38+
local str = function (offset, length)
39+
return packet:getstring(base + offset, length)
40+
end
41+
42+
return byte, short, str
43+
end
44+
45+
local function offset(argument)
46+
return select(2, unpacker(argument, 0))(0)
47+
end
48+
49+
local function sniclassify(ctx)
50+
local argument = ctx:argument()
51+
local skb = skbattr(ctx:skb())
52+
local data = skb:data()
53+
local byte, short, str = unpacker(data, offset(argument))
54+
55+
if byte(0) ~= handshake or byte(5) ~= client_hello then
56+
ctx:action(action.ACT_OK)
57+
return
58+
end
59+
60+
local cipher = (session + 1) + byte(session)
61+
local compression = cipher + 2 + short(cipher)
62+
local extension = compression + 3 + byte(compression)
63+
64+
for _ = 1, max_extensions do
65+
local data_off = extension + 4
66+
if short(extension) == server_name then
67+
local sni = str(data_off + 5, short(data_off + 3))
68+
for pattern, classid in pairs(policy) do
69+
if sni:match(pattern) then
70+
log(sni, classid)
71+
skb.priority = classid
72+
break
73+
end
74+
end
75+
ctx:action(action.ACT_OK)
76+
return
77+
end
78+
extension = data_off + short(extension + 2)
79+
end
80+
81+
ctx:action(action.ACT_OK)
82+
end
83+
84+
tc.attach(sniclassify)
85+

0 commit comments

Comments
 (0)