تصحيح أخطاء الشبكة باستخدام eBPF (RHEL 8 Beta)

كل ذلك مع العطلات الماضية!

قررنا تخصيص مقالنا الأول بعد الإجازات لنظام Linux ، أي لدورة مسؤول Linux الرائعة ، والتي لدينا في مجموعة من أكثر الدورات ديناميكية ، أي مع المواد والممارسات الأكثر صلة. حسنًا ، ووفقًا لذلك ، نقدم مقالات مثيرة للاهتمام ودرسًا مفتوحًا .

أرسلت بواسطة ماتيو كروس
العنوان الأصلي: تصحيح الشبكة باستخدام eBPF (RHEL 8 Beta)

مقدمة

الشبكات هي تجربة مثيرة ، ولكن لا يتم تجنب المشاكل دائمًا. يمكن أن تكون عملية استكشاف الأخطاء وإصلاحها صعبة ، كما تحاول إعادة إنتاج السلوك الخاطئ الذي يحدث "في الحقل".

لحسن الحظ ، هناك أدوات يمكن أن تساعد في ذلك: مساحات أسماء الشبكات ، والأجهزة الظاهرية ، tc و netfilter . يمكن إعادة إنتاج إعدادات الشبكة البسيطة باستخدام مساحات أسماء الشبكات وأجهزة veth ، بينما تتطلب الإعدادات الأكثر تعقيدًا توصيل الأجهزة الظاهرية بجسر برامج واستخدام أدوات الشبكة القياسية ، مثل iptables أو tc ، لمحاكاة السلوك غير الصحيح. إذا كانت هناك مشكلة في استجابات ICMP التي تم إنشاؤها عند iptables -A INPUT -p tcp --dport 22 -j REJECT --reject-with icmp-host-unreachable خادم SSH ، فيمكن أن تساعد iptables -A INPUT -p tcp --dport 22 -j REJECT --reject-with icmp-host-unreachable في مساحة الاسم الصحيحة المساعدة في حل المشكلة.

توضح هذه المقالة كيفية استكشاف مشكلات الشبكة المعقدة باستخدام eBPF (امتداد BPF) ، وهو إصدار متقدم من Berkeley Packet Filter. يعد eBPF تقنية جديدة نسبيًا ، والمشروع في مرحلة مبكرة ، وبالتالي فإن الوثائق و SDK ليست جاهزة بعد. لكن دعونا نأمل في إجراء تحسينات ، خاصة وأن XDP (مسار بيانات eXpress) يأتي مع Red Hat Enterprise Linux 8 Beta ، والذي يمكنك تنزيله وتشغيله الآن.

لن يحل eBPF جميع المشكلات ، لكنه لا يزال أداة تصحيح أخطاء شبكة قوية تستحق الاهتمام. أنا متأكد من أنها ستلعب دورًا مهمًا في مستقبل الشبكات.



المشكلة

قمت بتصحيح مشكلة شبكة Open vSwitch (OVS) ، والتي تضمنت تثبيتًا معقدًا للغاية: تم تناثر بعض حزم TCP وتسليمها بالترتيب الخاطئ ، وانخفض عرض النطاق الترددي للأجهزة الافتراضية من 6 جيجابت / ثانية مستقرة إلى 2-4 جيجابت / ثانية. أظهر التحليل أنه تم إرسال أول حزمة TCP لكل اتصال بعلامة PSH بالترتيب الخاطئ: فقط الأولى والوحيدة لكل اتصال.

حاولت إعادة إنتاج هذا الإعداد باستخدام جهازين nftables ، وبعد العديد من مقالات المساعدة واستعلامات البحث ، وجدت أنه لا يمكن لأي من iptables أو nftables معالجة علامات TCP ، بينما يمكن tc ، ولكن فقط عن طريق الكتابة فوق الإشارات ومقاطعة الاتصالات الجديدة و TCP بشكل عام.

قد يكون من الممكن حل المشكلة باستخدام مزيج من iptables و conntrack و tc ، لكنني قررت أن هذه مهمة رائعة لـ eBPF.

ما هو eBPF؟

eBPF هو نسخة محسنة من مرشح حزم بيركلي. إنها تجلب الكثير من التحسينات على BPF. على وجه الخصوص ، يتيح لك الكتابة في الذاكرة ، وليس فقط القراءة ، لذلك لا يمكن تصفية الحزم فقط ، ولكن أيضًا تحريرها.

غالبًا ما تسمى eBPF ببساطة BPF ، وتسمى BPF نفسها cBPF (الكلاسيكية (الكلاسيكية) BPF) ، لذلك يمكن استخدام كلمة "BPF" لتعني كلا الإصدارين ، اعتمادًا على السياق: في هذه المقالة أتحدث دائمًا عن الإصدار الموسّع.

"تحت الغطاء" ، يحتوي eBPF على جهاز افتراضي بسيط للغاية يمكنه تنفيذ أجزاء صغيرة من الرمز الفرعي وتحرير بعض المخازن المؤقتة للذاكرة. توجد قيود في eBPF تحميها من الاستخدام الضار:

  • الدورات محظورة بحيث ينتهي البرنامج دائمًا في وقت محدد ؛
  • يمكن فقط الوصول إلى الذاكرة من خلال المخزن المؤقت المكدس والخدش؛
  • يمكن استدعاء وظائف kernel المسموح بها فقط.

يمكن تحميل برنامج في النواة بطرق مختلفة باستخدام تصحيح الأخطاء والتتبع . في حالتنا ، يهتم eBPF بالعمل مع الأنظمة الفرعية للشبكة. هناك طريقتان لاستخدام برنامج eBPF:

  • متصل عبر XDP ببداية مسار RX لبطاقة شبكة فعلية أو افتراضية ؛
  • متصلة عبر tc إلى qdisc في الإدخال أو الإخراج.

لإنشاء برنامج eBPF للاتصال ، ما عليك سوى كتابة رمز C وتحويله إلى رمز ثانوي. التالي مثال بسيط باستخدام XDP:

 SEC("prog") int xdp_main(struct xdp_md *ctx) { void *data_end = (void *)(uintptr_t)ctx->data_end; void *data = (void *)(uintptr_t)ctx->data; struct ethhdr *eth = data; struct iphdr *iph = (struct iphdr *)(eth + 1); struct icmphdr *icmph = (struct icmphdr *)(iph + 1); /* sanity check needed by the eBPF verifier */ if (icmph + 1 > data_end) return XDP_PASS; /* matched a pong packet */ if (eth->h_proto != ntohs(ETH_P_IP) || iph->protocol != IPPROTO_ICMP || icmph->type != ICMP_ECHOREPLY) return XDP_PASS; if (iph->ttl) { /* save the old TTL to recalculate the checksum */ uint16_t *ttlproto = (uint16_t *)&iph->ttl; uint16_t old_ttlproto = *ttlproto; /* set the TTL to a pseudorandom number 1 < x < TTL */ iph->ttl = bpf_get_prandom_u32() % iph->ttl + 1; /* recalculate the checksum; otherwise, the IP stack will drop it */ csum_replace2(&iph->check, old_ttlproto, *ttlproto); } return XDP_PASS; } char _license[] SEC("license") = "GPL"; 

المقتطف أعلاه ، دون include التعبيرات والمساعدين والرمز الاختياري ، هو برنامج XDP يغير TTL لردود صدى ICMP المستلمة ، أي البونغ ، برقم عشوائي. الوظيفة الرئيسية تحصل على بنية xdp_md ، والتي تحتوي على مؤشرين إلى بداية ونهاية الحزمة.

لتجميع الكود الخاص بنا إلى eBPF bytecode ، يلزم وجود مترجم مع الدعم المناسب. Clang يدعمها ويقوم بإنشاء eBPF bytecode عن طريق تحديد bpf كهدف في وقت الترجمة:

 $ clang -O2 -target bpf -c xdp_manglepong.c -o xdp_manglepong.o 

ينشئ الأمر أعلاه ملفًا يبدو للوهلة الأولى أنه ملف كائن عادي ، ولكن بعد الفحص الدقيق ، اتضح أن نوع الكمبيوتر المحدد هو Linux eBPF ، وليس النوع الأصلي لنظام التشغيل:

 $ readelf -h xdp_manglepong.o ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: REL (Relocatable file) Machine: Linux BPF <--- HERE [...] 

بعد تلقي حزمة ملف كائن عادي ، يكون برنامج eBPF جاهزًا لتنزيل الجهاز والاتصال به عبر XDP. يمكن القيام بذلك باستخدام ip من الحزمة iproute2 مع بناء الجملة التالي:

 # ip -force link set dev wlan0 xdp object xdp_manglepong.o verbose 

يحدد هذا الأمر واجهة wlan0 الهدف ، وبفضل خيار القوة ، يقوم بالكتابة فوق أي كود eBPF موجود بالفعل تم تحميله بالفعل. بعد تحميل كود eBPF الثانوي ، يتصرف النظام على النحو التالي:

 $ ping -c10 192.168.85.1 PING 192.168.85.1 (192.168.85.1) 56(84) bytes of data. 64 bytes from 192.168.85.1: icmp_seq=1 ttl=41 time=0.929 ms 64 bytes from 192.168.85.1: icmp_seq=2 ttl=7 time=0.954 ms 64 bytes from 192.168.85.1: icmp_seq=3 ttl=17 time=0.944 ms 64 bytes from 192.168.85.1: icmp_seq=4 ttl=64 time=0.948 ms 64 bytes from 192.168.85.1: icmp_seq=5 ttl=9 time=0.803 ms 64 bytes from 192.168.85.1: icmp_seq=6 ttl=22 time=0.780 ms 64 bytes from 192.168.85.1: icmp_seq=7 ttl=32 time=0.847 ms 64 bytes from 192.168.85.1: icmp_seq=8 ttl=50 time=0.750 ms 64 bytes from 192.168.85.1: icmp_seq=9 ttl=24 time=0.744 ms 64 bytes from 192.168.85.1: icmp_seq=10 ttl=42 time=0.791 ms --- 192.168.85.1 ping statistics --- 10 packets transmitted, 10 received, 0% packet loss, time 125ms rtt min/avg/max/mdev = 0.744/0.849/0.954/0.082 ms 

تمر كل حزمة عبر eBPF ، مما يؤدي في النهاية إلى إجراء بعض التغييرات وتحديد ما إذا كان سيتم إسقاط الحزمة أو تخطيها.

كيف يمكن eBPF المساعدة

بالعودة إلى مشكلة الشبكة الأصلية ، نذكر أنه كان من الضروري وضع علامة على عدة علامات TCP ، واحدة لكل اتصال ، ولا يمكن لـ iptables أو tc القيام بذلك. كتابة التعليمات البرمجية لهذا السيناريو ليست صعبة على الإطلاق: تكوين جهازين ظاهريين متصلين بجسر OVS ، وقم ببساطة بتوصيل eBPF بأحد أجهزة VM الافتراضية.

يبدو هذا حلاً رائعًا ، لكن ضع في اعتبارك أن XDP يدعم معالجة الحزم المستلمة فقط ، ولن يكون لتوصيل eBPF بمسار rx الخاص بالجهاز الظاهري المستقبِل أي تأثير على المحول.

لحل هذه المشكلة ، يجب تحميل eBPF باستخدام tc وتوصيله بمسار إخراج VM ، لأن tc يمكنه تحميل وتوصيل برامج eBPF بـ qdisk. لتعليم الحزم الخارجة من المضيف ، يجب أن يكون eBPF متصلاً بـ qdisk الإخراج.

عند تحميل برنامج eBPF ، هناك بعض الاختلافات بين XDP و tc API: بشكل افتراضي ، أسماء أقسام مختلفة ، ونوع بنية وسيطة الوظيفة الرئيسية ، وقيم الإرجاع المختلفة. لكن هذه ليست مشكلة. يوجد أدناه مقتطف من برنامج يقوم بتمييز TCP عند الانضمام إلى إجراء tc:

 #define RATIO 10 SEC("action") int bpf_main(struct __sk_buff *skb) { void *data = (void *)(uintptr_t)skb->data; void *data_end = (void *)(uintptr_t)skb->data_end; struct ethhdr *eth = data; struct iphdr *iph = (struct iphdr *)(eth + 1); struct tcphdr *tcphdr = (struct tcphdr *)(iph + 1); /* sanity check needed by the eBPF verifier */ if ((void *)(tcphdr + 1) > data_end) return TC_ACT_OK; /* skip non-TCP packets */ if (eth->h_proto != __constant_htons(ETH_P_IP) || iph->protocol != IPPROTO_TCP) return TC_ACT_OK; /* incompatible flags, or PSH already set */ if (tcphdr->syn || tcphdr->fin || tcphdr->rst || tcphdr->psh) return TC_ACT_OK; if (bpf_get_prandom_u32() % RATIO == 0) tcphdr->psh = 1; return TC_ACT_OK; } char _license[] SEC("license") = "GPL"; 

يتم التجميع في bytecode كما هو موضح في مثال XDP أعلاه باستخدام ما يلي:

 clang -O2 -target bpf -c tcp_psh.c -o tcp_psh.o 

لكن التنزيل مختلف:

 # tc qdisc add dev eth0 clsact # tc filter add dev eth0 egress matchall action bpf object-file tcp_psh.o 

الآن يتم تحميل eBPF في المكان الصحيح ويتم وضع علامة على الحزم التي تغادر جهاز VM. بعد التحقق من الحزم المستلمة في جهاز VM الثاني ، سنرى ما يلي:



يؤكد tcpdump أن كود eBPF الجديد يعمل ، وحوالي 1 من كل 10 حزم TCP بها مجموعة إشارة PSH. كانت هناك حاجة إلى 20 سطرًا فقط من التعليمات البرمجية لـ C لتمييز حزم TCP بشكل انتقائي وترك الجهاز الظاهري ، وإعادة إنتاج الخطأ الذي يحدث "في المعركة" ، وكل ذلك دون إعادة ترجمة أو حتى إعادة التشغيل! أدى ذلك إلى تبسيط عملية التحقق من إصلاح برنامج Open vSwitch ، والذي كان من المستحيل تحقيقه باستخدام أدوات أخرى.

الخاتمة

eBPF هي تقنية جديدة إلى حد ما ، وللمجتمع رأي واضح حول تنفيذها. تجدر الإشارة أيضًا إلى أن المشروعات القائمة على eBPF ، مثل bpfilter ، أصبحت أكثر شيوعًا ، ونتيجة لذلك ، بدأ العديد من موردي المعدات في تنفيذ دعم eBPF مباشرةً على بطاقات الشبكة.

لن يحل eBPF جميع المشاكل ، لذلك لا تسيء استخدامها ، ولكن لا يزال أداة قوية للغاية لتصحيح أخطاء الشبكة وتستحق الاهتمام. أنا متأكد من أنها ستلعب دورًا مهمًا في مستقبل الشبكات.

النهاية

نحن في انتظار تعليقاتكم هنا ، ونحن ندعوك لزيارة درسنا المفتوح ، حيث يمكنك أيضًا طرح الأسئلة.

Source: https://habr.com/ru/post/ar436528/


All Articles