تفرض شبكات المؤسسات الحديثة ومسارات العبور السحابية على بوابات VPN المركزية معالجة عشرات الملايين من الحزم في الثانية الواحدة (Mpps) عبر واجهات شبكية بسرعات 40 و100 غيغابت في الثانية. وفي ظل هذه الكثافة العالية، يصطدم مكدس الشبكة التقليدي في أنظمة التشغيل بعائق هيكلي يتمثل في استهلاك دورات المعالج المركزية على مستوى كل حزمة مستقلة.

ففي كل مرة يقوم فيها جهاز نفق افتراضي قياسي في لينكس (مثل TUN/TAP أو IP-in-IP أو GRE أو VXLAN) بتغليف الحزم أو فك تغليفها، تضطر النواة إلى تخصيص بنية بيانات struct sk_buff بحجم 240 بايت، وإجراء نسخ أو توسيع للذاكرة عبر الدالة pskb_expand_head()، ثم تمرير الحزمة عبر طبقات الطوابير وجداول Netfilter المتعددة. وحتى مع استخدام أفضل تقنيات التسريع العتادي، تتسبب هذه الآلية التقليدية في استنزاف كامل طاقة نواة المعالج عند معدل يتراوح بين 1.5 و2.5 مليون حزمة في الثانية.

يأتي دمج تقنية eXpress Data Path (XDP) مع الدالة المساعدة bpf_xdp_adjust_head() ليغير هذا الواقع جذرياً. فمن خلال العمل مباشرة داخل حلقة استطلاع المقاطعات لبرنامج تشغيل بطاقة الشبكة (NAPI) قبل تخصيص أي مساحات تخزين مؤقت للمقابس، تتيح تقنية XDP لمهندسي الشبكات تنفيذ عمليات تغليف وفك الحزم بنمط انعدام النسخ (Zero-Copy) وبأقصى سرعة تسمح بها الأسلاك الفيزيائية، محققة أكثر من 14 مليون حزمة في الثانية على نواة معالج مفردة.

مقارنة بين مسار الأنفاق التقليدي في النواة والمسار السريع XDP داخل برنامج التشغيل:

المسار التقليدي للأنفاق في النواة (استهلاك مرتفع لطاقة المعالج):
السلك الفيزيائي ──> نقل DMA للبطاقة ──> تخصيص sk_buff (240 بايت) ──> Netfilter PREROUTING
                                                                            │
                                                                            ▼
[خدمة النفق / الجهاز الافتراضي] ◄── pskb_expand_head() (إعادة تخصيص) ◄── جدول التوجيه
           │
           ▼ (تغليف بترويسات IP وUDP الخارجية)
إعادة الحقن في المكدس ──> Netfilter POSTROUTING ──> طابور qdisc ──> حلقة إرسال البطاقة
سقف الإنتاجية: نحو 2.0 مليون حزمة في الثانية لكل نواة معالج

المسار السريع الفائق XDP بنمط zero-copy داخل برنامج التشغيل:
السلك الفيزيائي ──> نقل DMA للبطاقة ──> حلقة NAPI RX ──> تنفيذ كود XDP البرمجي
                                                                │
                                                                ▼
                                                   bpf_xdp_adjust_head(ctx, -delta)
                                                   (توسيع المساحة مباشرة في حلقة DMA!)
                                                                │
                                                                ▼
                                                   كتابة ترويسات التغليف الخارجية IP وUDP
                                                                │
                                                                ▼
                                                   الإجراء: XDP_TX / bpf_redirect_map()
                                                   (إرسال فوري دون الحاجة لتخصيص sk_buff!)
الإنتاجية الفعلية: نحو 14.5 مليون حزمة في الثانية لكل نواة (السرعة القصوى للعتاد!)

أوجه القصور الهيكلية في أنفاق النواة التقليدية

لفهم الفارق الشاسع في الأداء الذي تقدمه تقنية XDP، يتعين فحص الخطوات التي ينفذها مكدس لينكس التقليدي أثناء عملية تغليف الحزم:

  1. أعباء تخصيص كائنات sk_buff: عند استلام كل إطار شبكي خام، يجب على برنامج التشغيل تهيئة بنية struct sk_buff. وتتطلب هذه العملية تصفير الذاكرة، وضبط عدادات الإسناد الذرية، وتهيئة الحقول التعريفية للطوابع الزمنية والواجهات.
  2. نفاد المساحة الاحتياطية وإعادة حجز الذاكرة (Headroom Starvation): تحتاج برامج تشغيل الأنفاق الافتراضية إلى إضافة ترويسات خارجية (مثل Ethernet وIPv4/IPv6 وUDP). وإذا كانت المساحة المتروكة في بداية الذاكرة المخصصة غير كافية، تضطر النواة لاستدعاء pskb_expand_head()، مما يفرض إعادة تخصيص الذاكرة ونسخ الحزمة بالكامل، مسبباً تشتتاً حاداً لذاكرة التخزين المؤقت للمعالج (Cache Thrashing).
  3. التنقل المتكرر بين الأنظمة الفرعية: تعبر الحزمة مسار استقبال IP، ونظام تتبع الاتصالات (nf_conntrack)، وجهاز النفق الافتراضي، ثم يعاد إرسالها كحزمة جديدة لتجتاز كافة مرشحات الخروج مجدداً.

وفي ظروف التدفق الكثيف، تستهلك أنوية المعالج ما يصل إلى 70% من دوراتها التشغيلية في إدارة الذاكرة والأقفال بدلاً من معالجة حركة البيانات الفعلية للعملاء.

البنية الداخلية لدالة bpf_xdp_adjust_head في برنامج التشغيل

تعمل تقنية XDP عند أدنى نقطة برمجية ممكنة في نظام التشغيل. وفي هذه المرحلة، تكون الحزمة عبارة عن كتلة ذاكرة فيزيائية متصلة تصفها البنية struct xdp_buff:

struct xdp_buff {
    void *data;             /* مؤشر بداية بيانات الحزمة */
    void *data_end;         /* مؤشر نهاية بيانات الحزمة */
    void *data_meta;        /* مخزن البيانات الوصفية قبل data */
    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) — التغليف (Encapsulation): يؤدي طرح البايتات إلى إزاحة المؤشر ctx->data إلى الخلف باتجاه data_hard_start، مما يوسع الحزمة داخل المساحة الاحتياطية المحجوزة مسبقاً. يوفر ذلك مساحة شاغرة ليكتب برنامج eBPF الترويسات الخارجية مباشرة.
  • القيمة الموجبة (delta > 0) — فك التغليف (Decapsulation): يؤدي جمع البايتات إلى تقديم المؤشر ctx->data إلى الأمام، مما يؤدي فوراً إلى اقتطاع الترويسات الخارجية (مثل Ethernet وIP وUDP) وكشف الحمولة الداخلية للنواة أو توجيهها دون إبطاء.
شكل الذاكرة في xdp_buff أثناء توسيع الترويسات:

قبل استدعاء bpf_xdp_adjust_head (الحزمة المستلمة كما هي):
[ data_hard_start ] ──[ مساحة احتياطية محجوزة (256B) ]──> [ data ]───────[ data_end ]
                                                           │              │
                                                           └─ IP الداخلي ─┘

بعد استدعاء bpf_xdp_adjust_head(ctx, -28):
[ data_hard_start ] ──[ المساحة المتبقية (228B) ]──> [ data ]────────────[ data_end ]
                                                      │                   │
                                                      ├─ ترويسة IP خارجي (20B)
                                                      ├─ ترويسة UDP خارجي (8B)
                                                      └─ بيانات IP الداخلية ──┘

شروط أمان مدقق BPF: إبطال صلاحية المؤشرات

تعتمد كتابة برامج التغليف بتقنية XDP على مراعاة القواعد الصارمة لمدقق الأمان في نواة لينكس (BPF Verifier).

فعند تنفيذ دالة bpf_xdp_adjust_head() بنجاح، يتغير العنوان الفيزيائي لذاكرة ctx->data بالكامل. وتبعاً لذلك، يبطل المدقق فوراً صلاحية كافة المؤشرات وإثباتات فحص الحدود التي تمت سابقاً.

وإذا حاول المطور الوصول إلى الذاكرة باستخدام المؤشرات القديمة بعد استدعاء الدالة، سيرفض المدقق تحميل البرنامج مع عرض الخطأ R1 invalid mem access. لذا يتعين على المبرمج إعادة قراءة مؤشري ctx->data وctx->data_end وإعادة التحقق من الحدود قبل الكتابة:

// 1. توسيع المساحة في بداية الحزمة
if (bpf_xdp_adjust_head(ctx, 0 - (int)encap_size)) {
    return XDP_DROP;
}

// 2. إعادة قراءة المؤشرات فوراً وبشكل إلزامي
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;

// 3. إعادة فحص الحدود قبل أي وصول للذاكرة
if (data + sizeof(struct ethhdr) + sizeof(struct iphdr) > data_end) {
    return XDP_DROP;
}

التطبيق البرمجي الكامل: تغليف نفق UDP بسرعة المعالجة العتادية

فيما يلي برنامج eBPF بلغة C متكامل لتنفيذ التغليف بنمط انعدام النسخ لحزم IPv4 الواردة باستخدام bpf_xdp_adjust_head():

#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;

    // فحص سلامة ترويسة Ethernet الأصلية
    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;
    }

    // توسيع المساحة بمقدار حجم الترويسات الإضافية ENCAP_SIZE
    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;
    }

    // كتابة ترويسة Ethernet الخارجية
    __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 بسرعة 25 غيغابت في الثانية مع حزم بحجم 64 بايت، أظهرت المعالجة عبر XDP فارقاً حاسماً:

معمارية التنفيذ معدل التمرير (Mpps) متوسط زمن التأخير ($\mu\text{s}$) حمل المعالج لنواة واحدة (%)
جهاز TUN القياسي + تطبيق في مساحة المستخدم 1.8 Mpps 14.2 $\mu\text{s}$ 100 % (عنق زجاجة)
وحدة النواة الافتراضية (VXLAN/UDP Kernel) 3.2 Mpps 8.6 $\mu\text{s}$ 100 % (عنق زجاجة)
المسار السريع eBPF XDP (bpf_xdp_adjust_head) 14.6 Mpps 0.8 $\mu\text{s}$ 42 % (السرعة القصوى للعتاد)

إن الاستغناء التام عن إنشاء كائنات struct sk_buff والتعديل المباشر على مخازن DMA يرفع معدل الإنتاجية بنحو 4.5 إلى 8 أضعاف مع خفض زمن تأخير تسلسل الحزم بأكثر من 90%.

خطوات التجميع والربط التشغيلي

يمكن للمهندسين تجميع برامج XDP وربطها بالواجهات الشبكية باستخدام clang وأدوات حزمة iproute2:

# 1. تجميع الكود المصدري إلى بايت كود BPF
clang -O2 -target bpf -c xdp_encap.c -o xdp_encap.o

# 2. ربط البرنامج بخطاف برنامج التشغيل الأصيل على الواجهة eth0
ip link set dev eth0 xdpgeneric off
ip link set dev eth0 xdp obj xdp_encap.o sec xdp

# 3. التأكد من حالة ارتباط XDP على الواجهة
ip link show dev eth0 | grep -i "xdp"

# 4. متابعة إحصائيات المعالجة عبر bpftool
bpftool prog dump xlated name xdp_vpn_encapsulate

ولإلغاء الربط بشكل آمن:

ip link set dev eth0 xdp off

التطبيق العملي في تقنية RiceGuard من VPNRice

تعتمد البنية التحتية العالمية لشبكة Anycast التابعة لـ VPNRice على تسريع eBPF XDP كركيزة أساسية:

  1. التحقق المسبق في برنامج التشغيل: مع تدفق الحزم المشفرة إلى عقد Anycast، تفحص برامج XDP التوقيعات الأمنية داخل ذاكرة برنامج التشغيل مباشرة، مما يحيد هجمات الإغراق والحزم غير المصرح بها قبل استهلاك موارد نظام التشغيل.
  2. التغليف السريع بين مراكز البيانات: لربط الموجهات الرئيسية عبر القارات، يستخدم محرك RiceGuard الدالة bpf_xdp_adjust_head لتغليف حركة البيانات بسرعة الأسلاك، مانعاً تضخم الطوابير وتنافس المعالجات على الأقفال.
  3. التكيف المرن مع البيئات السحابية: في الحالات التي لا تدعم فيها برامج تشغيل الأجهزة الافتراضية نمط XDP الأصيل، ينتقل RiceGuard تلقائياً إلى النمط العام (xdpgeneric)، مما يحافظ على سياسات الأمان الموحدة.

يمثل إتقان استخدام bpf_xdp_adjust_head معياراً هندسياً رائداً في أنظمة لينكس لإطلاق العنان لكامل الطاقة الإنتاجية للعتاد الشبكي المتطور.