Đạt được thông lượng hàng chục gigabit mỗi giây trong các cổng VPN không gian người dùng (userspace) đòi hỏi sự thay đổi căn bản trong cách chuyển tiếp gói tin giữa card mạng vật lý (NIC) và tiến trình ứng dụng. Theo truyền thống, các đường hầm bảo mật viết bằng Go, Rust hoặc C kết nối với hệ điều hành thông qua thiết bị ảo TUN/TAP (/dev/net/tun). Mặc dù TUN/TAP mang lại tính tương thích rộng rãi, kiến trúc này tạo ra độ trễ rất lớn: mỗi gói tin đều phải trải qua chuyển đổi ngữ cảnh nhân-người dùng, cấp phát bộ nhớ trung gian struct sk_buff và sao chép dữ liệu qua lệnh copy_to_user().

Ở tốc độ 10 Gbps và 40 Gbps, các chi phí phụ trội này làm nghẽn bus bộ nhớ CPU và tráo đổi liên tục bộ nhớ đệm, giới hạn thông lượng đơn lõi ở mức khoảng 1,5 triệu gói tin mỗi giây (Mpps). Mặc dù bộ phát triển mặt phẳng dữ liệu DPDK giải quyết được vấn đề này bằng cách chiếm quyền điều khiển trực tiếp phần cứng mạng, DPDK lại tách rời card mạng hoàn toàn khỏi hệ điều hành, vô hiệu hóa các công cụ quản trị chuẩn của Linux (iproute2, nftables, tcpdump) cũng như các dịch vụ mạng thông thường.

Cơ chế Address Family XDP (AF_XDP, còn gọi là XSK) của nhân Linux kết nối hoàn hảo hai nhu cầu này. Bằng cách kết hợp khả năng điều hướng gói tin eBPF ở tầng driver với vùng nhớ dùng chung UMEM do người dùng cấp phát, AF_XDP mang lại khả năng truyền gói tin zero-copy từ phần cứng trực tiếp vào ứng dụng mà vẫn bảo toàn môi trường mạng của Linux.

So sánh kiến trúc đường truyền: TUN/TAP và AF_XDP

Để nhận thấy sự khác biệt về mặt hiệu năng, hãy xem xét vòng đời của một gói tin VPN mã hóa trên cả hai mô hình đường truyền:

[Đường truyền TUN/TAP truyền thống]
Hàng đợi RX của NIC ──> alloc_skb() ──> Ngăn xếp IP nhân / Netfilter ──> Driver TUN
                                                                             │
                      Chuyển ngữ cảnh + Sao chép bộ nhớ (copy_to_user) <─────┘
                                             │
                                             ▼
                                  Ứng dụng VPN (Giải mã)

─────────────────────────────────────────────────────────────────────────

[Đường truyền Zero-Copy AF_XDP]
Hàng đợi RX của NIC ──> Truy cập bộ nhớ trực tiếp (DMA) ──> Khung UMEM cấp phát trước
                             │
                 Bộ lọc XDP: bpf_redirect_map()
                             │
                             ▼ (Zero-Copy, chuyển giao chỉ mục không khóa)
                 Tiến trình VPN (Giải mã trực tiếp trên bộ nhớ)

Trong quy trình AF_XDP, nhân hệ điều hành không cần khởi tạo cấu trúc struct sk_buff. Bộ điều khiển DMA trên card mạng vật lý ghi thẳng các khung Ethernet thô vào các khối bộ nhớ UMEM đã đăng ký từ trước. Một chương trình eBPF đính kèm tại tầng driver (BPF_PROG_TYPE_XDP) kiểm tra phần đầu gói tin. Nếu gói tin thuộc về dịch vụ VPN (khớp với cổng UDP hoặc chữ ký phiên), chương trình sẽ gọi lệnh bpf_redirect_map(&xsk_map, queue_id, 0). Các luồng dữ liệu thông thường khác nhận giá trị XDP_PASS và đi tiếp vào ngăn xếp IP của Linux như bình thường.

Kiến trúc vùng nhớ UMEM và bốn hàng đợi vòng không khóa

Cốt lõi vận hành của AF_XDP là UMEM—một vùng nhớ ảo liên tục được đăng ký với nhân thông qua lời gọi hệ thống setsockopt(..., SOL_XDP, XDP_UMEM_REG, ...). Vùng nhớ này được chia nhỏ thành các khung có kích thước cố định (thường là 2048 hoặc 4096 byte).

Quá trình giao tiếp giữa không gian người dùng và driver nhân diễn ra thông qua bốn hàng đợi vòng (circular ring queues) đơn người sản xuất, đơn người tiêu thụ (SPSC) không cần dùng khóa:

  1. Hàng đợi Fill (Người dùng sản xuất -> Nhân tiêu thụ): Tiến trình người dùng gửi các địa chỉ khung UMEM còn trống vào hàng đợi này, cung cấp sẵn bộ nhớ để driver card mạng nhận các gói tin mới đến.
  2. Hàng đợi Rx (Nhân sản xuất -> Người dùng tiêu thụ): Khi gói tin đến, driver card mạng đặt các bộ mô tả khung (độ lệch bộ nhớ, độ dài gói tin) vào hàng đợi Rx để ứng dụng tiếp nhận xử lý.
  3. Hàng đợi Tx (Người dùng sản xuất -> Nhân tiêu thụ): Khi cần gửi các gói tin mã hóa ra ngoài, ứng dụng xếp các bộ mô tả khung dữ liệu vào hàng đợi Tx để card mạng phát trực tiếp lên đường truyền.
  4. Hàng đợi Completion (Nhân sản xuất -> Người dùng tiêu thụ): Sau khi card mạng phát thành công khung dữ liệu, driver ghi địa chỉ khung vào hàng đợi Completion để trả lại quyền sử dụng bộ đệm cho ứng dụng tái chế.

Do các hàng đợi này hoạt động hoàn toàn bằng các phép toán tăng con trỏ nguyên tử, không có khóa tương hỗ (mutex) hay khóa xoay, CPU có thể xử lý các lô gói tin gồm 32 hoặc 64 phần tử với hiệu suất tối đa mà không gặp xung đột luồng.

Cài đặt trong nhân: Điều hướng XDP ở tầng driver

Thành phần chạy trong nhân là một bộ phân loại eBPF tinh gọn được gắn trực tiếp vào giao diện mạng vật lý:

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <bpf/bpf_helpers.h>

struct {
    __uint(type, BPF_MAP_TYPE_XSKMAP);
    __uint(max_entries, 64);
    __type(key, __u32);
    __type(value, __u32);
} qidconf_map SEC(".maps");

SEC("xdp")
int xdp_vpn_redirect(struct xdp_md *ctx)
{
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;
    
    /* Kiem tra gioi han bo nho cua phan dau Ethernet */
    if (data + sizeof(struct ethhdr) > data_end)
        return XDP_PASS;
        
    struct ethhdr *eth = data;
    if (eth->h_proto != __constant_htons(ETH_P_IP))
        return XDP_PASS;
        
    struct iphdr *iph = data + sizeof(struct ethhdr);
    if ((void *)(iph + 1) > data_end || iph->protocol != IPPROTO_UDP)
        return XDP_PASS;
        
    struct udphdr *udph = (void *)iph + (iph->ihl * 4);
    if ((void *)(udph + 1) > data_end)
        return XDP_PASS;
        
    /* Dieu huong cong UDP 51820 sang ban do socket AF_XDP */
    if (udph->dest == __constant_htons(51820)) {
        return bpf_redirect_map(&qidconf_map, ctx->rx_queue_index, XDP_PASS);
    }
    
    return XDP_PASS;
}

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

Đoạn mã này thực thi trong vài nano giây ngay tại ngữ cảnh ngắt của driver mạng. Các gói tin VPN được chuyển thẳng vào các hàng đợi của người dùng, trong khi các gói SSH, quản trị hệ thống và BGP tiếp tục di chuyển bình thường qua ngăn xếp mạng Linux.

Khởi tạo từ không gian người dùng và thăm dò zero-copy

Ở không gian người dùng, tiến trình khởi tạo cấp phát bộ nhớ thông qua posix_memalign(), thiết lập các thông số UMEM và liên kết socket với cờ zero-copy:

#include <xdp/xsk.h>
#include <stdlib.h>
#include <sys/socket.h>

#define NUM_FRAMES 4096
#define FRAME_SIZE 2048

struct xsk_umem_info {
    struct xsk_ring_prod fq;
    struct xsk_ring_cons cq;
    struct xsk_umem *umem;
    void *buffer;
};

int init_af_xdp_socket(struct xsk_umem_info *umem_info, const char *ifname, int queue_id)
{
    struct xsk_socket_config xsk_cfg;
    struct xsk_umem_config umem_cfg = {
        .fill_size = NUM_FRAMES,
        .comp_size = NUM_FRAMES,
        .frame_size = FRAME_SIZE,
        .frame_headroom = XDP_PACKET_HEADROOM,
        .flags = 0
    };
    
    /* Cap phat bo dem bo nho lien tuc da can chinh */
    posix_memalign(&umem_info->buffer, getpagesize(), NUM_FRAMES * FRAME_SIZE);
    
    /* Dang ky vung nho UMEM voi nhan Linux */
    xsk_umem__create(&umem_info->umem, umem_info->buffer, 
                     NUM_FRAMES * FRAME_SIZE, &umem_info->fq, 
                     &umem_info->cq, &umem_cfg);
                     
    xsk_cfg.rx_size = NUM_FRAMES;
    xsk_cfg.tx_size = NUM_FRAMES;
    xsk_cfg.libbpf_flags = 0;
    xsk_cfg.xdp_flags = XDP_FLAGS_UPDATE_IF_NOEXIST | XDP_FLAGS_DRV_MODE;
    xsk_cfg.bind_flags = XDP_ZEROCOPY | XDP_USE_NEED_WAKEUP;
    
    return 0;
}

Bằng cách sử dụng cờ XDP_USE_NEED_WAKEUP, ứng dụng giảm thiểu tối đa các lời gọi hệ thống không cần thiết. Nhân chỉ can thiệp vào tiến trình thăm dò phần cứng khi ứng dụng gửi tín hiệu thông báo hàng đợi cần được bổ sung.

Hiệu quả thực tiễn trong mạng Anycast RiceGuard

Hạ tầng Anycast của RiceGuard triển khai AF_XDP tại các cụm cổng kết nối biên nhằm duy trì hiệu năng ổn định dưới tải lượng đa người dùng cực lớn:

  • Tốc độ xử lý hàng chục triệu gói tin: RiceGuard xử lý hơn 24 triệu gói tin mỗi giây (Mpps) trên mỗi nút máy chủ kép, tăng vọt so với các giao diện TUN truyền thống.
  • Độ trễ xử lý cực thấp: Nhờ loại bỏ tranh chấp khóa socket và việc sao chép bộ nhớ trong nhân, độ trễ chuyển tiếp của cổng mạng luôn duy trì dưới 1,2 micro giây.
  • Duy trì kiến trúc vận hành chuẩn của Linux: Giao diện mạng vật lý vẫn đóng vai trò là một card mạng Linux thông thường, cho phép các công cụ giám sát và tiến trình định tuyến BGP vận hành trơn tru song hành cùng đường truyền VPN tăng tốc.

Nhờ tận dụng AF_XDP và kiến trúc bộ nhớ UMEM zero-copy, các giải pháp WireGuard và RiceGuard nâng tốc độ xử lý gói tin của không gian người dùng chạm ngưỡng giới hạn vật lý của phần cứng mà không làm mất đi khả năng tích hợp linh hoạt với hệ điều hành Linux.