تتيح تقنية EXpress Data Path (XDP) المعالجة التعسفية لحركة المرور على واجهات Linux قبل وصول الحزم إلى حزمة شبكة kernel. تطبيق XDP - الحماية ضد هجمات DDoS (CloudFlare) والمرشحات المعقدة وجمع الإحصائيات (Netflix). يتم تنفيذ برامج XDP بواسطة الجهاز الظاهري eBPF ، وبالتالي ، توجد قيود على كل من التعليمات البرمجية وعلى وظائف kernel المتوفرة ، اعتمادًا على نوع التصفية.
الغرض من المقالة هو سد أوجه القصور في العديد من مواد XDP. أولاً ، يوفرون كودًا جاهزًا يتخطى على الفور ميزات XDP: تم إعداده للتحقق أو بسيط جدًا في التسبب في حدوث مشكلات. عند محاولة كتابة التعليمات البرمجية الخاصة بك من نقطة الصفر من نقطة الصفر ، لا يوجد فهم لما يجب فعله بالأخطاء النموذجية. ثانياً ، لا يتم تغطية طرق اختبار XDP محليًا بدون VMs والأجهزة ، على الرغم من أن لديهم مطباتهم الخاصة. النص مخصص للمبرمجين المطلعين على الشبكات ولينكس المهتمين بـ XDP و eBPF.
في هذا الجزء ، سنبحث بالتفصيل كيف يتم تجميع مرشح XDP وكيفية اختباره ، ثم سنكتب نسخة بسيطة من آلية ملفات تعريف الارتباط SYN المعروفة على مستوى معالجة الحزمة. في حين أننا لن نشكل "قائمة بيضاء"
التحقق من العملاء ، والحفاظ على العدادات وإدارة عامل التصفية - سجلات كافية.
سنكتب في C - هذا ليس من المألوف ، ولكن عملي. كل الشفرة متاحة على GitHub عبر الرابط في النهاية ، وتنقسم إلى إلتزامات وفقًا للمراحل الموضحة في المقالة.
تنويه. أثناء المقال ، سيتم تطوير حل صغير لصد هجمات DDoS ، لأن هذه مهمة واقعية لـ XDP ومنطقي. ومع ذلك ، فإن الهدف الرئيسي هو التعامل مع التكنولوجيا ، وهذا ليس دليلًا لإنشاء حماية جاهزة. لم يتم تحسين رمز التدريب وحذف بعض الفروق الدقيقة.
XDP في لمحة
سأحدد النقاط الرئيسية فقط حتى لا أكرر الوثائق والمقالات الموجودة.
لذلك ، يتم تحميل رمز التصفية في النواة. يتم إرسال الحزم الواردة إلى المرشح. نتيجةً لذلك ، يجب أن يتخذ المرشح قرارًا: تخطي الحزمة إلى النواة ( XDP_PASS
) ، وتجاهل الحزمة ( XDP_DROP
) أو إعادتها ( XDP_TX
). يمكن للمرشح تغيير الحزمة ، وهذا ينطبق بشكل خاص على XDP_TX
. يمكنك أيضًا تعطل البرنامج ( XDP_ABORTED
) وتجاهل الحزمة ، ولكن هذا تناظرية assert(0)
للتصحيح.
تم تصميم الجهاز الظاهري eBPF (مرشح Berkley Packet Filter) بشكل بسيط بحيث يمكن للنواة التحقق من أن الكود لا ينفصل ولا يضر بذاكرة شخص آخر. القيود والشيكات الإجمالية:
- دورات محظورة (القفز مرة أخرى).
- هناك كومة للبيانات ، ولكن لا توجد وظائف (يجب أن تكون جميع وظائف C مضمنة).
- يحظر الوصول إلى الذاكرة خارج المخزن المؤقت والحزم.
- حجم الرمز محدود ، لكن هذا ليس مهمًا في الممارسة العملية.
- لا يُسمح بالمكالمات إلا لوظائف kernel الخاصة (مساعدين eBPF).
يبدو تصميم المرشح وتركيبه كما يلي:
- يتم ترجمة التعليمات البرمجية المصدر (على سبيل المثال ،
kernel.c
) إلى الكائن ( kernel.o
) ضمن بنية الجهاز الظاهري eBPF. اعتبارًا من أكتوبر 2019 ، تم دعم التحويل البرمجي في eBPF من قِبل Clang ووعد في GCC 10.1. - إذا كان هناك في رمز الكائن هذا مكالمات لبنية kernel (على سبيل المثال ، الجداول والعدادات) ، يتم استخدام الأصفار بدلاً من معرفاتها ، أي أنه لا يمكن تنفيذ مثل هذا الرمز. قبل التحميل في kernel ، تحتاج إلى استبدال هذه الأصفار بمعرف كائنات محددة تم إنشاؤها من خلال مكالمات kernel (رمز الرابط). يمكنك القيام بذلك باستخدام أدوات مساعدة خارجية ، أو يمكنك كتابة برنامج يقوم بربط عامل تصفية محدد وتحميله.
- يتحقق kernel من البرنامج المحمّل. يتم التحقق من عدم وجود حلقات والغياب وراء حدود الحزمة والمكدس. إذا تعذر على المدقق إثبات صحة الكود ، فسيتم رفض البرنامج - يجب أن تكون قادرًا على إرضائه.
- بعد التحقق بنجاح ، تقوم kernel بتجميع كود كائن هندسة eBPF في كود آلة هندسة النظام (في الوقت المناسب).
- البرنامج يعلق على واجهة ويبدأ في معالجة الحزم.
نظرًا لأن XDP يعمل في kernel ، يتم تصحيح الأخطاء عن طريق سجلات التتبع ، وفي الواقع ، بواسطة الحزم التي يقوم البرنامج بتصفيةها أو إنشاؤها. ومع ذلك ، يوفر eBPF الحماية للرمز الذي تم تحميله للنظام ، بحيث يمكنك تجربة XDP مباشرة على Linux المحلي.
إعداد البيئة
جمعية
لا يمكن لـ Clang إصدار رمز كائن لبنية eBPF مباشرة ، لذلك تتكون العملية من خطوتين:
- ترجمة C رمز في LLVM bytecode (
clang -emit-llvm
). - تحويل bytecode إلى رمز كائن eBPF (
llc -march=bpf -filetype=obj
).
عند كتابة مرشح ، يكون الملفان اللذان يحتويان على وظائف مساعدة ووحدات ماكرو من اختبارات kernel مفيدًا. من المهم أن تتطابق مع إصدار kernel ( KVER
). قم helpers/
في helpers/
:
export KVER=v5.3.7 export BASE=https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/plain/tools/testing/selftests/bpf wget -P helpers --content-disposition "${BASE}/bpf_helpers.h?h=${KVER}" "${BASE}/bpf_endian.h?h=${KVER}" unset KVER BASE
Makefile for Arch Linux (kernel 5.3.7):
CLANG ?= clang LLC ?= llc KDIR ?= /lib/modules/$(shell uname -r)/build ARCH ?= $(subst x86_64,x86,$(shell uname -m)) CFLAGS = \ -Ihelpers \ \ -I$(KDIR)/include \ -I$(KDIR)/include/uapi \ -I$(KDIR)/include/generated/uapi \ -I$(KDIR)/arch/$(ARCH)/include \ -I$(KDIR)/arch/$(ARCH)/include/generated \ -I$(KDIR)/arch/$(ARCH)/include/uapi \ -I$(KDIR)/arch/$(ARCH)/include/generated/uapi \ -D__KERNEL__ \ \ -fno-stack-protector -O2 -g xdp_%.o: xdp_%.c Makefile $(CLANG) -c -emit-llvm $(CFLAGS) $< -o - | \ $(LLC) -march=bpf -filetype=obj -o $@ .PHONY: all clean all: xdp_filter.o clean: rm -f ./*.o
يحتوي KDIR
على المسار إلى رؤوس kernel ، ARCH
- بنية النظام. قد تختلف المسارات والأدوات قليلاً بين التوزيعات.
مثال على الفرق لـ Debian 10 (kernel 4.19.67) # CLANG ?= clang LLC ?= llc-7 # KDIR ?= /usr/src/linux-headers-$(shell uname -r) ARCH ?= $(subst x86_64,x86,$(shell uname -m)) # -I CFLAGS = \ -Ihelpers \ \ -I/usr/src/linux-headers-4.19.0-6-common/include \ -I/usr/src/linux-headers-4.19.0-6-common/arch/$(ARCH)/include \ #
تتضمن CFLAGS
دليلًا يحتوي على رؤوس مساعدة والعديد من الأدلة التي تحتوي على رؤوس kernel. يعني رمز __KERNEL__
أن رؤوس UAPI (واجهات برمجة التطبيقات لفضاء المستخدمين) محددة لرمز kernel ، لأن المرشح يعمل في kernel.
يمكن تعطيل حماية المكدس ( -fno-stack-protector
) ، لأن أداة التحقق من كود eBPF ما زالت تبحث عن مخرج من المكدس. يجب تضمين التحسين على الفور لأن حجم رمز eBPF bytecode محدود.
لنبدأ بمرشح يتخطى جميع الحزم ولا يفعل شيئًا:
#include <uapi/linux/bpf.h> #include <bpf_helpers.h> SEC("prog") int xdp_main(struct xdp_md* ctx) { return XDP_PASS; } char _license[] SEC("license") = "GPL";
xdp_filter.o
الأمر make
xdp_filter.o
. أين لاختباره الآن؟
موقف اختبار
يجب أن يشتمل الحامل على واجهتين: حيث سيكون هناك مرشح وأي منها سيتم إرسال الحزم. يجب أن تكون هذه أجهزة Linux كاملة الوظائف مع IP الخاص بها من أجل التحقق من كيفية عمل التطبيقات العادية مع الفلتر الخاص بنا.
أجهزة مثل veth (شبكة إيثرنت افتراضية) مناسبة لنا: فهي عبارة عن زوج من واجهات الشبكة الافتراضية "متصلة" مباشرة مع بعضها البعض. يمكنك إنشائها مثل هذا (في هذا القسم ، يتم تنفيذ جميع أوامر ip
root
):
ip link add xdp-remote type veth peer name xdp-local
هنا xdp-remote
و xdp-local
هي أسماء الأجهزة. سيتم إرفاق عامل تصفية بـ xdp-local
(192.0.2.1/24) ، وسيتم إرسال حركة المرور الواردة من xdp-remote
(192.0.2.2/24). ومع ذلك ، فهناك مشكلة: فالواجهات موجودة على نفس الجهاز ، ولن يرسل Linux حركة المرور إلى واحد منهم عبر الآخر. يمكنك حل هذا باستخدام قواعد iptables
الصعبة ، لكن سيتعين عليها تغيير الحزم ، وهو أمر غير مريح عند تصحيح الأخطاء. من الأفضل استخدام مساحات أسماء الشبكات (مساحات أسماء الشبكات ، المشار إليها فيما يلي بالشبكات).
تحتوي مساحة اسم الشبكة على مجموعة من الواجهات وجداول التوجيه وقواعد NetFilter ، معزولة عن كائنات مماثلة في شبكات أخرى. يتم تشغيل كل عملية في مساحة اسم ، ولا يمكن الوصول إليها إلا من كائنات هذه الشبكات. بشكل افتراضي ، يحتوي النظام على مساحة اسم شبكة واحدة لجميع الكائنات ، بحيث يمكنك العمل على Linux وعدم معرفة الشبكات.
إنشاء مساحة اسم xdp-test
جديدة ونقل xdp-remote
.
ip netns add xdp-test ip link set dev xdp-remote netns xdp-test
عندئذٍ لن "ترى" xdp-local
العملية قيد التشغيل في xdp-test
xdp-local
(ستبقى في الشبكات الافتراضية بشكل افتراضي) وسوف ترسلها عبر xdp-remote
عند إرسال حزمة إلى 192.0.2.1 ، لأن هذه هي الواجهة الوحيدة في 192.0.2.0/ 24 المتاحة لهذه العملية. هذا يعمل أيضا في الاتجاه المعاكس.
عند التنقل بين الشبكات ، تسقط الواجهة وتفقد العنوان. لتكوين الواجهة في netns ، تحتاج إلى تشغيل ip ...
في مساحة اسم أمر ip netns exec
:
ip netns exec xdp-test \ ip address add 192.0.2.2/24 dev xdp-remote ip netns exec xdp-test \ ip link set xdp-remote up
كما ترون ، هذا لا يختلف عن إعداد xdp-local
في مساحة الاسم الافتراضية:
ip address add 192.0.2.1/24 dev xdp-local ip link set xdp-local up
إذا قمت بتشغيل tcpdump -tnevi xdp-local
، يمكنك أن ترى أن الحزم المرسلة من xdp-test
يتم تسليمها إلى هذه الواجهة:
ip netns exec xdp-test ping 192.0.2.1
أنها مريحة لتشغيل shell في xdp-test
. يوجد برنامج نصي في المستودع يعمل تلقائيًا مع الحامل ، على سبيل المثال ، يمكنك تكوين الحامل باستخدام sudo ./stand up
command وحذفه باستخدام sudo ./stand down
تتبع
يتم توصيل المرشح بالجهاز كما يلي:
ip -force link set dev xdp-local xdp object xdp_filter.o verbose
يلزم -force
فرض ربط برنامج جديد إذا كان آخر منضمًا بالفعل. "لا توجد أخبار جيدة" لا يتعلق بهذا الأمر ، فالنتيجة هي في أي حال ضخمة. verbose
اختياريًا ، ولكن به يظهر تقرير عن عمل أداة التحقق من الكود مع قائمة التجميع:
Verifier analysis: 0: (b7) r0 = 2 1: (95) exit
فك البرنامج من الواجهة:
ip link set dev xdp-local xdp off
في البرنامج النصي ، هذه هي sudo ./stand attach
و sudo ./stand detach
.
من خلال إرفاق مرشح ، يمكنك التحقق من استمرار تنفيذ الأمر ping
، لكن هل يعمل البرنامج؟ أضف السجلات. bpf_trace_printk()
وظيفة bpf_trace_printk()
printf()
، لكنها تدعم حتى ثلاث وسيطات فقط ، باستثناء القالب ، وقائمة محدودة من المؤهلات. يبسط الماكرو bpf_printk()
المكالمة.
SEC("prog") int xdp_main(struct xdp_md* ctx) { + bpf_printk("got packet: %p\n", ctx); return XDP_PASS; }
يذهب الإخراج إلى قناة التتبع kernel ، والتي تحتاج إلى تمكين:
echo -n 1 | sudo tee /sys/kernel/debug/tracing/options/trace_printk
عرض تدفق الرسالة:
cat /sys/kernel/debug/tracing/trace_pipe
يقوم كلا الأمرين بإجراء مكالمة sudo ./stand log
.
يجب على Ping الآن تشغيل الرسائل التالية فيه:
<...>-110930 [004] ..s1 78803.244967: 0: got packet: 00000000ac510377
إذا نظرت عن كثب إلى إخراج المدقق ، ستلاحظ حسابات غريبة:
0: (bf) r3 = r1 1: (18) r1 = 0xa7025203a7465 3: (7b) *(u64 *)(r10 -8) = r1 4: (18) r1 = 0x6b63617020746f67 6: (7b) *(u64 *)(r10 -16) = r1 7: (bf) r1 = r10 8: (07) r1 += -16 9: (b7) r2 = 16 10: (85) call bpf_trace_printk#6 <...>
الحقيقة هي أن برامج eBPF لا تحتوي على قسم بيانات ، وبالتالي فإن الطريقة الوحيدة لترميز سلسلة التنسيق هي باستخدام الوسائط المباشرة لأوامر VM:
$ python -c "import binascii; print(bytes(reversed(binascii.unhexlify('0a7025203a74656b63617020746f67'))))" b'got packet: %p\n'
لهذا السبب ، يؤدي إخراج التصحيح إلى تضخيم التعليمة البرمجية الناتجة إلى حد كبير.
إرسال حزم XDP
دعنا نغير المرشح: دعه يرسل جميع الحزم الواردة مرة أخرى. هذا غير صحيح من وجهة نظر الشبكة ، حيث سيكون من الضروري تغيير العناوين في الرؤوس ، ولكن العمل الآن مهم من حيث المبدأ.
bpf_printk("got packet: %p\n", ctx); - return XDP_PASS; + return XDP_TX; }
تشغيل tcpdump
على xdp-remote
. يجب أن يُظهر طلب ارتداد ICMP الصادر والوارد متطابقًا ويتوقف عن إظهار رد ارتداد ICMP. ولكن لا تظهر. اتضح أنه لكي يعمل XDP_TX
في برنامج على xdp-local
من الضروري xdp-remote
واجهة xdp-remote
للواجهة xdp-remote
، حتى لو كانت فارغة ، ويجب رفعها.
كيف اكتشفت ذلك؟بالمناسبة ، تتيح آلية أحداث perf ، باستخدام نفس الجهاز الظاهري ، تتبع مسار الحزمة في النواة ، أي يتم استخدام eBPF للتفكيك مع eBPF.
عليك أن تصنع الخير من الشر ، لأنه لا يوجد شيء آخر تستفيد منه.
$ sudo perf trace --call-graph dwarf -e 'xdp:*' 0.000 ping/123455 xdp:xdp_bulk_tx:ifindex=19 action=TX sent=0 drops=1 err=-6 veth_xdp_flush_bq ([veth]) veth_xdp_flush_bq ([veth]) veth_poll ([veth]) <...>
ما هو الكود 6؟
$ errno 6 ENXIO 6 No such device or address
تتلقى وظيفة veth_xdp_flush_bq()
رمز خطأ من veth_xdp_xmit()
، حيث نبحث عن طريق ENXIO
ونجد تعليقًا.
استعادة الحد الأدنى للتصفية ( XDP_PASS
) في ملف xdp_dummy.c
، أضفه إلى Makefile ، xdp-remote
بـ xdp-remote
:
ip netns exec remote \ ip link set dev int xdp object dummy.o
يظهر الآن tcpdump
ما هو متوقع:
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), length 84) 192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, length 64 62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), length 84) 192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, length 64
إذا تم عرض ARP فقط ، فستحتاج إلى إزالة المرشحات (يتم ذلك عن طريق sudo ./stand detach
) ، ابدأ في ping
، ثم قم بتعيين المرشحات وحاول مرة أخرى. المشكلة هي أن عامل التصفية XDP_TX
يؤثر على كل من ARP وإذا كان المكدس
تمكنت مساحة اسم xdp-test
من "نسيان" عنوان MAC 192.0.2.1 ، ولن تتمكن من حل عنوان IP هذا.
بيان المشكلة
دعنا ننتقل إلى المهمة المذكورة: كتابة آلية ملفات تعريف الارتباط SYN على XDP.
حتى الآن ، يظل فيضان SYN هجوم DDoS شائع ، جوهره على النحو التالي. عند إنشاء اتصال (مصافحة TCP) ، يستقبل الخادم SYN ، ويخصص موارد لاتصال مستقبلي ، ويستجيب بحزمة SYNACK ، وينتظر ACK. يقوم المهاجم ببساطة بإرسال حزم SYN من عناوين وهمية بمبلغ الآلاف في الثانية من كل مضيف من عدة آلاف من الروبوتات. يضطر الخادم إلى تخصيص الموارد فور وصول الحزمة ، ويتحرر وقت انتهاء مهلة كبير ، ونتيجة لذلك ، يتم استنفاد الذاكرة أو الحدود ، عدم قبول اتصالات جديدة ، الخدمة غير متوفرة.
إذا لم تقم بتخصيص موارد لحزمة SYN ، لكنك استجابت فقط بحزمة SYNACK ، فكيف يمكن أن يفهم الخادم أن حزمة ACK التي جاءت لاحقًا تشير إلى حزمة SYN لم يتم حفظها؟ بعد كل شيء ، يمكن للمهاجم أيضا إنشاء ACKs وهمية. جوهر ملف تعريف الارتباط SYN هو تشفير معلمات seqnum
في seqnum
من العناوين والموانئ وتغيير الملح. إذا تمكنت ACK من الوصول قبل تغيير الملح ، فيمكنك مرة أخرى حساب التجزئة ومقارنته مع acknum
. لا يمكن للمهاجم مزيف acknum
، لأن الملح يتضمن سرًا ، ولن يكون لديه وقت للفرز بسبب القناة المحدودة.
تم تطبيق ملف تعريف الارتباط SYN منذ فترة طويلة في نواة Linux وقد يتم تشغيله تلقائيًا إذا وصل SYN بسرعة كبيرة وبكميات كبيرة.
البرنامج التعليمي على مصافحة TCPيوفر TCP نقل البيانات كدفق من البايتات ، على سبيل المثال ، يتم إرسال طلبات HTTP عبر TCP. ينتقل التيار في قطع في حزم. تحتوي جميع حزم TCP على علامات منطقية وأرقام تسلسل 32 بت:
تحدد مجموعة العلامات دور حزمة معينة. تعني علامة SYN أن هذه هي الحزمة الأولى للمرسل في الاتصال. تعني علامة ACK أن المرسل قد تلقى جميع بيانات الاتصال قبل بايت acknum
. يمكن أن تحتوي الحزمة على عدة علامات ويتم استدعاؤها بواسطة المجموعة الخاصة بها ، على سبيل المثال ، حزمة SYNACK.
يحدد رقم التسلسل (seqnum) الإزاحة في دفق البيانات للبايت الأول الذي يتم إرساله في هذه الحزمة. على سبيل المثال ، إذا كان هذا الرقم في الحزمة الأولى التي تحتوي على X بايتات من البيانات هو N ، فسيكون N + X في الحزمة التالية مع بيانات جديدة. في بداية الاتصال ، يقوم كل جانب بتحديد هذا الرقم بشكل تعسفي.
رقم الإقرار (acknum) - هو نفس الإزاحة seqnum ، ولكنه لا يحدد عدد البايتات المراد إرسالها ، ولكن رقم البايت الأول من المستلم الذي لم يره المرسل.
في بداية الاتصال ، يجب أن يتفق الطرفان على seqnum
و acknum
. يرسل العميل حزمة SYN مع seqnum = X
يستجيب الخادم بحزمة SYNACK ، حيث يكتب seqnum = Y
acknum = X + 1
. يستجيب العميل إلى SYNACK مع حزمة ACK ، حيث seqnum = X + 1
، acknum = Y + 1
. بعد ذلك ، يبدأ نقل البيانات الفعلي.
إذا لم يؤكد المحاور استلام الحزمة ، فسيرسلها TCP مرة أخرى بحلول المهلة.
لماذا لا يتم استخدام ملفات تعريف الارتباط SYN دائمًا؟أولاً ، في حالة فقد SYNACK أو ACK ، يجب عليك انتظار إعادة الإرسال - يتم إبطاء الاتصال. ثانيا ، في حزمة SYN - وفقط في ذلك! - يتم إرسال عدد من الخيارات التي تؤثر على مواصلة تشغيل الاتصال. دون تذكر حزم SYN الواردة ، يتجاهل الخادم هذه الخيارات ، في الحزم التالية لن يرسلها العميل بعد الآن. في هذه الحالة ، يمكن أن يعمل TCP ، ولكن على الأقل في المرحلة الأولية ، ستنخفض جودة الاتصال.
من حيث الحزم ، يجب أن يقوم برنامج XDP بما يلي:
- SYNACK مع ملف تعريف الارتباط للرد على SYN ؛
- الرد على ACK RST (قطع الاتصال) ؛
- تجاهل الحزم الأخرى.
خوارزمية الكود الكاذب مع تحليل الحزمة:
Ethernet, . IPv4, . , (*) , . TCP, . (**) SYN, SYN-ACK cookie. ACK, acknum cookie, . N . (*) RST. (**) .
يشير واحد (*)
إلى النقاط التي يمكن من خلالها التحكم في حالة النظام - في المرحلة الأولى ، يمكنك الاستغناء عنها ببساطة عن طريق تطبيق مصافحة TCP مع إنشاء ملف تعريف ارتباط SYN مثل seqnum.
في المكان (**)
، على الرغم من عدم وجود جدول ، فإننا سنتخطى الحزمة.
تنفيذ مصافحة TCP
تحليل الحزمة والتحقق من الكود
نحن بحاجة إلى هياكل رأس الشبكة: Ethernet ( uapi/linux/if_ether.h
) و IPv4 ( uapi/linux/ip.h
) و TCP ( uapi/linux/tcp.h
). آخر واحد لم أستطع الاتصال بسبب الأخطاء المتعلقة atomic64_t
، واضطررت إلى نسخ التعاريف اللازمة في التعليمات البرمجية.
يجب أن تكون جميع الوظائف المخصصة في C لقابلية القراءة مدمجة في مكان الاستدعاء ، لأن مدقق eBPF في النواة يحظر الانتقال من جديد ، أي في الواقع حلقات ومكالمات الوظائف.
#define INTERNAL static __attribute__((always_inline))
تعطيل الماكرو LOG()
الطباعة في إنشاء الإصدار.
البرنامج عبارة عن ناقل للوظائف. يتلقى كل منها حزمة يتم فيها تمييز رأس المستوى المقابل ، على سبيل المثال ، يتوقع process_ether()
أن يكون ether
ممتلئًا. بناءً على نتائج التحليل الميداني ، يمكن أن تنقل الوظيفة الرزمة إلى مستوى أعلى. نتيجة الدالة هي إجراء XDP. حتى الآن ، تقوم معالجات SYN و ACK بتمرير جميع الحزم.
struct Packet { struct xdp_md* ctx; struct ethhdr* ether; struct iphdr* ip; struct tcphdr* tcp; }; INTERNAL int process_tcp_syn(struct Packet* packet) { return XDP_PASS; } INTERNAL int process_tcp_ack(struct Packet* packet) { return XDP_PASS; } INTERNAL int process_tcp(struct Packet* packet) { ... } INTERNAL int process_ip(struct Packet* packet) { ... } INTERNAL int process_ether(struct Packet* packet) { struct ethhdr* ether = packet->ether; LOG("Ether(proto=0x%x)", bpf_ntohs(ether->h_proto)); if (ether->h_proto != bpf_ntohs(ETH_P_IP)) { return XDP_PASS; } // B struct iphdr* ip = (struct iphdr*)(ether + 1); if ((void*)(ip + 1) > (void*)packet->ctx->data_end) { return XDP_DROP; /* malformed packet */ } packet->ip = ip; return process_ip(packet); } SEC("prog") int xdp_main(struct xdp_md* ctx) { struct Packet packet; packet.ctx = ctx; // A struct ethhdr* ether = (struct ethhdr*)(void*)ctx->data; if ((void*)(ether + 1) > (void*)ctx->data_end) { return XDP_PASS; } packet.ether = ether; return process_ether(&packet); }
ألفت الانتباه إلى الفحوصتين A و B. إذا علقت على A ، فسيتم تجميع البرنامج ، ولكن سيكون هناك خطأ في التحقق أثناء التحميل:
Verifier analysis: <...> 11: (7b) *(u64 *)(r10 -48) = r1 12: (71) r3 = *(u8 *)(r7 +13) invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0) R7 offset is outside of the packet processed 11 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0 Error fetching program/map!
سطر المفتاح هو invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0)
: هناك مسارات للتنفيذ عندما تكون البايتة الثالثة عشرة من بداية المخزن المؤقت خارج الحزمة. وفقًا لقائمة البيانات ، من الصعب فهم أي سطر نتحدث عنه ، ولكن يوجد رقم تعليمي (12) ومفكك يوضح أسطر الكود المصدري:
llvm-objdump -S xdp_filter.o | less
في هذه الحالة ، يشير إلى سلسلة
LOG("Ether(proto=0x%x)", bpf_ntohs(ether->h_proto));
التي من الواضح أن المشكلة في ether
. سيكون دائما كذلك.
الرد على SYN
الهدف في هذه المرحلة هو تكوين حزمة SYNACK صحيحة مع seqnum
ثابت ، والذي سيتم استبداله بملف تعريف ارتباط SYN في المستقبل. تحدث كل التغييرات في process_tcp_syn()
والمنطقة المحيطة بها.
فحص الحزمة
من الغريب أن هذا هو الخط الأكثر بروزًا ، وبشكل أكثر دقة ، تعليق عليه:
/* Required to verify checksum calculation */ const void* data_end = (const void*)ctx->data_end;
عند كتابة الإصدار الأول من الكود ، تم استخدام kernel 5.1 ، وكان هناك فرق بين data_end
و (const void*)ctx->data_end
. عند كتابة المقالة ، لم يكن لدى 5.3.1 kernel هذه المشكلة. ربما قام المترجم بالوصول إلى المتغير المحلي بشكل مختلف عن الحقل. الشفرة الأخلاقية المبسطة يمكن أن تساعد في الكثير من التعشيش.
مزيد من الفحوصات الروتينية للأطوال على شرف المدقق حول MAX_CSUM_BYTES
أدناه.
const u32 ip_len = ip->ihl * 4; if ((void*)ip + ip_len > data_end) { return XDP_DROP; /* malformed packet */ } if (ip_len > MAX_CSUM_BYTES) { return XDP_ABORTED; /* implementation limitation */ } const u32 tcp_len = tcp->doff * 4; if ((void*)tcp + tcp_len > (void*)ctx->data_end) { return XDP_DROP; /* malformed packet */ } if (tcp_len > MAX_CSUM_BYTES) { return XDP_ABORTED; /* implementation limitation */ }
حزمة انتشار
املأ seqnum
و acknum
، اضبط ACK (تم ضبط SYN بالفعل):
const u32 cookie = 42; tcp->ack_seq = bpf_htonl(bpf_ntohl(tcp->seq) + 1); tcp->seq = bpf_htonl(cookie); tcp->ack = 1;
تبديل منافذ TCP وعنوان IP وعنوان MAC. لا يمكن الوصول إلى المكتبة القياسية من برنامج XDP ، لذلك memcpy()
هو ماكرو يخفي كلانج مضمنًا.
const u16 temp_port = tcp->source; tcp->source = tcp->dest; tcp->dest = temp_port; const u32 temp_ip = ip->saddr; ip->saddr = ip->daddr; ip->daddr = temp_ip; struct ethhdr temp_ether = *ether; memcpy(ether->h_dest, temp_ether.h_source, ETH_ALEN); memcpy(ether->h_source, temp_ether.h_dest, ETH_ALEN);
اختباري إعادة حساب
تتطلب عمليات اختبار IPv4 و TCP إضافة جميع الكلمات ذات 16 بت في الرؤوس ، كما أن حجم الرؤوس مكتوب فيها ، أي أنه في وقت التجميع ، يكون الأمر غير معروف. هذه مشكلة لأن المدقق لن يتخطى حلقة منتظمة إلى حدود متغيرة. لكن حجم الرؤوس محدود: ما يصل إلى 64 بايت لكل منهما. يمكنك إنشاء حلقة مع عدد ثابت من التكرارات ، والتي يمكن أن تنتهي قبل الموعد المحدد.
ألاحظ أن هناك RFC 1624 حول كيفية إعادة حساب المجموع الاختباري جزئيًا إذا تم تغيير كلمات الحزمة الثابتة فقط. ومع ذلك ، فإن الطريقة ليست عالمية ، وسيكون التنفيذ أكثر صعوبة.
اختباري حساب وظيفة:
#define MAX_CSUM_WORDS 32 #define MAX_CSUM_BYTES (MAX_CSUM_WORDS * 2) INTERNAL u32 sum16(const void* data, u32 size, const void* data_end) { u32 s = 0; #pragma unroll for (u32 i = 0; i < MAX_CSUM_WORDS; i++) { if (2*i >= size) { return s; /* normal exit */ } if (data + 2*i + 1 + 1 > data_end) { return 0; /* should be unreachable */ } s += ((const u16*)data)[i]; } return s; }
, size
, , .
32- :
INTERNAL u32 sum16_32(u32 v) { return (v >> 16) + (v & 0xffff); }
:
ip->check = 0; ip->check = carry(sum16(ip, ip_len, data_end)); u32 tcp_csum = 0; tcp_csum += sum16_32(ip->saddr); tcp_csum += sum16_32(ip->daddr); tcp_csum += 0x0600; tcp_csum += tcp_len << 8; tcp->check = 0; tcp_csum += sum16(tcp, tcp_len, data_end); tcp->check = carry(tcp_csum); return XDP_TX;
carry()
32- 16- , RFC 791.
TCP
netcat
, ACK, Linux RST-, SYN — SYNACK - , .
$ sudo ip netns exec xdp-test nc -nv 192.0.2.1 6666 192.0.2.1 6666: Connection reset by peer
tcpdump
xdp-remote
, , hping3
.
SYN cookie
XDP . , , . Linux, , SipHash, XDP .
TODO, :
:
$ sudoip netns exec xdp-test nc -nv 192.0.2.1 6666 192.0.2.1 6666: Connection reset by peer
( flags=0x2
— SYN, flags=0x10
— ACK):
Ether(proto=0x800) IP(src=0x20e6e11a dst=0x20e6e11e proto=6) TCP(sport=50836 dport=6666 flags=0x2) Ether(proto=0x800) IP(src=0xfe2cb11a dst=0xfe2cb11e proto=6) TCP(sport=50836 dport=6666 flags=0x10) cookie matches for client 20200c0
IP, SYN flood , ACK flood, :
sudo ip netns exec xdp-test hping3 --flood -A -s 1111 -p 2222 192.0.2.1
:
Ether(proto=0x800) IP(src=0x15bd11a dst=0x15bd11e proto=6) TCP(sport=3236 dport=2222 flags=0x10) cookie mismatch
استنتاج
eBPF XDP , . , XDP — , , DPDK kernel bypass. , XDP , , , . , userspace-.
, , , userspace- .
المراجع: