نكتب الحماية ضد هجمات DDoS على XDP. الجزء النووي

تسمح تقنية مسار بيانات eXpress (XDP) بمعالجة عشوائية لحركة المرور على واجهات Linux قبل دخول الحزم إلى مكدس شبكة kernel. تطبيق XDP - الحماية من هجمات DDoS (CloudFlare) ، المرشحات المعقدة ، جمع الإحصائيات (Netflix). يتم تنفيذ برامج XDP بواسطة الجهاز الظاهري لـ eBPF ، وبالتالي فإن لها قيودًا على كل من التعليمات البرمجية ووظائف kernel المتاحة ، اعتمادًا على نوع المرشح.

تهدف المقالة إلى تعويض أوجه القصور في العديد من المواد على XDP. أولاً ، يقدمون رمزًا جاهزًا يتخطى فورًا ميزات XDP: مُعد للتحقق أو بسيط جدًا بحيث لا يتسبب في حدوث مشكلات. عندما تحاول كتابة التعليمات البرمجية الخاصة بك من البداية لاحقًا ، لا يوجد فهم لما يجب فعله مع الأخطاء النموذجية. ثانيًا ، لا يغطي طرق اختبار XDP محليًا بدون جهاز افتراضي وأجهزة ، على الرغم من حقيقة أن لديهم عيوبهم الخاصة. النص مخصص للمبرمجين المطلعين على الشبكات ونظام Linux المهتمين بـ 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).

يبدو تطوير مرشح وتثبيته كما يلي:

  1. كود المصدر (على سبيل المثال. kernel.c) يجمع للاعتراض (kernel.o) لبنية الآلة الافتراضية eBPF. اعتبارًا من أكتوبر 2019 ، يتم دعم التجميع إلى eBPF بواسطة Clang ووعد به في دول مجلس التعاون الخليجي 10.1.
  2. إذا كانت هناك استدعاءات في كود الكائن هذا لهياكل kernel (على سبيل المثال ، للجداول والعدادات) ، فبدلاً من معرفاتهم توجد أصفار ، أي لا يمكن تنفيذ هذا الكود. قبل التحميل في النواة ، يجب استبدال هذه الأصفار بمعرفات كائنات محددة تم إنشاؤها من خلال استدعاءات kernel (اربط الكود). يمكنك القيام بذلك باستخدام أدوات مساعدة خارجية ، أو يمكنك كتابة برنامج يقوم بربط وتحميل عامل تصفية معين.
  3. تتحقق النواة من البرنامج الجاري تحميله. يتحقق من عدم وجود دورات وعدم خروج من الحزمة وحدود المكدس. إذا لم يتمكن المدقق من إثبات صحة الرمز ، فسيتم رفض البرنامج - يجب أن يكون المرء قادرًا على إرضائه.
  4. بعد التحقق الناجح ، تقوم النواة بتجميع رمز كائن بنية eBPF في رمز آلة بنية النظام (في الوقت المناسب).
  5. يتم إرفاق البرنامج بالواجهة ويبدأ في معالجة الحزم.

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

اعداد البيئة

جمعية

لا يمكن لـ Clang إصدار رمز كائن مباشرة لهندسة eBPF ، لذلك تتكون العملية من خطوتين:

  1. ترجمة كود C إلى LLVM bytecode (clang -emit-llvm).
  2. تحويل بايت كود إلى كود كائن eBPF (llc -march=bpf -filetype=obj).

عند كتابة مرشح ، سيكون زوجان من الملفات ذات الوظائف الإضافية ووحدات الماكرو في متناول اليد من اختبارات النواة. من المهم أن تتطابق مع إصدار kernel (KVER). قم بتنزيلها على 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 (userspace API) محددة لرمز 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";

فريق 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-test، لن ترى" xdp-local (ستبقى في netns افتراضيًا) وعند إرسال حزمة إلى 192.0.2.1 سوف تمر عبرها xdp-remote، لأن هذه هي الواجهة الوحيدة المتاحة لهذه العملية عند 192.0.2.0/24. هذا أيضا يعمل في الاتجاه المعاكس.

عند التنقل بين الشبكات ، تنخفض الواجهة وتفقد العنوان. لإعداد واجهة في الشبكات ، تحتاج إلى تشغيل 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

من الملائم تشغيل قذيفة فيها xdp-test. يحتوي المستودع على برنامج نصي يعمل على أتمتة العمل مع الحامل ، على سبيل المثال ، يمكنك إعداد الحامل باستخدام الأمر sudo ./stand up وإزالته 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() مشابه ل printf()، ولكنه يدعم فقط ما يصل إلى ثلاث وسيطات بخلاف النمط وقائمة محدودة من المحددات. دقيق bpf_printk() يبسط المكالمة.

   SEC("prog")
   int xdp_main(struct xdp_md* ctx) {
+      bpf_printk("got packet: %pn", 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: %pn'

لهذا السبب ، يؤدي إخراج التصحيح إلى تضخيم الكود الناتج بشكل كبير.

إرسال حزم XDP

دعنا نغير عامل التصفية: دعه يرسل كل الحزم الواردة مرة أخرى. هذا غير صحيح من وجهة نظر الشبكة ، حيث سيكون من الضروري تغيير العناوين في الرؤوس ، لكن العمل الآن من حيث المبدأ مهم.

       bpf_printk("got packet: %pn", ctx);
-      return XDP_PASS;
+      return XDP_TX;
   }

نطلق tcpdump في xdp-remote. يجب أن يُظهر طلب ارتداد ICMP الصادر والوارد المتطابق ويتوقف عن إظهار رد ICMP Echo Reply. لكنها لا تظهر. اتضح للعمل XDP_TX في برنامج xdp-local يجبلإقران الواجهة 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:

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 التي لم يتم حفظها؟ بعد كل شيء ، يمكن للمهاجم أيضًا إنشاء إشعارات مزيفة. جوهر ملف تعريف الارتباط SYN هو الترميز بتنسيق seqnum معلمات الاتصال كتجزئة للعناوين والموانئ والملح المتغير. إذا تمكنت ACK من الوصول قبل تغيير الملح ، فيمكنك حساب التجزئة مرة أخرى والمقارنة مع acknum. مزيف acknum لا يستطيع المهاجم ، لأن الملح يشمل السر ، ولن يكون لديه وقت لفرزها بسبب القناة المحدودة.

تم تنفيذ ملفات تعريف الارتباط SYN في Linux kernel لفترة طويلة ويمكن حتى تمكينها تلقائيًا إذا وصلت SYNs بسرعة كبيرة وبشكل مجمّع.

برنامج تعليمي عن مصافحة 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 بما يلي:

  • الرد على SYN مع SYNACK مع ملف تعريف الارتباط ؛
  • الرد على ACK مع RST (قطع الاتصال) ؛
  • إسقاط الحزم الأخرى.

الكود الزائف للخوارزمية مع تحليل الحزمة:

Если это не Ethernet,
    пропустить пакет.
Если это не IPv4,
    пропустить пакет.
Если адрес в таблице проверенных,               (*)
        уменьшить счетчик оставшихся проверок,
        пропустить пакет.
Если это не TCP,
    сбросить пакет.     (**)
Если это SYN,
    ответить SYN-ACK с cookie.
Если это ACK,
    если в acknum лежит не cookie,
        сбросить пакет.
    Занести в таблицу адрес с N оставшихся проверок.    (*)
    Ответить RST.   (**)
В остальных случаях сбросить пакет.

واحد (*) يتم تمييز النقاط التي تحتاج فيها إلى إدارة حالة النظام - في المرحلة الأولى ، يمكنك الاستغناء عنها ببساطة عن طريق تنفيذ مصافحة TCP مع إنشاء ملف تعريف ارتباط SYN باعتباره seqnum.

بالموقع (**)، بينما ليس لدينا طاولة ، سنتخطى الحزمة.

تنفيذ مصافحة TCP

تحليل الحزمة والتحقق من الكود

نحتاج إلى هياكل رؤوس الشبكة: إيثرنت (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;

عند كتابة الإصدار الأول من الكود ، تم استخدام نواة 5.1 ، حيث كان هناك فرق بين المدقق data_end и (const void*)ctx->data_end. في وقت كتابة هذا التقرير ، لم يكن لدى النواة 5.3.1 هذه المشكلة. ربما كان المترجم يصل إلى متغير محلي بشكل مختلف عن الحقل. أخلاقي - عند إجراء تداخل كبير ، يمكن أن يساعد تبسيط الكود.

المزيد من الفحوصات الروتينية لأطوال مجد المدقق ؛ ا 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 لا يستجيب لمجموعات اختبارية غير صحيحة.

من وجهة نظر XDP ، فإن الشيك نفسه تافه. تعد خوارزمية الحساب بدائية وربما تكون عرضة لمهاجم متطور. نواة Linux ، على سبيل المثال ، تستخدم التشفير SipHash ، ولكن من الواضح أن تنفيذها لـ XDP خارج نطاق هذه المقالة.

ظهرت في TODOs الجديدة المتعلقة بالتفاعل الخارجي:

  • لا يمكن تخزين برنامج XDP cookie_seed (الجزء السري من الملح) في متغير عام ، فأنت بحاجة إلى متجر kernel سيتم تحديث قيمته بشكل دوري من مُنشئ موثوق.

  • إذا تطابق ملف تعريف الارتباط SYN في حزمة ACK ، فلن تحتاج إلى طباعة رسالة ، ولكن تذكر عنوان IP الخاص بالعميل الذي تم التحقق منه لتخطي الحزم منه.

التحقق من صحة من قبل عميل شرعي:

$ 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 نفسه ، ولكن هنا رد الفعل على فيضان ACK الذي أطلقه هذا الأمر:

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 الأخرى. من ناحية أخرى ، يتيح لك XDP تنفيذ منطق معقد إلى حد ما ، والذي ، علاوة على ذلك ، من السهل تحديثه دون توقف في معالجة حركة المرور. المدقق لا يخلق مشاكل كبيرة ، وأنا شخصيا لن أرفض مثل هذه لأجزاء من رمز مساحة المستخدمين.

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

المراجع:

المصدر: www.habr.com

إضافة تعليق