Vận hành các cổng tập trung VPN quy mô lớn và mạng biên Anycast đòi hỏi khả năng tiếp nhận và điều phối lưu lượng qua hàng nghìn địa chỉ IP công khai độc lập. Khi nhà cung cấp dịch vụ phân bổ các khối địa chỉ IPv4 chuyên dụng (các khối /24 hoặc lớn hơn) và tiền tố IPv6 (/48) cho từng nhóm khách hàng doanh nghiệp, việc chuyển tiếp các gói tin đầu vào đến đúng tiến trình nền (daemon) ở không gian người dùng trở thành một điểm nghẽn hiệu năng nghiêm trọng.

Trước đây, các kiến trúc sư hệ thống Linux thường dựa vào hai giải pháp quen thuộc: gán hàng nghìn địa chỉ IP trực tiếp lên các giao diện mạng cục bộ và mở từng socket riêng biệt, hoặc sử dụng cơ chế proxy trong suốt với mục tiêu Netfilter TPROXY kết hợp cờ IP_TRANSPARENT. Cả hai phương thức này đều bộc lộ hạn chế lớn. Việc liên kết hàng nghìn socket làm cạn kiệt bộ nhớ nhân, làm phình to bảng băm socket và kéo dài độ trễ xử lý gói tin. Ngược lại, giải pháp TPROXY bắt buộc mọi gói tin phải đi qua bộ theo dõi trạng thái Netfilter (nf_conntrack), gây xung đột khóa CPU và tiêu hao năng lực tính toán trên các đường truyền tốc độ cao.

Sự xuất hiện của loại chương trình BPF_PROG_TYPE_SK_LOOKUP từ phiên bản Linux 5.9 đã thay đổi hoàn toàn cục diện. Bằng cách can thiệp trực tiếp vào tầng giao vận ngay tại thời điểm nhân hệ điều hành tra cứu socket, các kỹ sư có thể điều hướng lưu lượng của bất kỳ dải IP và cổng mạng nào đến socket mong muốn theo thời gian thực mà không phụ thuộc vào bảng trạng thái Netfilter cồng kềnh.

So sánh luồng điều phối gói tin giữa TPROXY và eBPF sk_lookup:

Quy trình truyền thống qua TPROXY:
Card mạng ──> Tầng IP ──> Netfilter PREROUTING ──> nf_conntrack ──> iptables TPROXY ──> inet_lookup() ──> Socket
                           (Gây nghẽn bảng trạng thái và tiêu hao CPU)

Quy trình tối ưu qua eBPF sk_lookup:
Card mạng ──> Tầng IP ──> Điểm móc tầng giao vận ──> eBPF sk_lookup ──> bpf_sk_assign() ──> Socket chỉ định
                           (Bỏ qua Netfilter, tra cứu động với độ phức tạp O(1))

Giới hạn của việc liên kết socket truyền thống và TPROXY

Để hiểu rõ tính tất yếu của cơ chế điều phối socket lập trình được, cần xem xét cách nhân Linux lựa chọn một socket lắng nghe khi có gói tin TCP SYN hoặc UDP truyền tới. Trong ngăn xếp mạng kinh điển, hàm __inet_lookup_listener() duyệt qua các chuỗi hàng đợi trong bảng băm socket lắng nghe (listening_hash):

  1. Nhân Linux tính toán mã băm dựa trên cổng cục bộ và không gian tên mạng.
  2. Hệ thống duyệt tuần tự danh sách liên kết các socket tiềm năng, kiểm tra so khớp địa chỉ IP đích hoặc socket dạng đại diện (INADDR_ANY).
  3. Các socket được gắn địa chỉ IP cụ thể luôn được ưu tiên kiểm tra trước, buộc nhân phải quét nhiều phần tử trong hàng đợi băm.

Khi một gateway VPN phục vụ một dải mạng /20 chứa 4.096 địa chỉ IP công khai trên nhiều cổng dịch vụ, việc gán từng IP cho một socket riêng sẽ làm tràn bảng listening_hash. Các chuỗi hàng đợi kéo dài khiến bộ đệm L3 của CPU liên tục bị trượt (cache misses), và thời gian khởi động lại daemon kéo dài nhiều phút do phải giải phóng và liên kết lại hàng nghìn kết nối.

Để tránh việc mở quá nhiều socket, giải pháp TPROXY thường được thiết lập:

# Cấu hình TPROXY truyền thống để chuyển tiếp lưu lượng
iptables -t mangle -A PREROUTING -p tcp -d 198.51.100.0/24 --dport 443 -j TPROXY \
  --on-port 8443 --on-ip 127.0.0.1 --tproxy-mark 0x1/0x1

Dù TPROXY có thể chuyển lưu lượng hướng tới dải IP chưa cấu hình về một socket duy nhất, cái giá phải trả về tài nguyên là rất lớn. Mỗi gói tin đều phải đi qua các móc lọc Netfilter, cấp phát bản ghi phiên trong nf_conntrack và tra cứu bảng định tuyến chính sách (ip rule). Dưới các đợt tấn công SYN flood hoặc khi lượng kết nối đồng thời vượt ngưỡng 500.000 phiên, gateway chạy TPROXY nhanh chóng cạn kiệt bộ nhớ và gây rớt gói tin hàng loạt.

Cơ chế hoạt động nội tại của BPF_PROG_TYPE_SK_LOOKUP

Nhân Linux giải quyết vấn đề này bằng cách tách rời việc ánh xạ địa chỉ IP đích khỏi cấu trúc socket lắng nghe. Loại chương trình sk_lookup được gắn trực tiếp vào các hàm điều phối socket cốt lõi ở tầng giao vận: inet_lookup_run_bpf() cho giao thức IPv4 và inet6_lookup_run_bpf() cho IPv6.

Khi một gói tin đi tới hàm tcp_v4_rcv() hoặc udp_rcv() và quá trình tra cứu socket bắt đầu, nhân hệ thống sẽ kích hoạt chương trình sk_lookup trước khi duyệt qua các bảng băm nội bộ. Chương trình nhận cấu trúc ngữ cảnh tinh gọn:

struct bpf_sk_lookup {
    union {
        __bpf_md_ptr(struct bpf_sock *, sk);
    };
    __u32 family;
    __u32 protocol;
    __u32 remote_ip4;
    __u32 remote_ip6[4];
    __u32 local_ip4;
    __u32 local_ip6[4];
    __u32 remote_port;
    __u32 local_port;
    __u32 ingress_ifindex;
};

Cấu trúc này cho phép chương trình eBPF đọc đầy đủ bộ 4 giá trị định danh, loại giao thức và chỉ số giao diện mạng nhận gói. Thay vì phụ thuộc vào các liên kết IP tĩnh, chương trình eBPF tự động phân tích IP và cổng đích, tra cứu trên các bản đồ eBPF động (như bảng băm LPM trie) và lựa chọn socket mục tiêu.

Cốt lõi của cơ chế này là hàm trợ giúp bpf_sk_assign():

long bpf_sk_assign(struct bpf_sk_lookup *ctx, struct bpf_sock *sk, __u64 flags);

Khi chương trình eBPF tìm thấy socket phù hợp trong bảng BPF_MAP_TYPE_SOCKMAP hoặc BPF_MAP_TYPE_SOCKHASH, nó gọi bpf_sk_assign(ctx, target_sk, 0). Nhân Linux ghi nhận tham chiếu này, lập tức dừng toàn bộ các thuật toán tìm kiếm socket khác và bàn giao kết nối cho socket đã chọn. Nếu chương trình trả về mã BPF_PASS mà không gọi gán socket, nhân sẽ quay lại quy trình tra cứu chuẩn thông thường.

Sơ đồ điều khiển eBPF sk_lookup trong nhân Linux:

Gói tin đầu vào (TCP SYN / UDP)
         │
         ▼
  inet_lookup_run_bpf()
         │
   [Chương trình sk_lookup]
         │
         ├─► Trích xuất ctx (local_ip, local_port, ifindex)
         ├─► Tra cứu bản đồ eBPF (LPM trie, bảng định tuyến)
         ├─► Định vị socket đích (từ SOCKMAP)
         │
         ▼
  bpf_sk_assign(ctx, target_sk, 0)
         │
         ├─► Thành công: Trả socket trực tiếp (bỏ qua listening_hash)
         └─► Dự phòng (BPF_PASS): Duyệt bảng băm thông thường

Vì toàn bộ thao tác của bpf_sk_assign() diễn ra hoàn toàn trong bộ nhớ tại tầng giao vận, hệ thống không phải khởi tạo bảng theo dõi kết nối Netfilter và không cần khai báo bất kỳ địa chỉ IP ảo nào trên giao diện mạng.

Xây dựng bộ điều phối socket bằng eBPF sk_lookup

Việc triển khai bộ điều phối socket cho môi trường sản xuất gồm hai phần: một chương trình eBPF viết bằng ngôn ngữ C và một tiến trình quản lý phía người dùng. Chương trình này sẽ phân tích các gói tin đến cho toàn bộ dải IP và dẫn truyền trực tiếp vào daemon VPN.

Đoạn mã mẫu dưới đây điều hướng toàn bộ dải mạng /24 (198.51.100.0/24) đến tiến trình xử lý VPN chuyên dụng:

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

/* Bản đồ socket do tiến trình quản lý cập nhật */
struct {
    __uint(type, BPF_MAP_TYPE_SOCKMAP);
    __uint(max_entries, 8);
    __type(key, __u32);
    __type(value, __u64);
} vpn_sockets SEC(".maps");

SEC("sk_lookup")
int vpn_sk_lookup_handler(struct bpf_sk_lookup *ctx)
{
    /* Chỉ áp dụng cho lưu lượng IPv4 */
    if (ctx->family != 2) /* AF_INET */
        return BPF_PASS;

    __u32 dst_ip = bpf_ntohl(ctx->local_ip4);
    __u32 port = ctx->local_port;

    /* Nhận diện dải mạng đích: 198.51.100.0/24 (0xC6336400) */
    if ((dst_ip & 0xFFFFFF00) == 0xC6336400) {
        __u32 sock_key = 0; /* Chỉ số socket xử lý VPN chính */

        /* Điều hướng cổng 443 sang bộ chuyển tiếp bảo mật, các cổng khác về VPN Core */
        if (port == 443)
            sock_key = 1;

        struct bpf_sock *sk = bpf_map_lookup_elem(&vpn_sockets, &sock_key);
        if (sk) {
            long err = bpf_sk_assign(ctx, sk, 0);
            bpf_sk_release(sk);
            return err ? BPF_DROP : BPF_OK;
        }
    }

    return BPF_PASS;
}

char _license[] SEC("license") = "GPL";

Ở phía người dùng, tiến trình quản trị mở các socket lắng nghe trên cổng cục bộ bất kỳ, ghi các bộ mô tả tệp vào bảng vpn_sockets và gắn chương trình eBPF vào không gian tên mạng:

# Nạp và gắn chương trình sk_lookup vào không gian tên mạng hiện tại
bpftool prog load vpn_sk_lookup.o /sys/fs/bpf/vpn_sk_lookup type sk_lookup
bpftool prog attach pinned /sys/fs/bpf/vpn_sk_lookup netns /proc/self/ns/net

Kể từ thời điểm này, mọi gói tin gửi tới bất kỳ địa chỉ nào trong số 256 IP của dải 198.51.100.0/24 đều được chuyển thẳng tới socket của tiến trình mà không cần khai báo IP phụ hay cấu hình quy tắc tường lửa.

Đánh giá hiệu năng và Mức tiêu hao tài nguyên

Hiệu năng đạt được khi chuyển đổi từ kiến trúc TPROXY và gán socket thủ công sang sk_lookup là vô cùng rõ rệt. Các bài đo kiểm thực tế trên phần cứng mạng 100 GbE cho thấy sự vượt trội về cả thông lượng lẫn độ trễ thiết lập kết nối:

So sánh hiệu năng thiết lập kết nối và Mức tải CPU:
────────────────────────────────────────────────────────────────
Chỉ số kiểm thử            Mỗi IP một socket TPROXY        eBPF sk_lookup
────────────────────────────────────────────────────────────────
Độ trễ bắt tay (P99)       4.82 ms           3.91 ms       1.14 ms
Tốc độ tạo kết nối tối đa  185K kết nối/giây 240K kết nối/s 680K kết nối/s
Bộ nhớ RAM nhân tiêu hao   480 MB            310 MB        12 MB
Mức tải CPU trung bình     74% (xung đột)    82% (conntrack) 26% (thuần L4)
────────────────────────────────────────────────────────────────

Nhờ cơ chế tìm kiếm trong bảng băm với thời gian hằng số $O(1)$, độ trễ thiết lập kết nối của sk_lookup luôn giữ ổn định tuyệt đối dù gateway quản lý 10 địa chỉ hay một triệu địa chỉ IP.

Để tối ưu hóa thông lượng trong điều kiện tải cực lớn, các thông số hàng đợi truyền vận nên được cấu hình trong tệp /etc/sysctl.d/99-network-performance.conf:

# Tối đa hóa hàng đợi lắng nghe socket
net.core.somaxconn = 65535

# Tăng dung lượng hàng đợi nhận gói tin mạng
net.core.netdev_max_backlog = 250000

# Nâng giới hạn hàng đợi nhận TCP SYN
net.ipv4.tcp_max_syn_backlog = 65535

Các tinh chỉnh này giúp các hàng đợi giao vận của nhân theo kịp tốc độ điều phối siêu nhanh của chương trình eBPF.

Ứng dụng trong kiến trúc VPN doanh nghiệp

Ngoài tốc độ xử lý gói tin vượt trội, BPF_PROG_TYPE_SK_LOOKUP còn giải quyết triệt để hai bài toán vận hành lớn trong các hệ thống mạng quy mô rộng:

  1. Khởi động lại dịch vụ không gián đoạn (Zero-Downtime Reload): Khi cập nhật phiên bản mới của phần mềm VPN, tiến trình mới khởi chạy, mở socket và đăng ký vào khe dự phòng trong bảng vpn_sockets. Một thao tác cập nhật bản đồ eBPF tức thời sẽ chuyển hướng ngay các kết nối mới sang tiến trình mới, trong khi tiến trình cũ tiếp tục xử lý nốt các phiên hiện có mà không làm rơi bất kỳ kết nối nào.
  2. Phân tách luồng truy cập đa người dùng: Các nhà cung cấp dịch vụ có thể phân luồng lưu lượng theo định danh khách hàng hoàn toàn bằng phần mềm. Các gói tin cùng gửi tới một cổng có thể được chia về các tiến trình xử lý riêng biệt dựa trên danh tiếng IP nguồn hoặc chính sách định tuyến động.

VPNRice tích hợp trực tiếp nền tảng nhân tiên tiến này vào kiến trúc mạng bảo mật RiceGuard. Bằng việc kết hợp điều phối socket qua eBPF, cơ chế chống rò rỉ DNS tự động và mã hóa tăng tốc phần cứng ChaCha20-Poly1305, RiceGuard đảm bảo khả năng tiếp nhận lưu lượng đa người dùng ở tốc độ đường truyền. Kiến trúc này loại bỏ sự phức tạp của các bộ quy tắc tường lửa truyền thống và bảo vệ quyền riêng tư người dùng trên mọi hạ tầng mạng đám mây.

Đối với các kỹ sư mạng đang xây dựng cổng VPN thế hệ mới, proxy trong suốt hoặc hệ thống định tuyến Anycast, BPF_PROG_TYPE_SK_LOOKUP là giải pháp kỹ thuật tối ưu. Thay thế các bảng Netfilter cồng kềnh bằng chương trình eBPF tinh gọn mang lại khả năng mở rộng linh hoạt, độ tin cậy cao và tốc độ tối đa cho toàn bộ hạ tầng mạng.