Capstone: Tự Viết connmon — Monitor Kết Nối TCP Toàn Node

K
Kai··5 min read

Bài cuối. Không khái niệm mới — chỉ ghép những mảnh của cả series thành một công cụ quan sát thật: connmon, in mỗi kết nối TCP mới của toàn node ngay khi nó xảy ra. Nó dùng kprobe (Bài 6), ring buffer (Bài 9), CO-RE (Bài 5), và loader Go cilium/ebpf (Bài 10) — đúng tinh thần "từ số không" tới một thứ chạy được.

Ý tưởng: bắt mọi tcp_connect

Mỗi kết nối TCP đi ra đều gọi hàm nhân tcp_connect(). Gắn một kprobe vào đó, ta bắt được mọi kết nối của mọi tiến trình trên node — không cần sửa hay biết trước ứng dụng nào. Từ tham số struct sock *sk, đọc địa chỉ/cổng đích rồi đẩy lên userspace.

Phía nhân: kprobe + ring buffer

struct event {                       // chia sẻ layout với phía Go
    __u32 pid;
    __u32 saddr, daddr;          // __be32 (network order)
    __u16 dport;                 // host order
    __u16 af;
    char  comm[16];
};

struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);
} events SEC(".maps");

SEC("kprobe/tcp_connect")
int BPF_KPROBE(trace_tcp_connect, struct sock *sk)
{
    if (BPF_CORE_READ(sk, __sk_common.skc_family) != AF_INET)
        return 0;                                // IPv4 cho gọn

    struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
    if (!e) return 0;
    e->pid   = bpf_get_current_pid_tgid() >> 32;
    e->saddr = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr);  // CO-RE (Bài 5)
    e->daddr = BPF_CORE_READ(sk, __sk_common.skc_daddr);
    e->dport = bpf_ntohs(BPF_CORE_READ(sk, __sk_common.skc_dport));
    bpf_get_current_comm(&e->comm, sizeof(e->comm));
    bpf_ringbuf_submit(e, 0);                        // đẩy lên userspace (Bài 9)
    return 0;
}

BPF_KPROBE (macro libbpf) khai triển chữ ký kprobe để lấy struct sock *sk từ pt_regs. Các trường socket đọc bằng BPF_CORE_READ — relocate theo BTF của kernel này (Bài 5), nên cùng binary chạy được trên kernel khác layout. Sự kiện đẩy qua ring buffer y như execsnoop (Bài 9).

Phía userspace: loader Go

//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang connmon connmon.bpf.c -- -I. -D__TARGET_ARCH_x86

func main() {
    rlimit.RemoveMemlock()
    objs := connmonObjects{}
    loadConnmonObjects(&objs, nil)                              // nạp + verifier + CO-RE + JIT
    defer objs.Close()

    kp, _ := link.Kprobe("tcp_connect", objs.TraceTcpConnect, nil)  // gắn kprobe
    defer kp.Close()

    rd, _ := ringbuf.NewReader(objs.Events)                    // đọc ring buffer
    defer rd.Close()

    for {
        rec, err := rd.Read()
        if errors.Is(err, ringbuf.ErrClosed) { return }
        var e event
        binary.Read(bytes.NewReader(rec.RawSample), binary.LittleEndian, &e)
        fmt.Printf("%-12s %-16s %-7d %-15s -> %s:%d\n",
            time.Now().Format("15:04:05.000"), cstr(e.Comm[:]), e.Pid,
            ipv4(e.Saddr), ipv4(e.Daddr), e.Dport)
    }
}

Đúng khung của Bài 10: bpf2go nhúng object + sinh connmonObjects/loadConnmonObjects; link.Kprobe thay link.Tracepoint; ringbuf.Reader đọc sự kiện. ipv4() đổi __be32 sang chuỗi a.b.c.d (byte thấp = octet đầu).

Một cái bẫy build thật: kprobe cần biết kiến trúc

Lần go generate đầu báo lỗi lạ:

GCC error "The eBPF is using target specific macros, please provide -target
that is not bpf, bpfel or bpfeb"

Nguyên nhân: BPF_KPROBE lấy tham số từ pt_regs, mà layout pt_regs khác nhau theo kiến trúc CPUbpf_tracing.h cần biết kiến trúc đích qua macro __TARGET_ARCH_x86. Bài 9-10 dùng tracepoint (không đụng pt_regs) nên không gặp; kprobe thì cần. Sửa: thêm -D__TARGET_ARCH_x86 vào cờ biên dịch của bpf2go (thấy trong directive go:generate ở trên). Một cái bẫy rất thật khi chuyển từ tracepoint sang kprobe.

Chạy: thấy cả node đang kết nối đi đâu

go generate && go build -o connmon .
sudo ./connmon
TIME         COMM             PID     SADDR           -> DADDR:PORT
03:39:43.301 coredns          2555    127.0.0.1       -> 127.0.0.1:8080
03:39:44.082 kubelet          817     10.200.0.64     -> 10.200.0.180:9808
03:39:44.093 curl             7812    10.0.1.20       -> 10.0.1.10:6443
03:39:45.868 kubelet          817     10.200.0.64     -> 10.200.0.30:8181
03:39:48.803 kubelet          817     10.0.1.20       -> 10.0.1.20:9808

Một binary tĩnh ~5.4MB, chạy là thấy ngay đời sống mạng của node: coredns self-check cổng 8080, kubelet thăm dò sức khỏe các pod (:9808, :8181), và curl của ta đi tới kube-apiserver qua LB (10.0.1.10:6443). Mỗi dòng là một tcp_connect thật, bắt ở nhân, đẩy qua ring buffer, in realtime — không cài gì vào các tiến trình đó. Thoát (Ctrl+C) là kprobe tự gỡ, node về 140 chương trình. Đây là một tcpconnect thu nhỏ — đúng kiểu công cụ mà các bộ quan sát sản xuất (Cilium, Pixie, Falco) xây ở quy mô lớn.

🧹 Dọn dẹp

connmon tự gỡ kprobe + ring buffer khi thoát; node về 140 chương trình. Mã nguồn đầy đủ (connmon.bpf.c, main.go, go.mod/go.sum) ở github.com/nghiadaulau/ebpf-from-scratch, thư mục 21-capstone-connmon; chạy go generate && go build.

Nhìn lại: hành trình eBPF từ số không

Hai mươi mốt bài, một mạch:

  • Part I — Nền tảng: máy ảo eBPF (thanh ghi, lệnh, JIT), verifier chứng minh an toàn, maps chia sẻ trạng thái, program type & hook, BTF + CO-RE để một binary chạy đa kernel. Mỗi khái niệm neo vào 140 chương trình thật của Cilium trên node.
  • Part II — Tracing: bpftrace từ one-liner tới histogram tới soi pod từ host — quan sát không cần viết C.
  • Part III — Viết công cụ thật: execsnoop bằng C (libbpf + skeleton + ring buffer) rồi bằng Go (cilium/ebpf + bpf2go).
  • Part IV — Networking: XDP (firewall, hook sớm nhất), tc/sched_cls, mổ datapath Cilium thật, tự viết tc.
  • Part V — Security: LSM BPF (chặn trước, cần bật bpf LSM), seccomp (cBPF, mọi container), kiểu Tetragon (kprobe + bpf_send_signal).
  • Part VI — Observability: CPU profiling (perf_event), off-CPU/độ trễ scheduler, Hubble internals.
  • Part VII — Ghép lại: hành trình một gói qua datapath, và connmon bài này.

Sợi chỉ xuyên suốt: eBPF là một công nghệ — nạp một chương trình nhỏ vào nhân, verifier chứng minh nó an toàn, JIT biên dịch, nó gắn vào một hook và chia sẻ trạng thái qua map. Từ nền đó mọc ra tracing, networking, security, observability — và cả một dự án như Cilium. Bắt đầu từ số không, giờ bạn đọc được 140 chương trình trên một node thật, viết được công cụ của riêng mình bằng C lẫn Go, và hiểu vì sao hạ tầng cloud-native hiện đại dựng trên eBPF.

Cảm ơn đã theo tới cuối. Toàn bộ mã nguồn hands-on ở github.com/nghiadaulau/ebpf-from-scratch — clone về, chạy thử, sửa, và viết chương trình eBPF của riêng bạn.

Related Posts