يتطلب تحقيق معدلات نقل بمليارات البتات في الثانية (multi-gigabit) داخل بوابات شبكات VPN العاملة في مساحة المستخدم إعادة النظر في كيفية انتقال حزم البيانات بين بطاقة الشبكة الفعلية (NIC) وبرمجيات المعالجة. تقليدياً، تعتمد برمجيات الأنفاق المطورة بلغات مثل Go أو Rust أو C على واجهات TUN/TAP الافتراضية (/dev/net/tun) للتفاعل مع نظام التشغيل. على الرغم من التوافقية الواسعة لمحرك TUN، إلا أنه يفرض أعباء تشغيلية ثقيلة؛ فكل حزمة تتطلب تبديل سياق المعالجة بين النواة ومساحة المستخدم، وبناء هياكل struct sk_buff داخل النواة، ونسخ البيانات عبر استدعاء copy_to_user().

عند تدفق البيانات بمعدلات 10 أو 40 جيجابت في الثانية، تستنزف هذه العمليات مسارات الذاكرة وتتسبب في إفراغ الذاكرة المخبأة للمعالج مراراً، مما يحد من إنتاجية النواة الواحدة عند حوالي 1.5 مليون حزمة في الثانية (Mpps). ورغم أن حزم DPDK تتجاوز هذه المعضلة بالاستحواذ المباشر على العتاد، إلا أنها تفصل بطاقة الشبكة تماماً عن نظام لينكس، مما يعطل أدوات المراقبة المعتادة (iproute2 و nftables و tcpdump) والخدمات المجاورة على نفس الخادم.

يقدم نظام Linux AF_XDP (والمعروف أيضاً باسم XSK) حلاً معمارياً متكاملاً. من خلال دمج إعادة توجيه الحزم عبر eBPF على مستوى مشغل الشبكة مع مخازن الذاكرة المخصصة مسبقاً في مساحة المستخدم (UMEM)، يتيح AF_XDP نقلاً مباشراً للحزم دون نسخ عتادي مع الحفاظ الكامل على أدوات إدارة لينكس القياسية.

مقارنة مسارات البيانات: الفارق بين TUN/TAP و AF_XDP

لتوضيح الاختلاف في الأداء، نتتبع دورة حياة حزمة VPN مشفرة عبر المسارين:

[مسار البيانات التقليدي TUN/TAP]
حلقة استقبال بطاقة الشبكة ──> alloc_skb() ──> مكدس IP بالنواة / Netfilter ──> مشغل TUN
                                                                                   │
                         تبديل السياق + نسخ الذاكرة (copy_to_user) <───────────────┘
                                           │
                                           ▼
                               تطبيق VPN بمساحة المستخدم (فك التشفير)

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

[مسار البيانات دون نسخ AF_XDP]
حلقة استقبال بطاقة الشبكة ──> نقل DMA مباشر ──> كتلة ذاكرة UMEM المخصصة
                                     │
                     مرشح XDP: bpf_redirect_map()
                                     │
                                     ▼ (نقل دون نسخ ودون أقفال برمجية)
                     محرك VPN بمساحة المستخدم (فك التشفير المباشر بالذاكرة)

في بنية AF_XDP، تتفادى النواة إنشاء هياكل struct sk_buff. بدلاً من ذلك، تقوم وحدة التحكم في الوصول المباشر للذاكرة (DMA) بنقل حزم الإيثرنت مباشرة إلى منطقة ذاكرة متصلة مسجلة مسبقاً (UMEM). يقوم برنامج eBPF يعمل عند مشغل البطاقة (BPF_PROG_TYPE_XDP) بفحص ترويسة الحزمة فقط. إذا كانت الحزمة موجهة لخدمة VPN (مطابقة لمنفذ UDP أو توقيع الاتصال)، يوجهها البرنامج فوراً عبر bpf_redirect_map(&qidconf_map, queue_id, 0) إلى مقبس XSK. أما باقي حركة مرور الشبكة فتحصل على XDP_PASS لتمر بانسيابية عبر مكدس لينكس المعتاد.

بنية UMEM والحلقات الأربع الدائرية بدون أقفال

العنصر المحوري في منظومة AF_XDP هو UMEM؛ مساحة متصلة من الذاكرة الافتراضية تُسجل لدى النواة عبر استدعاء setsockopt(..., SOL_XDP, XDP_UMEM_REG, ...). يتم تقسيم هذه المساحة إلى أطر ثابتة الحجم (غالباً 2048 أو 4096 بايت).

يجري التبادل بين مساحة المستخدم ومشغل العتاد من خلال أربع حلقات دائرية أحادية المنتج وأحادية المستهلك (SPSC) تعمل بلا أقفال تنافسية:

  1. حلقة التعبئة (Fill Ring - إنتاج مساحة المستخدم -> استهلاك النواة): يضع التطبيق عناوين أطر UMEM الفارغة في هذه الحلقة لتكون البطاقة جاهزة لاستقبال البيانات.
  2. حلقة الاستقبال (Rx Ring - إنتاج النواة -> استهلاك مساحة المستخدم): عند وصول حزم جديدة، يودع المشغل واصفات الأطر (إزاحة العنوان وطول الحزمة) داخل الحلقة لمعالجتها.
  3. حلقة الإرسال (Tx Ring - إنتاج مساحة المستخدم -> استهلاك النواة): عند تشفير حزم البيانات الصادرة، يدرج التطبيق الواصفات في حلقة الإرسال لتنفيذ النقل عبر الشبكة.
  4. حلقة الإتمام (Completion Ring - إنتاج النواة -> استهلاك مساحة المستخدم): بعد إتمام إرسال الإطار مادياً عبر السلك، يسجل المشغل عنوان الإطار في حلقة الإتمام ليعاد استخدامه على الفور.

تعتمد هذه الحلقات على مؤشرات ذرية متزايدة، مما يسمح لأنوية المعالج بمعالجة الحزم ضمن دفعات من 32 أو 64 حزمة دفعة واحدة دون مواجهة تعارض أو قفل تزامني.

التنفيذ داخل النواة: إعادة التوجيه عبر eBPF في مشغل الشبكة

يتطلب المكون الخاص بالنواة مصنف eBPF مختصراً يرتبط بالواجهة المادية:

#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;
    
    /* التحقق من حدود الترويسة لضمان سلامة إطار الإيثرنت */
    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;
        
    /* توجيه حزم منفذ VPN المستهدف إلى مقبس 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";

ينفذ هذا البرنامج خلال نانوثوان معدودة في سياق مقاطعة مشغل العتاد. وتتدفق حزم VPN مباشرة إلى حلقات مساحة المستخدم، بينما تواصل جلسات SSH وإشارات بروتوكول BGP مسارها الطبيعي عبر مكدس النظام.

محرك مساحة المستخدم: تهيئة UMEM والاستطلاع دون نسخ

في مساحة التطبيق، تبدأ تهيئة عمال AF_XDP بحجز الذاكرة المتوافقة مع حجم الصفحات عبر posix_memalign() وضبط مقبس التوصيل برايات تسريع النقل:

#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
    };
    
    /* تخصيص مساحة ذاكرة متصلة ومتطابقة مع صفحات النظام */
    posix_memalign(&umem_info->buffer, getpagesize(), NUM_FRAMES * FRAME_SIZE);
    
    /* تسجيل مصفوفة UMEM في النواة */
    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;
}

باستخدام راية XDP_USE_NEED_WAKEUP، يتفادى التطبيق استدعاءات النظام الزائدة؛ حيث تتدخل النواة فقط عندما تشير مساحة المستخدم إلى حاجة حقيقية لتغذية رتل العتاد بالبيانات.

التطبيق الميداني في بوابات شبكة RiceGuard Anycast

تعتمد منصة RiceGuard على قنوات AF_XDP في خوادم Anycast الموزعة لضمان استقرار الأداء تحت أعباء تدفق حركة المرور الكثيفة:

  • كثافة استيعاب الحزم: تحقق وحدات RiceGuard ثنائية المعالجات معدلات تتجاوز 24 مليون حزمة في الثانية (Mpps)، وهو ما يمثل قفزة نوعية مقارنة بواجهات TUN التقليدية.
  • استقرار زمن الاستجابة في حدود الميكروثانية: بفضل التخلص من تعارض الأقفال وعمليات نسخ الذاكرة، يستقر زمن تأخير معالجة الحزم عند مستوى أقل من 1.2 ميكروثانية.
  • تكامل تام مع بنية لينكس التحتية: بما أن البطاقة تظل واجهة شبكة أصلية داخل لينكس، فإن خدمات التوجيه الديناميكي Anycast BGP وأنظمة المراقبة تعمل دون أي تعديل إلى جانب مسار البيانات المسرع.

يجمع الدمج بين تقنية AF_XDP ومصفوفات UMEM دون نسخ بين تحقيق السرعة القصوى لنقل الحزم عبر العتاد والحفاظ الكامل على مرونة بيئة لينكس القياسية.