ہم XDP پر DDoS حملوں کے خلاف تحفظ لکھتے ہیں۔ جوہری حصہ

ایکسپریس ڈیٹا پاتھ (XDP) ٹیکنالوجی لینکس انٹرفیس پر پیکٹ کے کرنل نیٹ ورک اسٹیک میں داخل ہونے سے پہلے بے ترتیب ٹریفک پروسیسنگ کی اجازت دیتی ہے۔ XDP کا اطلاق - DDoS حملوں کے خلاف تحفظ (CloudFlare)، پیچیدہ فلٹرز، شماریات کا مجموعہ (Netflix)۔ XDP پروگراموں کو eBPF ورچوئل مشین کے ذریعے عمل میں لایا جاتا ہے، اس لیے فلٹر کی قسم کے لحاظ سے ان کے کوڈ اور دستیاب کرنل فنکشنز دونوں پر پابندیاں ہیں۔

مضمون کا مقصد XDP پر متعدد مواد کی خامیوں کو پُر کرنا ہے۔ سب سے پہلے، وہ ریڈی میڈ کوڈ فراہم کرتے ہیں جو فوری طور پر XDP کی خصوصیات کو نظرانداز کرتا ہے: یہ تصدیق کے لیے تیار کیا گیا ہے یا مسائل پیدا کرنے کے لیے بہت آسان ہے۔ جب آپ شروع سے اپنا کوڈ لکھنے کی کوشش کرتے ہیں، تو آپ کو اندازہ نہیں ہوتا کہ عام غلطیوں کا کیا کرنا ہے۔ دوم، VM اور ہارڈ ویئر کے بغیر مقامی طور پر XDP کو جانچنے کے طریقے شامل نہیں ہیں، اس حقیقت کے باوجود کہ ان کے اپنے نقصانات ہیں۔ متن کا مقصد نیٹ ورکنگ اور لینکس سے واقف پروگرامرز کے لیے ہے جو XDP اور eBPF میں دلچسپی رکھتے ہیں۔

اس حصے میں، ہم تفصیل سے سمجھیں گے کہ XDP فلٹر کو کیسے جمع کیا جاتا ہے اور اسے کیسے ٹیسٹ کیا جاتا ہے، پھر ہم پیکٹ پروسیسنگ کی سطح پر معروف SYN کوکیز میکانزم کا ایک سادہ ورژن لکھیں گے۔ ہم ابھی "وائٹ لسٹ" نہیں بنائیں گے۔
تصدیق شدہ کلائنٹس، کاؤنٹر رکھیں اور فلٹر کا انتظام کریں - کافی لاگ۔

ہم C میں لکھیں گے - یہ فیشن نہیں ہے، لیکن یہ عملی ہے۔ تمام کوڈ GitHub پر آخر میں لنک کے ذریعے دستیاب ہے اور مضمون میں بیان کردہ مراحل کے مطابق کمٹ میں تقسیم کیا گیا ہے۔

ڈس کلیمر اس مضمون کے دوران، میں DDoS حملوں سے بچنے کے لیے ایک چھوٹا سا حل تیار کروں گا، کیونکہ یہ XDP اور میری مہارت کے شعبے کے لیے ایک حقیقت پسندانہ کام ہے۔ تاہم، بنیادی مقصد ٹیکنالوجی کو سمجھنا ہے؛ یہ ریڈی میڈ پروٹیکشن بنانے کے لیے گائیڈ نہیں ہے۔ ٹیوٹوریل کوڈ کو بہتر نہیں بنایا گیا ہے اور کچھ باریکیوں کو چھوڑ دیا گیا ہے۔

XDP کا مختصر جائزہ

میں صرف اہم نکات کا خاکہ پیش کروں گا تاکہ دستاویزات اور موجودہ مضامین کو نقل نہ کیا جائے۔

لہذا، فلٹر کوڈ دانا میں بھری ہوئی ہے۔ آنے والے پیکٹ فلٹر میں بھیجے جاتے ہیں۔ نتیجے کے طور پر، فلٹر کو فیصلہ کرنا ہوگا: پیکٹ کو دانا میں منتقل کریں (XDP_PASS)، ڈراپ پیکٹ (XDP_DROP) یا اسے واپس بھیج دیں (XDP_TX)۔ فلٹر پیکج کو تبدیل کر سکتا ہے، یہ خاص طور پر درست ہے۔ XDP_TX. آپ پروگرام کو بھی روک سکتے ہیں (XDP_ABORTED) اور پیکیج کو دوبارہ ترتیب دیں، لیکن یہ ایک جیسا ہے۔ assert(0) - ڈیبگنگ کے لیے۔

eBPF (توسیع شدہ برکلے پیکٹ فلٹر) ورچوئل مشین کو جان بوجھ کر آسان بنایا گیا ہے تاکہ دانا چیک کر سکے کہ کوڈ لوپ نہیں کرتا اور دوسرے لوگوں کی یادداشت کو نقصان نہیں پہنچاتا۔ مجموعی پابندیاں اور چیکس:

  • لوپس (پیچھے کی طرف) ممنوع ہیں۔
  • ڈیٹا کے لیے ایک اسٹیک ہے، لیکن کوئی فنکشن نہیں ہے (تمام سی فنکشنز کو ان لائن ہونا چاہیے)۔
  • اسٹیک اور پیکٹ بفر کے باہر میموری تک رسائی ممنوع ہے۔
  • کوڈ کا سائز محدود ہے، لیکن عملی طور پر یہ بہت اہم نہیں ہے۔
  • صرف خصوصی کرنل فنکشنز (eBPF مددگار) کو کال کرنے کی اجازت ہے۔

فلٹر کو ڈیزائن اور انسٹال کرنا اس طرح لگتا ہے:

  1. ماخذ کوڈ (مثال کے طور پر kernel.c) کو آبجیکٹ میں مرتب کیا گیا ہے (kernel.o) eBPF ورچوئل مشین فن تعمیر کے لیے۔ اکتوبر 2019 تک، eBPF کی تالیف کو Clang کے ذریعے تعاون حاصل ہے اور GCC 10.1 میں وعدہ کیا گیا ہے۔
  2. اگر اس آبجیکٹ کوڈ میں کرنل ڈھانچے (مثال کے طور پر، ٹیبلز اور کاؤنٹرز) کی کالز شامل ہیں، تو ان کی IDs کو زیرو سے بدل دیا جاتا ہے، جس کا مطلب ہے کہ اس طرح کے کوڈ پر عمل نہیں کیا جا سکتا۔ کرنل میں لوڈ کرنے سے پہلے، آپ کو کرنل کالز کے ذریعے بنائی گئی مخصوص اشیاء کی IDs کے ساتھ ان زیرو کو تبدیل کرنے کی ضرورت ہے (کوڈ کو لنک کریں)۔ آپ یہ بیرونی افادیت کے ساتھ کر سکتے ہیں، یا آپ ایک پروگرام لکھ سکتے ہیں جو ایک مخصوص فلٹر کو لنک اور لوڈ کرے گا۔
  3. دانا بھری ہوئی پروگرام کی تصدیق کرتا ہے۔ سائیکلوں کی عدم موجودگی اور پیکٹ اور اسٹیک کی حدود سے تجاوز کرنے میں ناکامی کی جانچ کی جاتی ہے۔ اگر تصدیق کنندہ یہ ثابت نہیں کر سکتا کہ کوڈ درست ہے، تو پروگرام کو مسترد کر دیا جاتا ہے - آپ کو اسے خوش کرنے کے قابل ہونا چاہیے۔
  4. کامیاب تصدیق کے بعد، کرنل سسٹم فن تعمیر کے لیے eBPF آرکیٹیکچر آبجیکٹ کوڈ کو مشین کوڈ میں مرتب کرتا ہے (صرف وقت پر)۔
  5. پروگرام انٹرفیس سے منسلک ہوتا ہے اور پیکٹوں پر کارروائی شروع کرتا ہے۔

چونکہ XDP دانا میں چلتا ہے، اس لیے ڈیبگنگ ٹریس لاگز اور درحقیقت ان پیکٹوں کے ذریعے کی جاتی ہے جنہیں پروگرام فلٹر کرتا ہے یا تیار کرتا ہے۔ تاہم، eBPF اس بات کو یقینی بناتا ہے کہ ڈاؤن لوڈ کردہ کوڈ سسٹم کے لیے محفوظ ہے، لہذا آپ اپنے مقامی لینکس پر براہ راست XDP کے ساتھ تجربہ کر سکتے ہیں۔

ماحول کی تیاری

اسمبلی

بجنا براہ راست ای بی پی ایف فن تعمیر کے لیے آبجیکٹ کوڈ نہیں بنا سکتا، اس لیے یہ عمل دو مراحل پر مشتمل ہے:

  1. سی کوڈ کو LLVM بائٹ کوڈ میں مرتب کریں (clang -emit-llvm).
  2. بائٹ کوڈ کو ای بی پی ایف آبجیکٹ کوڈ میں تبدیل کریں (llc -march=bpf -filetype=obj).

فلٹر لکھتے وقت، معاون فنکشنز اور میکروز والی چند فائلیں کارآمد ہوں گی۔ دانا ٹیسٹ سے. یہ ضروری ہے کہ وہ کرنل ورژن سے مماثل ہوں (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

میک فائل برائے آرک لینکس (کرنل 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 کرنل ہیڈرز کے راستے پر مشتمل ہے، 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__ اس کا مطلب یہ ہے کہ UAPI (userspace API) ہیڈر کی تعریف کرنل کوڈ کے لیے کی گئی ہے، کیونکہ فلٹر کو دانا میں عمل میں لایا جاتا ہے۔

اسٹیک تحفظ کو غیر فعال کیا جا سکتا ہے (-fno-stack-protector)، کیونکہ eBPF کوڈ کی تصدیق کنندہ اب بھی اسٹیک سے باہر کی خلاف ورزیوں کی جانچ کرتا ہے۔ یہ فوری طور پر اصلاح کو آن کرنے کے قابل ہے، کیونکہ eBPF بائیک کوڈ کا سائز محدود ہے۔

آئیے ایک فلٹر کے ساتھ شروع کریں جو تمام پیکٹوں کو پاس کرتا ہے اور کچھ نہیں کرتا:

#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. اب اسے کہاں آزمائیں؟

ٹیسٹ سٹینڈ

اسٹینڈ میں دو انٹرفیس شامل ہونے چاہئیں: جس پر ایک فلٹر ہوگا اور جس سے پیکٹ بھیجے جائیں گے۔ یہ چیک کرنے کے لیے کہ باقاعدہ ایپلیکیشنز ہمارے فلٹر کے ساتھ کیسے کام کرتی ہیں، ان کے اپنے IP کے ساتھ مکمل لینکس ڈیوائسز ہونے چاہئیں۔

ویتھ (ورچوئل ایتھرنیٹ) قسم کے آلات ہمارے لیے موزوں ہیں: یہ ورچوئل نیٹ ورک انٹرفیس کا ایک جوڑا ہے جو براہ راست ایک دوسرے سے "منسلک" ہیں۔ آپ انہیں اس طرح بنا سکتے ہیں (اس سیکشن میں تمام کمانڈز 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) آنے والی ٹریفک بھیجی جائے گی۔ تاہم، ایک مسئلہ ہے: انٹرفیس ایک ہی مشین پر ہیں، اور لینکس ان میں سے ایک کو دوسری کے ذریعے ٹریفک نہیں بھیجے گا۔ آپ اسے مشکل قوانین سے حل کر سکتے ہیں۔ iptables، لیکن انہیں پیکجز کو تبدیل کرنا پڑے گا، جو ڈیبگنگ کے لیے تکلیف دہ ہے۔ نیٹ ورک کے نام کی جگہیں استعمال کرنا بہتر ہے (اس کے بعد نیٹ این ایس)۔

نیٹ ورک کے نام کی جگہ میں انٹرفیس، روٹنگ ٹیبلز، اور نیٹ فلٹر قواعد کا ایک سیٹ ہوتا ہے جو دوسرے نیٹ این ایس میں ملتے جلتے اشیاء سے الگ تھلگ ہوتے ہیں۔ ہر عمل ایک نام کی جگہ پر چلتا ہے اور اسے صرف اس نیٹ کی اشیاء تک رسائی حاصل ہوتی ہے۔ پہلے سے طے شدہ طور پر، سسٹم میں تمام اشیاء کے لیے ایک ہی نیٹ ورک نام کی جگہ ہوتی ہے، لہذا آپ لینکس میں کام کر سکتے ہیں اور netns کے بارے میں نہیں جانتے۔

آئیے ایک نیا نام کی جگہ بنائیں xdp-test اور اسے وہاں منتقل کریں xdp-remote.

ip netns add xdp-test
ip link set dev xdp-remote netns xdp-test

پھر عمل جاری ہے۔ xdp-test، "دیکھیں گے" نہیں xdp-local (یہ پہلے سے طے شدہ طور پر نیٹ این ایس میں رہے گا) اور جب پیکٹ 192.0.2.1 پر بھیجیں گے تو یہ اس سے گزر جائے گا۔ xdp-remoteکیونکہ یہ 192.0.2.0/24 پر واحد انٹرفیس ہے جو اس عمل تک قابل رسائی ہے۔ یہ بھی مخالف سمت میں کام کرتا ہے۔

netns کے درمیان منتقل ہونے پر، انٹرفیس نیچے جاتا ہے اور اپنا پتہ کھو دیتا ہے۔ 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

اس میں شیل لانچ کرنا آسان ہے۔ 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;
   }

آؤٹ پٹ کرنل ٹریس چینل پر جاتا ہے، جس کو فعال کرنے کی ضرورت ہے:

echo -n 1 | sudo tee /sys/kernel/debug/tracing/options/trace_printk

پیغام کا دھاگہ دیکھیں:

cat /sys/kernel/debug/tracing/trace_pipe

یہ دونوں کمانڈز کال کرتے ہیں۔ sudo ./stand log.

پنگ کو اب اس طرح کے پیغامات کو متحرک کرنا چاہئے:

<...>-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 ایکو جواب دکھانا بند کر دینا چاہیے۔ لیکن یہ ظاہر نہیں ہوتا ہے۔ یہ کام کے لئے پتہ چلتا ہے XDP_TX پر پروگرام میں xdp-local ضروری ہےجوڑے کے انٹرفیس پر xdp-remote ایک پروگرام بھی تفویض کیا گیا تھا، چاہے وہ خالی ہی کیوں نہ ہو، اور اسے اٹھایا گیا۔

مجھے یہ کیسے معلوم ہوا؟

دانا میں ایک پیکیج کا راستہ ٹریس کریں۔ پرف ایونٹس میکانزم، ویسے، ایک ہی ورچوئل مشین کا استعمال کرنے کی اجازت دیتا ہے، یعنی، 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

اگر اس کے بجائے صرف اے آر پی دکھائے جاتے ہیں، تو آپ کو فلٹرز کو ہٹانے کی ضرورت ہے (یہ کرتا ہے۔ sudo ./stand detach)، جانے دو ping، پھر فلٹرز سیٹ کریں اور دوبارہ کوشش کریں۔ مسئلہ یہ ہے کہ فلٹر XDP_TX ARP اور اگر اسٹیک دونوں پر درست ہے۔
نام کی جگہیں xdp-test MAC ایڈریس 192.0.2.1 کو "بھولنے" میں کامیاب، یہ اس آئی پی کو حل نہیں کر سکے گا۔

مسئلہ کی تشکیل

آئیے بیان کردہ کام کی طرف بڑھتے ہیں: XDP پر SYN کوکیز کا طریقہ کار لکھیں۔

SYN سیلاب ایک مقبول DDoS حملہ ہے، جس کا خلاصہ درج ذیل ہے۔ جب کنکشن قائم ہو جاتا ہے (TCP ہینڈ شیک)، سرور ایک SYN وصول کرتا ہے، مستقبل کے کنکشن کے لیے وسائل مختص کرتا ہے، SYNACK پیکٹ کے ساتھ جواب دیتا ہے اور ACK کا انتظار کرتا ہے۔ حملہ آور ایک کثیر ہزار مضبوط بوٹ نیٹ میں ہر میزبان کے جعلی پتوں سے فی سیکنڈ ہزاروں SYN پیکٹ بھیجتا ہے۔ سرور کو پیکٹ کی آمد پر فوری طور پر وسائل مختص کرنے پر مجبور کیا جاتا ہے، لیکن ایک بڑی مدت کے بعد انہیں جاری کرتا ہے، نتیجتاً، میموری یا حدود ختم ہو جاتی ہیں، نئے کنکشن قبول نہیں کیے جاتے، اور سروس دستیاب نہیں ہوتی۔

اگر آپ SYN پیکٹ کی بنیاد پر وسائل مختص نہیں کرتے ہیں، لیکن صرف SYNACK پیکٹ کے ساتھ جواب دیتے ہیں، تو پھر سرور کیسے سمجھ سکتا ہے کہ ACK پیکٹ جو بعد میں آیا ہے اس سے مراد SYN پیکٹ ہے جو محفوظ نہیں کیا گیا تھا؟ آخرکار، حملہ آور جعلی ACKs بھی بنا سکتا ہے۔ SYN کوکی کا نقطہ اسے انکوڈ کرنا ہے۔ seqnum کنکشن کے پیرامیٹرز کو پتوں، بندرگاہوں اور تبدیل کرنے والے نمک کے ہیش کے طور پر۔ اگر ACK نمک کو تبدیل کرنے سے پہلے پہنچنے میں کامیاب ہو گیا، تو آپ دوبارہ ہیش کا حساب لگا سکتے ہیں اور اس کا موازنہ کر سکتے ہیں۔ acknum. جعلسازی acknum حملہ آور نہیں کر سکتا، کیونکہ نمک میں راز شامل ہوتا ہے، اور ایک محدود چینل کی وجہ سے اسے چھانٹنے کا وقت نہیں ملے گا۔

SYN کوکی کو لینکس کرنل میں طویل عرصے سے لاگو کیا گیا ہے اور یہاں تک کہ اگر SYNs بہت جلدی اور اجتماعی طور پر پہنچ جائیں تو اسے خود بخود فعال کیا جا سکتا ہے۔

TCP مصافحہ پر تعلیمی پروگرام

TCP بائٹس کے ایک سلسلے کے طور پر ڈیٹا ٹرانسمیشن فراہم کرتا ہے، مثال کے طور پر، HTTP درخواستیں TCP پر منتقل کی جاتی ہیں۔ ندی کو پیکٹوں میں ٹکڑوں میں منتقل کیا جاتا ہے۔ تمام TCP پیکٹوں میں منطقی جھنڈے اور 32 بٹ ترتیب نمبر ہوتے ہیں:

  • جھنڈوں کا مجموعہ کسی خاص پیکیج کے کردار کا تعین کرتا ہے۔ SYN جھنڈا اشارہ کرتا ہے کہ یہ کنکشن پر بھیجنے والے کا پہلا پیکٹ ہے۔ ACK پرچم کا مطلب ہے کہ بھیجنے والے نے بائٹ تک تمام کنکشن ڈیٹا حاصل کر لیا ہے۔ acknum. ایک پیکٹ میں کئی جھنڈے ہوسکتے ہیں اور ان کے مجموعہ سے بلایا جاتا ہے، مثال کے طور پر، ایک SYNACK پیکٹ۔

  • ترتیب نمبر (seqnum) اس پیکٹ میں منتقل ہونے والے پہلے بائٹ کے لیے ڈیٹا اسٹریم میں آفسیٹ کی وضاحت کرتا ہے۔ مثال کے طور پر، اگر ڈیٹا کے X بائٹس والے پہلے پیکٹ میں یہ نمبر N تھا، تو نئے ڈیٹا کے ساتھ اگلے پیکٹ میں یہ N+X ہوگا۔ کنکشن کے آغاز میں، ہر طرف تصادفی طور پر اس نمبر کا انتخاب کرتا ہے۔

  • ایکنولجمنٹ نمبر (اکنم) - سیکنم کے طور پر ایک ہی آفسیٹ، لیکن یہ منتقل کیے جانے والے بائٹ کی تعداد کا تعین نہیں کرتا ہے، لیکن وصول کنندہ کی طرف سے پہلے بائٹ کا نمبر، جسے بھیجنے والے نے نہیں دیکھا۔

کنکشن کے آغاز میں، فریقین کا متفق ہونا ضروری ہے۔ 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.   (**)
В остальных случаях сбросить пакет.

ایک (*) وہ پوائنٹس جہاں آپ کو سسٹم کی حالت کو منظم کرنے کی ضرورت ہے وہ نشان زد ہیں - پہلے مرحلے پر آپ ان کے بغیر صرف ایک SYN کوکی کی نسل کے ساتھ TCP ہینڈ شیک کو بطور سیکنم لاگو کر سکتے ہیں۔

موقع پر (**)جب کہ ہمارے پاس میز نہیں ہے، ہم پیکٹ کو چھوڑ دیں گے۔

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;

ٹی سی پی پورٹس، آئی پی ایڈریس اور میک ایڈریسز کو تبدیل کریں۔ معیاری لائبریری 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 بائٹس تک۔ آپ تکرار کی ایک مقررہ تعداد کے ساتھ ایک لوپ بنا سکتے ہیں، جو جلد ختم ہو سکتی ہے۔

میں نوٹ کرتا ہوں کہ وہاں ہے۔ آر ایف سی 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() RFC 32 کے مطابق، 16-بٹ الفاظ کے 791-bit مجموعے سے چیکسم بناتا ہے۔

TCP مصافحہ کی تصدیق

فلٹر صحیح طریقے سے کنکشن قائم کرتا ہے۔ netcat, حتمی ACK غائب ہے، جس کا لینکس نے RST پیکٹ کے ساتھ جواب دیا، چونکہ نیٹ ورک اسٹیک کو SYN نہیں ملا تھا - اسے SYNACK میں تبدیل کر کے واپس بھیج دیا گیا تھا - اور OS کے نقطہ نظر سے، ایک پیکٹ آیا جو کھلنے سے متعلق نہیں تھا۔ کنکشنز

$ 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 نقطہ نظر سے، توثیق خود معمولی ہے۔ کیلکولیشن الگورتھم قدیم ہے اور ممکنہ طور پر ایک نفیس حملہ آور کے لیے خطرناک ہے۔ مثال کے طور پر لینکس کرنل کرپٹوگرافک SipHash استعمال کرتا ہے، لیکن XDP کے لیے اس کا نفاذ واضح طور پر اس مضمون کے دائرہ کار سے باہر ہے۔

بیرونی مواصلات سے متعلق نئے TODOs کے لیے متعارف کرایا گیا:

  • XDP پروگرام ذخیرہ نہیں کر سکتا cookie_seed (نمک کا خفیہ حصہ) ایک عالمی متغیر میں، آپ کو دانا میں ذخیرہ کرنے کی ضرورت ہے، جس کی قیمت وقتاً فوقتاً ایک قابل اعتماد جنریٹر سے اپ ڈیٹ ہوتی رہے گی۔

  • اگر 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

اگرچہ تصدیق شدہ IPs کی کوئی فہرست نہیں ہے، خود 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 آپ کو کافی پیچیدہ منطق کو لاگو کرنے کی اجازت دیتا ہے، جس کے علاوہ، ٹریفک پروسیسنگ میں رکاوٹ کے بغیر اپ ڈیٹ کرنا آسان ہے۔ تصدیق کنندہ ذاتی طور پر بڑی مشکلات پیدا نہیں کرتا ہے، میں یوزر اسپیس کوڈ کے کچھ حصوں کے لیے اس سے انکار نہیں کروں گا۔

دوسرے حصے میں، اگر موضوع دلچسپ ہے، تو ہم تصدیق شدہ کلائنٹس اور منقطع ہونے کی میز کو مکمل کریں گے، کاؤنٹرز کو نافذ کریں گے اور فلٹر کو منظم کرنے کے لیے یوزر اسپیس یوٹیلیٹی لکھیں گے۔

حوالہ جات:

ماخذ: www.habr.com

نیا تبصرہ شامل کریں