شبکه‌های سازمانی امروزی و زیرساخت‌های ابری نیازمند آن هستند که درگاه‌های متمرکز VPN توانایی پردازش ده‌ها میلیون بسته در ثانیه (Mpps) را روی رابط‌های پرسرعت ۴۰ و ۱۰۰ گیگابیت بر ثانیه داشته باشند. در چنین سرعت‌های بالایی، پشته‌های سنتی شبکه در سیستم‌عامل با یک مانع ساختاری روبرو می‌شوند: مصرف منابع پردازنده به ازای هر بسته مستقل.

هر زمان که یک رابط تونل نرم‌افزاری استاندارد لینوکس (مانند TUN/TAP، IP-in-IP، GRE یا VXLAN) بسته‌ها را کپسوله یا بازگشایی می‌کند، هسته مجبور است یک ساختار داده ۲۴۰ بایتی به نام struct sk_buff ایجاد کند، حافظه را با فراخوانی pskb_expand_head() بازتخصیص دهد و بسته را از لایه‌های متعدد فیلترینگ عبور دهد. حتی با قوی‌ترین بهینه‌سازی‌های سخت‌افزاری، روش‌های سنتی تونل‌سازی هسته معمولاً در نرخ ۱.۵ تا ۲.۵ میلیون بسته در ثانیه یک هسته پردازنده را به اشباع کامل می‌رسانند.

فناوری eXpress Data Path (XDP) به همراه تابع کمکی bpf_xdp_adjust_head() این مدل را به طور ریشه‌ای دگرگون کرده است. با اجرای مستقیم دستورات در حلقه نظارت بر وقفه‌های درایور کارت شبکه (NAPI) پیش از ساخت بافرهای سوکت، XDP به مهندسان امکان می‌دهد کپسوله‌سازی و استخراج بسته‌ها را به شیوه zero-copy و در سرعت خطی کارت شبکه به انجام برسانند و به سرعتی فراتر از ۱۴ میلیون بسته در ثانیه روی یک هسته پردازنده دست یابند.

مقایسه تونل‌سازی سنتی در هسته با مسیر سریع XDP درون درایور:

مسیر تونل سنتی هسته (مصرف بالای منابع پردازنده):
کابل فیزیکی ──> حافظه DMA کارت شبکه ──> تخصیص sk_buff (۲۴۰ بایت) ──> Netfilter PREROUTING
                                                                            │
                                                                            ▼
[سرویس تونل / کارت مجازی] ◄── pskb_expand_head() (بازتخصیص حافظه) ◄── جدول مسیریابی
           │
           ▼ (کپسوله‌سازی با سرآیندهای خارجی IP و UDP)
ورود دوباره به پشته ──> Netfilter POSTROUTING ──> صف qdisc ──> حلقه ارسال کارت شبکه
سقف توان پردازش: حدود ۲.۰ میلیون بسته در ثانیه روی هر هسته CPU

مسیر سریع zero-copy در فناوری XDP:
کابل فیزیکی ──> حافظه DMA کارت شبکه ──> حلقه NAPI RX ──> اجرای برنامه XDP
                                                                  │
                                                                  ▼
                                                     bpf_xdp_adjust_head(ctx, -delta)
                                                     (گسترش فضا دقیقاً در حلقه DMA درایور!)
                                                                  │
                                                                  ▼
                                                     نوشتن سرآیندهای کپسوله‌سازی IP و UDP
                                                                  │
                                                                  ▼
                                                     دستور: XDP_TX / bpf_redirect_map()
                                                     (ارسال آنی بدون ساخت ساختار sk_buff!)
توان عملیاتی: حدود ۱۴.۵ میلیون بسته در ثانیه روی هر هسته CPU (سرعت خطی سخت‌افزار!)

ناکارآمدی‌های ساختاری در تونل‌های سنتی لینوکس

برای درک دلایل برتری عملکردی XDP، باید عملیات درونی هسته لینوکس هنگام کپسوله‌سازی بسته را بررسی نماییم:

  1. هزینه پردازشی تخصیص sk_buff: با ورود هر بسته خام، درایور ناچار به مقداردهی اولیه شیء struct sk_buff است. این کار مستلزم پاکسازی حافظه، به‌روزرسانی شمارنده‌های اتمی و ثبت فراداده‌های زمانی و رابط شبکه است.
  2. کمبود فضای خالی در ابتدای بافر (Headroom Starvation): درایورهای تونل مجازی باید سرآیندهای بیرونی (شامل اترنت، IPv4/IPv6 و UDP) را در ابتدای بسته بگنجانند. اگر بافر اولیه‌ای که کارت شبکه تدارک دیده فضای خالی کافی نداشته باشد، هسته تابع pskb_expand_head() را صدا می‌زند که منجر به کپی مجدد داده‌ها و آسیب جدی به حافظه کش پردازنده می‌شود.
  3. پیمایش چندباره لایه‌های نرم‌افزاری: بسته باید مسیر ورودی پروتکل IP، سامانه ردیابی اتصالات (nf_conntrack) و درایور مجازی را طی کند، سپس به صورت یک بسته جدید دوباره در لایه‌های خروجی شبکه پردازش شود.

در ترافیک‌های سنگین، هسته‌های پردازنده ممکن است تا ۷۰٪ از ظرفیت خود را صرف مدیریت حافظه و قفل‌های همگام‌سازی نمایند، بدون آنکه ترافیک واقعی کلاینت‌ها را جابجا کنند.

نحوه عملکرد bpf_xdp_adjust_head در سطح درایور

فناوری XDP در پایین‌ترین نقطه ممکن نرم‌افزاری اجرا می‌شود. در این موقعیت، بسته تنها به شکل یک بافر پیوسته فیزیکی در ساختار struct xdp_buff تعریف شده است:

struct xdp_buff {
    void *data;             /* نشانگر آغاز داده‌های بسته */
    void *data_end;         /* نشانگر پایان داده‌های بسته */
    void *data_meta;        /* بافر فراداده پیش از داده‌ها */
    void *data_hard_start;  /* آغاز فیزیکی صفحه حافظه */
    struct xdp_rxq_info *rxq;
    struct xdp_mem_info mem;
    u32 flags;
};

درایورهای شبکه پیشرفته (مانند mlx5، i40e، ixgbe و virtio_net) به طور پیش‌فرض فضای خالی مناسبی (معمولاً XDP_PACKET_HEADROOM = 256 بایت) را بین data_hard_start و data آزاد نگه می‌دارند.

تغییر مرزهای بسته با bpf_xdp_adjust_head

هسته لینوکس تابع کمکی زیر را در اختیار برنامه‌های BPF قرار می‌دهد:

long bpf_xdp_adjust_head(struct xdp_md *xdp_md, int delta);

پارامتر delta جهت تغییر مرز حافظه را مشخص می‌کند:

  • مقدار دلتای منفی (delta < 0) — کپسوله‌سازی: کسر بایت‌ها نشانگر ctx->data را به عقب و به سمت data_hard_start حرکت می‌دهد و بسته را وارد فضای خالی رزروشده درایور می‌کند. این کار یک حافظه دست‌نخورده فراهم می‌کند تا برنامه eBPF سرآیندهای بیرونی را مستقیماً در آن بنویسد.
  • مقدار دلتای مثبت (delta > 0) — استخراج داده‌ها (Decapsulation): افزودن بایت‌ها نشانگر ctx->data را جلو می‌برد که باعث حذف فوری سرآیندهای لایه بیرونی (مانند اترنت، IP و UDP) شده و بسته درونی را برای هدایت سریع آشکار می‌سازد.
چیدمان حافظه xdp_buff هنگام گسترش سرآیند بسته:

پیش از فراخوانی bpf_xdp_adjust_head (بسته اولیه دریافتی):
[ data_hard_start ] ──[ فضای خالی آماده (256B) ]──> [ data ]───────[ data_end ]
                                                      │              │
                                                      └─ بسته درونی ─┘

پس از فراخوانی bpf_xdp_adjust_head(ctx, -28):
[ data_hard_start ] ──[ فضای باقیمانده (228B) ]──> [ data ]─────────[ data_end ]
                                                    │                │
                                                    ├─ سرآیند IP (20B)
                                                    ├─ سرآیند UDP (8B)
                                                    └─ داده‌های درونی ┘

الزامات اعتبارسنجی BPF: نامعتبر شدن نشانگرها

یک نکته بسیار مهم هنگام توسعه برنامه‌های XDP، انطباق با اعتبارسنج امنیت هسته (BPF Verifier) است.

هنگامی که تابع bpf_xdp_adjust_head() با موفقیت اجرا می‌شود، نشانی فیزیکی ctx->data تغییر می‌یابد. به همین علت، اعتبارسنج هسته بلافاصله تمامی نشانگرهای قبلی و نتایج بررسی حریم حافظه را باطل می‌کند.

اگر برنامه‌نویس پس از اجرای این تابع از نشانگرهای قبلی استفاده کند، برنامه با خطای R1 invalid mem access از سوی هسته رد خواهد شد. برنامه‌نویس باید حتماً نشانی‌های ctx->data و ctx->data_end را دوباره فراخوانی کرده و آزمون‌های دامنه حافظه را تکرار کند:

// ۱. تنظیم فضای سرآیند
if (bpf_xdp_adjust_head(ctx, 0 - (int)encap_size)) {
    return XDP_DROP;
}

// ۲. فراخوانی مجدد نشانگرها به صورت اجباری
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;

// ۳. بررسی مجدد حریم بسته پیش از دسترسی
if (data + sizeof(struct ethhdr) + sizeof(struct iphdr) > data_end) {
    return XDP_DROP;
}

کد کامل پیاده‌سازی: کپسوله‌سازی تونل UDP با کارایی خطی

در ادامه یک برنامه کامل eBPF به زبان C ارائه شده است که کپسوله‌سازی بسته‌های IPv4 را بدون کپی حافظه به نمایش می‌گذارد:

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

#define TUNNEL_PORT 51820
#define ENCAP_SIZE (sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct udphdr))

static __always_inline __u16 csum_fold_helper(__u32 csum) {
    csum = (csum >> 16) + (csum & 0xffff);
    csum += (csum >> 16);
    return (__u16)~csum;
}

SEC("xdp")
int xdp_vpn_encapsulate(struct xdp_md *ctx) {
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;

    // بررسی سلامت سرآیند اترنت ورودی
    struct ethhdr *orig_eth = data;
    if ((void *)(orig_eth + 1) > data_end) {
        return XDP_PASS;
    }

    // پردازش اختصاصی ترافیک پروتکل IPv4
    if (orig_eth->h_proto != bpf_htons(ETH_P_IP)) {
        return XDP_PASS;
    }

    // باز کردن فضای سرآیند به اندازه مورد نیاز
    if (bpf_xdp_adjust_head(ctx, 0 - (int)ENCAP_SIZE)) {
        return XDP_DROP;
    }

    // بارگذاری دوباره نشانگرهای حافظه پس از تغییر
    data = (void *)(long)ctx->data;
    data_end = (void *)(long)ctx->data_end;

    struct ethhdr *eth = data;
    struct iphdr *ip = (void *)(eth + 1);
    struct udphdr *udp = (void *)(ip + 1);

    if ((void *)(udp + 1) > data_end) {
        return XDP_DROP;
    }

    // تنظیم اطلاعات سرآیند اترنت بیرونی
    __builtin_memcpy(eth->h_dest, "\x52\x54\x00\x12\x34\x56", ETH_ALEN);
    __builtin_memcpy(eth->h_source, "\x52\x54\x00\xab\xcd\xef", ETH_ALEN);
    eth->h_proto = bpf_htons(ETH_P_IP);

    // تنظیم اطلاعات سرآیند IPv4 بیرونی
    ip->version = 4;
    ip->ihl = 5;
    ip->tos = 0;
    ip->tot_len = bpf_htons((__u16)(data_end - (void *)ip));
    ip->id = 0;
    ip->frag_off = bpf_htons(IP_DF);
    ip->ttl = 64;
    ip->protocol = IPPROTO_UDP;
    ip->saddr = bpf_htonl(0x0a000001); // 10.0.0.1
    ip->daddr = bpf_htonl(0x0a000002); // 10.0.0.2
    ip->check = 0;

    // محاسبه چک‌سام سرآیند IPv4
    __u32 csum = 0;
    __u16 *ip_words = (__u16 *)ip;
    #pragma unroll
    for (int i = 0; i < sizeof(struct iphdr) / 2; i++) {
        csum += ip_words[i];
    }
    ip->check = csum_fold_helper(csum);

    // تنظیم اطلاعات سرآیند UDP بیرونی
    udp->source = bpf_htons(45000);
    udp->dest = bpf_htons(TUNNEL_PORT);
    udp->len = bpf_htons((__u16)(data_end - (void *)udp));
    udp->check = 0;

    // ارسال فوری بسته کپسوله‌شده به کابل شبکه
    return XDP_TX;
}

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

نتایج ارزیابی و آزمایش عملکرد سخت‌افزاری

در آزمایش‌های مقایسه‌ای انجام‌شده روی کارت‌های شبکه ۲۵ گیگابیت Intel E810 با بسته‌های ۶۴ بایتی، نتایج زیر حاصل شد:

معماری پیاده‌سازی نرخ هدایت بسته‌ها (Mpps) میانگین تاخیر ($\mu\text{s}$) بار پردازشی یک هسته (%)
رابط استاندارد TUN + برنامه فضای کاربری ۱.۸ Mpps ۱۴.۲ $\mu\text{s}$ ۱۰۰ % (گلوگاه پردازشی)
ماژول استاندارد هسته (VXLAN/UDP) ۳.۲ Mpps ۸.۶ $\mu\text{s}$ ۱۰۰ % (گلوگاه پردازشی)
مسیر سریع eBPF XDP (bpf_xdp_adjust_head) ۱۴.۶ Mpps ۰.۸ $\mu\text{s}$ ۴۲ % (سرعت فیزیکی خط)

حذف مراحل تخصیص شیء sk_buff و کار مستقیم روی حافظه بافر درایور، توان عملیاتی را بین ۴.۵ تا ۸ برابر افزایش داده و تاخیر انتقال بسته را تا بیش از ۹۰٪ کاهش می‌دهد.

راهنمای کامپایل و اتصال برنامه در لینوکس

مهندسان شبکه می‌توانند برنامه‌های XDP را با کامپایلر clang و بسته ابزار iproute2 مدیریت نمایند:

# ۱. کامپایل کد منبع به بایت‌کد BPF
clang -O2 -target bpf -c xdp_encap.c -o xdp_encap.o

# ۲. اتصال برنامه به قلاب بومی درایور در رابط eth0
ip link set dev eth0 xdpgeneric off
ip link set dev eth0 xdp obj xdp_encap.o sec xdp

# ۳. بررسی وضعیت پیوند XDP در کارت شبکه
ip link show dev eth0 | grep -i "xdp"

# ۴. پایش آمار بسته‌ها به کمک bpftool
bpftool prog dump xlated name xdp_vpn_encapsulate

برای جدا کردن برنامه:

ip link set dev eth0 xdp off

پیاده‌سازی سازمانی در فناوری RiceGuard اختصاصی VPNRice

شبکه توزیع‌شده بین‌المللی VPNRice بر بستر زیرساخت Anycast از توانمندی‌های شتاب‌یافته XDP بهره می‌برد:

  1. اعتبارسنجی در سطح درایور: بسته‌های رمزنگاری‌شده هنگام ورود به نودهای Anycast، بلافاصله در حافظه درایور کارت شبکه از نظر کلیدهای امنیتی بررسی می‌شوند و بسته‌های غیرمجاز پیش از مصرف حافظه سیستم‌عامل مسدود می‌گردند.
  2. کپسوله‌سازی سریع بین مراکز داده: در لینک‌های ارتباطی میان مسیریاب‌های اصلی، موتور RiceGuard با کمک bpf_xdp_adjust_head ترافیک را در سرعت خطی کپسوله می‌کند و از بروز تاخیر ناشی از تجمع بسته‌ها جلوگیری می‌نماید.
  3. سازگاری خودکار: در صورت فقدان پشتیبانی مستقیم درایور در برخی ماشین‌های ابری، سامانه به طور خودکار به حالت عمومی (xdpgeneric) سوئیچ می‌کند تا امنیت شبکه به شکل یکپارچه حفظ گردد.

یادگیری و بهره‌گیری از bpf_xdp_adjust_head یکی از بالاترین مهارت‌های مهندسی شبکه در لینوکس است که دسترسی به نهایت ظرفیت سخت‌افزارهای پردازش داده را ممکن می‌سازد.