يتطلب توسيع نطاق بوابات VPN متعددة المشتركين لمعالجة تدفقات تصل إلى عشرات الجيجابت في الثانية توزيعاً عالي الكفاءة لملايين حزم UDP المشفرة عبر عشرات الأنوية في المعالج. في البنية الشبكية التقليدية، تتنافس خيوط المعالجة المرتبطة بمنفذ استماع موحد على استلام الحزم عبر طوابير مقابس مركزية، مما يؤدي إلى تنافس شديد على أقفال التدوير (spinlocks) واختناقات في تزامن المعالجة المتوازية.
ورغم أن خيار المقبس SO_REUSEPORT في نواة لينكس أتاح ربط عدة مقابس مستقلة بنفس عنوان IP ومنفذ الاستماع، إلا أن آلية التوزيع الافتراضية للنواة تعتمد على دالة تجزئة رباعية الحقول ثابتة (IP المصدر، منفذ المصدر، IP الوجهة، منفذ الوجهة). وفي بيئات الشبكات المشفرة عالية الإنتاجية، يسبب هذا النموذج الرياضي الصارم عيوباً هيكلية معقدة: فهو يعطل استمرارية الجلسة عند تنقل العملاء بين الشبكات، ويؤدي إلى وصول غير متوازن للذاكرة عبر عقد NUMA (الوصول غير المنتظم للذاكرة)، ويتسبب في تفريغ مستمر لذاكرة التخزين المؤقت L3 بين مقابس المعالجات.
وللتغلب على هذه المعوقات، يدمج مهندسو شبكات لينكس بين خيار SO_REUSEPORT ومصنفات مقابس eBPF البرمجية (BPF_PROG_TYPE_SK_REUSEPORT). وعبر تنفيذ توجيه الحزم البرمجي مباشرة أثناء مرحلة مطابقة المقابس في النواة، تحقق بوابات الشبكة استقراراً تاماً للجلسات وتوازناً في الأعباء بدون أقفال وتوافقاً دقيقاً مع الذاكرة المحلية لعقد NUMA.
عيوب التوسع في دالة التجزئة الافتراضية لخيار SO_REUSEPORT
في نواة لينكس القياسية، تُوزع حزم UDP الواردة المطابقة لمجموعة SO_REUSEPORT على المقابس المتاحة باستخدام خوارزمية reciprocal_scale(skb_get_hash(skb), num_sockets). ورغم كفاءة هذه الطريقة لحركة بيانات الويب غير المرتبطة بحالة، فإنها تسبب تراجعاً في كفاءة أنفاق VPN عبر ثلاثة مسارات:
حزمة UDP الواردة
│
▼
┌─────────────────────────────────┐
│ تجزئة النواة رباعية الحقول │
│ (src_ip, src_port, dst_ip, dst) │
└──────────────┬──────────────────┘
│
├─────────────────┬─────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ مقبس 0 │ │ مقبس 1 │ │ مقبس 2 │
│ نواة 0(NUMA0)│ │ نواة 4(NUMA0)│ │ نواة 8(NUMA1)│
└──────────────┘ └──────────────┘ └──────┬───────┘
│ <--- تأخير ناقل UPI بين عقد المعالجات
▼ (إلغاء خطوط ذاكرة L3)
ذاكرة L3 البعيدة
- تنقل العملاء وتغير منافذ ترجمة العناوين (NAT): يبدل مستخدمو الهواتف بانتظام بين شبكات Wi-Fi وشبكات الهاتف المحمول، أو تتغير منافذهم لدى مزودي الخدمة بسبب إعادة تدوير منافذ NAT. وعند تغير عنوان IP أو منفذ المصدر، تتبدل قيمة دالة التجزئة فوراً، مما يعيد توجيه الحزم لجلسة مشفرة نشطة إلى خيط معالجة آخر تماماً. لا يمتلك الخيط الجديد مفاتيح التشفير في ذاكرته المؤقتة، مما يفرض نقلاً مكلفاً لبيانات الجلسة بين الخيوط أو إعادة المصافحة.
- تأخير الذاكرة عبر عقد NUMA: في الخوادم متعددة المعالجات، تتصل بطاقات الشبكة PCIe مباشرة بمقبس معالج محدد (عقدة NUMA 0). فإذا قامت دالة التجزئة بتوجيه حزمة إلى خيط يعمل على عقدة NUMA 1، يتعين على كل معاملة ذاكرة عبور ناقل Ultra Path Interconnect (UPI)، مما يرفع زمن معالجة الحزمة من 800 نانوثانية إلى أكثر من 3.2 ميكروثانية.
- تفاوت أعباء العمل والتدفقات الضخمة: التوزيع بالتجزئة هو توزيع احتمالي. وعندما توجه الصدفة عدة جلسات ذات استهلاك عريض للنطاق الترددي إلى نفس النواة، تتعرض تلك النواة لضغط كامل وتفقد الحزم، في حين تظل الأنوية المجاورة في حالة خمول نسبي.
بنية التوجيه البرمجي: BPF_PROG_TYPE_SK_REUSEPORT
يعمل نوع البرنامج BPF_PROG_TYPE_SK_REUSEPORT جنباً إلى جنب مع مصفوفة التعيين BPF_MAP_TYPE_REUSEPORT_SOCKARRAY لاستبدال دالة التجزئة الثابتة للنواة بقواعد توجيه مخصصة بالكامل.
حزمة البيانات المشفرة الواردة
│
▼
┌────────────────────────────────────────────────────────┐
│ مصنف eBPF: نوع BPF_PROG_TYPE_SK_REUSEPORT │
│ - فحص حمولة UDP (معرف جلسة WireGuard المستلم) │
│ - قراءة smp_processor_id() لتوجيه النواة المحلية │
│ - استدعاء الدالة المساعدة bpf_sk_select_reuseport() │
└──────────────────────────┬─────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ BPF_MAP_TYPE_REUSEPORT_SOCKARRAY │
└────────────────┬─────────────────┘
│
┌───────────────────┴───────────────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ نواة العمل 0 │ (عقدة NUMA 0 المحلية) │ نواة العمل 1 │ (عقدة NUMA 0 المحلية)
│ واصف مقبس 0 │ │ واصف مقبس 1 │
└──────────────┘ └──────────────┘
عندما تصل الحزمة إلى مرحلة استعلام مقبس UDP، تستدعي النواة برنامج eBPF المرفق قبل إرسال الحزمة إلى أي طابور. يتاح للبرنامج سياق المعالجة struct sk_reuseport_md:
skb->dataوskb->data_end: مؤشرات مباشرة لبيانات ترويسة الحزمة وحمولتها.sk: المقبس المرشح للاستلام.ip_protocol: بروتوكول طبقة النقل (UDP أو TCP).bind_inany: مؤشر حالة ارتباط المنفذ.
وعبر استدعاء الدالة المساعدة bpf_sk_select_reuseport(md, &reuseport_map, &key, flags)، يحدد كود eBPF فهرس المقبس المطلوب في مصفوفة الذاكرة بدقة متناهية.
التنفيذ داخل النواة: التوجيه عبر مؤشر المستلم في WireGuard
في بروتوكول WireGuard، تحتوي كل حزمة بيانات مشفرة على حقل receiver_index بحجم 32 بت عند الإزاحة 4 بايت من حمولة UDP. يمثل هذا الرقم المعرف الفريد لجلسة الطرف المقابل.
يوضح برنامج eBPF التالي استخراج هذا المعرف وتوجيه الحزمة إلى نواة المعالجة المرتبطة بعقدة NUMA المحلية:
#include <linux/bpf.h>
#include <linux/filter.h>
#include <bpf/bpf_helpers.h>
#define WG_DATA_PACKET_TYPE 4
#define NUMA_NODE_CORES 16
struct {
__uint(type, BPF_MAP_TYPE_REUSEPORT_SOCKARRAY);
__uint(max_entries, 64);
__type(key, __u32);
__type(value, __u32);
} reuseport_sock_map SEC(".maps");
SEC("sk_reuseport")
int select_vpn_socket(struct sk_reuseport_md *ctx)
{
void *data = ctx->data;
void *data_end = ctx->data_end;
/* فحص حدود الذاكرة للحمولة الأساسية لحزمة UDP */
if (data + 8 > data_end)
return SK_PASS;
__u8 packet_type = *(__u8 *)data;
__u32 target_socket_index = 0;
if (packet_type == WG_DATA_PACKET_TYPE) {
/* استخراج معرف الجلسة receiver_index عند الإزاحة 4 بايت */
__u32 receiver_index = *(__u32 *)(data + 4);
/* ضمان التوجيه المستقر لنفس نواة المعالجة */
target_socket_index = receiver_index % NUMA_NODE_CORES;
} else {
/* حزم المصافحة الأولية توجه لنواة المعالج الحالية */
__u32 current_cpu = bpf_get_smp_processor_id();
target_socket_index = current_cpu % NUMA_NODE_CORES;
}
/* تحديد المقبس دون الحاجة لأقفال برمجية */
int ret = bpf_sk_select_reuseport(ctx, &reuseport_sock_map, &target_socket_index, 0);
if (ret == 0)
return SK_PASS;
return SK_DROP;
}
char _license[] SEC("license") = "GPL";
بهذا النهج، تصل جميع حزم الجلسة الواحدة إلى نفس النواة بشكل مستمر بصرف النظر عن تغير عنوان العميل أو منافذه. في المقابل، تُعالج حزم المصافحة الأولية على النواة التي استقبلت المقاطعة العتادية من بطاقة الشبكة، مما يبقي البيانات في ذاكرة L1/L2 النشطة.
الضبط والربط من مساحة المستخدم
لتفعيل التوجيه البرمجي، ينشئ البرنامج المقابس ويسجل شفرة eBPF التنفيذية عبر واجهات برمجة المقابس القياسية:
int setup_reuseport_cluster(int epoll_fd, int bpf_prog_fd, int sock_map_fd)
{
int num_cores = 16;
for (int i = 0; i < num_cores; i++) {
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
int opt = 1;
setsockopt(sfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(51820),
.sin_addr.s_addr = htonl(INADDR_ANY)
};
bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
/* تخزين واصف المقبس في مصفوفة eBPF */
__u32 key = i;
bpf_map_update_elem(sock_map_fd, &key, &sfd, BPF_ANY);
/* ربط برنامج eBPF بالمقبس الأول من المجموعة */
if (i == 0) {
setsockopt(sfd, SOL_SOCKET, SO_ATTACH_REUSEPORT_EBPF,
&bpf_prog_fd, sizeof(bpf_prog_fd));
}
}
return 0;
}
بمجرد ربطه بالمقبس الأول، تطبق النواة منطق توجيه eBPF تلقائياً على سائر المقابس التابعة لمجموعة SO_REUSEPORT.
التطبيق الميداني في بنية شبكة RiceGuard
تعتمد بنية RiceGuard الموزعة عبر شبكة Anycast على تقنية BPF_PROG_TYPE_SK_REUSEPORT لرفع كفاءة المعالجة:
- إلغاء تأخير الذاكرة بين عقد NUMA: تدمج RiceGuard حلقات فك التشفير مع أنوية المعالجات المتصلة مباشرة ببطاقة الشبكة 100G. وتتم معالجة الحزم وفك تشفيرها وتوجيهها بالكامل داخل نطاق ذاكرة L3 المحلية، مما يخفض وقت التأخير بنسبة تتجاوز 60%.
- استقرار الاتصال أثناء التجوال: عند تبديل المستخدم بين أبراج شبكة الجوال، يتعرف مرشح eBPF على معرف الجلسة ويوجه الحزم فوراً للخيط النشط دون الحاجة لإعادة التفاوض التشفيري.
- تحديثات البوابات دون انقطاع: أثناء ترقية برمجيات الخادم، تُسجل المقابس الجديدة في جدول
REUSEPORT_SOCKARRAYبينما تُكمل المقابس السابقة تصريف جلساتها النشطة بهدوء، مما يمنع سقوط الحزم كلياً.
يوفر الربط بين SO_REUSEPORT والتوجيه البرمجي عبر eBPF قابلية توسع عالية وأماناً متقدماً في عزل حركة المشتركين داخل بوابات WireGuard و RiceGuard الحديثة.



