Tự Viết Một Chương Trình tc: __sk_buff và Chuỗi tcx
Bài 12 đọc datapath tc của Cilium từ ngoài. Giờ ta tự viết một chương trình sched_cls để hiểu nó từ trong: một bộ đếm gói egress, phân loại theo giao thức. Mục tiêu không phải làm gì hữu ích ghê gớm, mà nắm cho chắc hai thứ — __sk_buff (thứ tc đưa cho chương trình, khác hẳn gói thô của XDP) và tcx (cách gắn nhiều chương trình tc thành chuỗi). Và ta sẽ vấp một bài học thật về chuỗi đó.
__sk_buff: gói đã có metadata, không phải byte thô
Khác biệt cốt lõi giữa XDP và tc nằm ở thứ nhân đưa cho chương trình. XDP (Bài 11) đưa xdp_md — về cơ bản chỉ hai con trỏ tới gói thô, mọi thứ phải tự parse. tc đưa __sk_buff, một góc nhìn lên sk_buff mà nhân đã dựng và điền sẵn metadata:
struct __sk_buff {
__u32 len; // độ dài gói — đã tính sẵn
__u32 protocol; // giao thức L3 (ETH_P_IP...) — đã điền sẵn
__u32 ifindex; // interface
__u32 mark; // nhãn fwmark (chia sẻ với netfilter/routing)
__u32 priority;
... // và data/data_end nếu cần parse thô
};
Nghĩa là nhiều câu hỏi không cần parse byte: muốn biết giao thức L3 thì đọc thẳng skb->protocol, muốn độ dài gói thì skb->len. Đây là tiện ích của việc chạy sau khi nhân đã dựng sk_buff — cái giá phải trả so với XDP, nhưng đổi lại thông tin sẵn sàng.
Chương trình: đếm egress theo giao thức
#define TC_ACT_OK 0 // == TCX_PASS: cho gói qua, DỪNG chuỗi tcx
struct { // mảng đếm: 0=tổng 1=IPv4 2=IPv6 3=khác, 9=tổng byte
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 10);
__type(key, __u32);
__type(value, __u64);
} egress SEC(".maps");
SEC("tc")
int count_egress(struct __sk_buff *skb)
{
add(0, 1); // mỗi gói: tăng tổng
add(9, skb->len); // cộng dồn độ dài — metadata sẵn
__u32 proto = bpf_ntohs(skb->protocol); // giao thức L3, không cần parse
if (proto == ETH_P_IP) add(1, 1);
else if (proto == ETH_P_IPV6) add(2, 1);
else add(3, 1);
return TC_ACT_OK; // không drop — chỉ quan sát
}
skb->protocol là __be16 (network byte order), nên bpf_ntohs để so sánh. add() là một helper nhỏ bpf_map_lookup_elem + __sync_fetch_and_add. Không một dòng parse Ethernet/IP nào — toàn bộ thông tin lấy thẳng từ metadata __sk_buff.
Giá trị trả về TC_ACT_OK mới là chỗ có chuyện — đọc kỹ ở cuối bài.
Gắn bằng tcx
tcx (kernel 6.6+, Bài 12) là cách gắn tc dựa trên BPF link; bpftool gắn trực tiếp:
clang -O2 -g -target bpf -I. -c count.bpf.c -o count.bpf.o
sudo bpftool prog loadall count.bpf.o /sys/fs/bpf/tccount
sudo bpftool net attach tcx_egress pinned /sys/fs/bpf/tccount/count_egress dev lo
Gắn vào lo (loopback), rồi sinh ít lưu lượng nội bộ và đọc map:
ping -c 3 127.0.0.1 # IPv4
ping6 -c 2 ::1 # IPv6
sudo bpftool map dump id <id>
key: 0 value: 30 <- tổng 30 gói egress trên lo
key: 1 value: 26 <- IPv4
key: 2 value: 4 <- IPv6 (2 echo + 2 reply của ping6 -c 2)
key: 3 value: 0 <- khác: không có
key: 9 value: 2902 <- tổng byte
Số khớp: ping6 -c 2 ::1 tạo 2 echo + 2 reply, cả bốn đều đi qua egress của lo → IPv6 = 4. IPv4 = 26 gồm 6 gói của ping 127.0.0.1 cộng lưu lượng loopback nền (systemd, DNS cục bộ). Tổng 30 = 26 + 4, không có gói "khác". Chương trình chạy đúng, phân loại bằng metadata __sk_buff mà không parse byte nào.
Vì sao gắn vào lo chứ không phải ens5?
Lần đầu thử, gắn đúng chương trình này vào egress của ens5 (card vật lý) — và bộ đếm tổng đứng yên ở 0, dù lưu lượng ra node rõ ràng có — không phải bug. bpftool net show dev ens5 cho thấy chuỗi tcx egress:
ens5 tcx/egress cil_to_netdev prog_id 2960 link_id 17 <- Cilium, gắn trước
ens5 tcx/egress count_egress prog_id 4669 <- của ta, gắn sau
Hai chương trình trên cùng hook egress, chạy theo thứ tự — Cilium trước, của ta sau. Vấn đề nằm ở mã trả về. Đọc thẳng định nghĩa trong /usr/include/linux/bpf.h của chính node:
/* (Simplified) user return codes for tcx prog type.
* ... unknown return codes are mapped to TCX_NEXT. */
enum tcx_action_base {
TCX_NEXT = -1, // chạy tiếp chương trình KẾ trong chuỗi
TCX_PASS = 0, // == TC_ACT_OK: cho gói qua, DỪNG chuỗi
TCX_DROP = 2, // == TC_ACT_SHOT
TCX_REDIRECT = 7, // == TC_ACT_REDIRECT
};
Chuỗi tcx chỉ chạy tiếp sang chương trình kế khi chương trình hiện tại trả TCX_NEXT (-1). Cilium's cil_to_netdev trả về một verdict kết thúc (TCX_PASS hoặc TCX_REDIRECT — nó chuyển/đẩy gói đi), nên chuỗi dừng ngay tại Cilium, không bao giờ tới count_egress đứng sau → tổng = 0. Trên lo thì khác: chương trình của ta là duy nhất và đứng đầu, nên nó chạy, rồi TC_ACT_OK của nó dừng chuỗi (chẳng còn ai sau để chạy).
Muốn bộ đếm chạy trên ens5 thì phải gắn nó trước Cilium trong chuỗi — đó là việc của cờ BPF_F_BEFORE khi tạo link (chính là tính năng tcx thêm vào ở kernel 6.6). Bài học rút ra: trên một hook đã có chủ như datapath Cilium, thứ tự trong chuỗi tcx quyết định chương trình của bạn có thấy gói hay không — và một verdict kết thúc từ chương trình phía trước sẽ che khuất mọi chương trình phía sau.
🧹 Dọn dẹp
sudo bpftool net detach tcx_egress dev lo
sudo rm -rf /sys/fs/bpf/tccount
Như Bài 11, lệnh detach đặt trong trap ... EXIT để luôn chạy. Gỡ xong node về 140 chương trình. Mã nguồn (count.bpf.c, lệnh build/attach) ở github.com/nghiadaulau/ebpf-from-scratch, thư mục 13-tc-write.
Tổng kết
Tự viết một chương trình tc/sched_cls làm rõ hai thứ. Một, __sk_buff: tc chạy sau khi nhân dựng sk_buff nên đưa cho chương trình metadata đã điền sẵn — skb->protocol (giao thức L3), skb->len, skb->mark, skb->ifindex — ta phân loại gói egress theo giao thức mà không parse một byte nào (khác XDP phải tự bóc từ con trỏ thô). Gắn bằng tcx (bpftool net attach tcx_egress, kernel 6.6+), chạy trên lo ra số đếm đúng (IPv4 26 / IPv6 4, khớp lưu lượng sinh ra). Hai, chuỗi tcx: nhiều chương trình xếp trên một hook chạy theo thứ tự, và chỉ chạy tiếp khi trả TCX_NEXT (-1); TC_ACT_OK (= TCX_PASS = 0) dừng chuỗi. Vì thế cùng chương trình đó gắn sau Cilium trên ens5 không chạy lần nào (Cilium trả verdict kết thúc trước) — kiểm chứng thẳng từ enum tcx_action_base trong header kernel của node. Muốn chạy phải gắn BPF_F_BEFORE. Trên hook đã có chủ, thứ tự chuỗi là tất cả.
Part IV khép lại: ta đã đi từ XDP (Bài 11) qua datapath Cilium (Bài 12) tới tự viết tc (bài này) — nắm cả hai hook mạng chính của eBPF và cách chúng kết chuỗi. Part V bước sang bảo mật: LSM BPF gắn vào các điểm kiểm soát an ninh của nhân, seccomp, và cách Tetragon dùng eBPF để quan sát rồi cưỡng chế hành vi tiến trình trên cụm.