ما در حال نوشتن حفاظت در برابر حملات DDoS بر روی XDP هستیم. بخش هسته ای

فناوری eXpress Data Path (XDP) اجازه می دهد تا پردازش تصادفی ترافیک در رابط های لینوکس قبل از ورود بسته ها به پشته شبکه هسته انجام شود. کاربرد XDP - محافظت در برابر حملات DDoS (CloudFlare)، فیلترهای پیچیده، جمع آوری آمار (Netflix). برنامه‌های XDP توسط ماشین مجازی eBPF اجرا می‌شوند، بنابراین بسته به نوع فیلتر، محدودیت‌هایی برای کد و عملکرد هسته موجود دارند.

هدف از این مقاله پر کردن کاستی‌های مواد متعدد در XDP است. در مرحله اول، آنها کد آماده ای را ارائه می دهند که بلافاصله ویژگی های XDP را دور می زند: برای تأیید آماده شده است یا برای ایجاد مشکل بسیار ساده است. وقتی سعی می کنید کد خود را از ابتدا بنویسید، نمی دانید با خطاهای معمولی چه کاری انجام دهید. ثانیاً، راه‌های تست محلی XDP بدون VM و سخت‌افزار پوشش داده نمی‌شوند، علی‌رغم اینکه مشکلات خاص خود را دارند. این متن برای برنامه نویسان آشنا به شبکه و لینوکس که به XDP و eBPF علاقه دارند در نظر گرفته شده است.

در این قسمت، نحوه مونتاژ فیلتر XDP و نحوه آزمایش آن را به تفصیل خواهیم فهمید، سپس یک نسخه ساده از مکانیسم شناخته شده کوکی SYN را در سطح پردازش بسته می نویسیم. ما هنوز یک "لیست سفید" ایجاد نمی کنیم
مشتریان تأیید شده، شمارنده نگه دارید و فیلتر را مدیریت کنید - سیاهههای مربوط به اندازه کافی.

ما به زبان C خواهیم نوشت - شیک نیست، اما عملی است. تمامی کدها از طریق لینک در انتها در GitHub در دسترس هستند و طبق مراحل توضیح داده شده در مقاله به commit تقسیم می شوند.

سلب مسئولیت. در طول این مقاله، من یک راه حل کوچک برای جلوگیری از حملات DDoS ایجاد خواهم کرد، زیرا این یک کار واقعی برای XDP و حوزه تخصص من است. با این حال، هدف اصلی درک فناوری است؛ این راهنمای ایجاد حفاظت آماده نیست. کد آموزش بهینه نیست و برخی نکات ظریف را حذف کرده است.

بررسی اجمالی XDP

من فقط نکات کلیدی را بیان می کنم تا اسناد و مقالات موجود تکراری نشود.

بنابراین، کد فیلتر در هسته بارگذاری می شود. بسته های ورودی به فیلتر منتقل می شوند. در نتیجه، فیلتر باید تصمیم بگیرد: بسته را به هسته (XDP_PASS، بسته را رها کنید (XDP_DROP) یا آن را برگردانید (XDP_TX). فیلتر می تواند بسته را تغییر دهد، این به ویژه برای XDP_TX. همچنین می توانید برنامه را لغو کنید (XDP_ABORTED) و بسته را بازنشانی کنید، اما این مشابه است assert(0) - برای رفع اشکال

ماشین مجازی eBPF (فیلتر بسته توسعه یافته برکلی) عمداً ساده ساخته شده است تا هسته بتواند بررسی کند که کد حلقه نمی زند و به حافظه افراد دیگر آسیب نمی رساند. محدودیت ها و چک های تجمعی:

  • حلقه ها (به عقب) ممنوع است.
  • یک پشته برای داده ها وجود دارد، اما هیچ توابعی وجود ندارد (همه توابع C باید درون خطی باشند).
  • دسترسی به حافظه خارج از پشته و بافر بسته ممنوع است.
  • اندازه کد محدود است، اما در عمل این خیلی مهم نیست.
  • فقط فراخوانی توابع هسته ویژه (دستیارهای eBPF) مجاز است.

طراحی و نصب فیلتر به صورت زیر است:

  1. کد منبع (مثلا kernel.c) به شیء (kernel.o) برای معماری ماشین مجازی eBPF. از اکتبر 2019، تلفیقی برای eBPF توسط Clang پشتیبانی می شود و در GCC 10.1 وعده داده شده است.
  2. اگر این کد شی شامل فراخوانی‌های ساختارهای هسته (مثلاً جداول و شمارنده‌ها) باشد، شناسه‌های آن‌ها با صفر جایگزین می‌شوند، به این معنی که چنین کدی قابل اجرا نیست. قبل از بارگذاری در هسته، باید این صفرها را با شناسه اشیاء خاص که از طریق فراخوانی هسته ایجاد شده اند جایگزین کنید (کد را پیوند دهید). شما می توانید این کار را با ابزارهای خارجی انجام دهید، یا می توانید برنامه ای بنویسید که یک فیلتر خاص را لینک و بارگذاری کند.
  3. هسته برنامه بارگذاری شده را تأیید می کند. عدم وجود چرخه و عدم تجاوز از مرزهای بسته و پشته بررسی می شود. اگر تأیید کننده نتواند درستی کد را ثابت کند، برنامه رد می شود - باید بتوانید او را راضی کنید.
  4. پس از تأیید موفقیت آمیز، هسته کد شی معماری eBPF را در کد ماشین برای معماری سیستم (در زمان مقرر) کامپایل می کند.
  5. برنامه به رابط متصل می شود و پردازش بسته ها را آغاز می کند.

از آنجایی که XDP در هسته اجرا می شود، اشکال زدایی با استفاده از گزارش های ردیابی و در واقع بسته هایی که برنامه فیلتر یا تولید می کند انجام می شود. با این حال، eBPF تضمین می کند که کد دانلود شده برای سیستم ایمن است، بنابراین می توانید XDP را مستقیماً در لینوکس محلی خود آزمایش کنید.

آماده سازی محیط

مجلس

Clang نمی تواند به طور مستقیم کد شی را برای معماری eBPF تولید کند، بنابراین فرآیند شامل دو مرحله است:

  1. کامپایل کد C به بایت کد LLVM (clang -emit-llvm).
  2. تبدیل بایت کد به کد شیء eBPF (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

Makefile برای Arch Linux (هسته 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 (هسته 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 های خود باشند تا بررسی کنیم برنامه های معمولی چگونه با فیلتر ما کار می کنند.

دستگاه‌هایی از نوع 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) ترافیک ورودی ارسال خواهد شد. با این حال، یک مشکل وجود دارد: اینترفیس ها روی یک دستگاه هستند و لینوکس ترافیک را از طریق دیگری به یکی از آنها ارسال نمی کند. شما می توانید این را با قوانین دشوار حل کنید iptables، اما آنها باید بسته ها را تغییر دهند که برای اشکال زدایی ناخوشایند است. بهتر است از فضای نام شبکه (از این پس netns) استفاده کنید.

فضای نام شبکه شامل مجموعه‌ای از رابط‌ها، جداول مسیریابی و قوانین NetFilter است که از اشیاء مشابه در شبکه‌های دیگر جدا شده‌اند. هر فرآیند در یک فضای نام اجرا می شود و فقط به اشیاء آن شبکه دسترسی دارد. به طور پیش فرض، سیستم یک فضای نام شبکه واحد برای همه اشیا دارد، بنابراین می توانید در لینوکس کار کنید و از شبکه ها اطلاعی نداشته باشید.

بیایید یک فضای نام جدید ایجاد کنیم xdp-test و آن را به آنجا منتقل کنید xdp-remote.

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

سپس فرآیند در حال اجرا است xdp-test، "نخواهم دید" xdp-local (به طور پیش فرض در net ها باقی می ماند) و هنگام ارسال بسته به 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;
   }

خروجی به کانال ردیابی هسته می رود که باید فعال شود:

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 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 اشاره دارد که ذخیره نشده است؟ پس از همه، یک مهاجم همچنین می تواند ACK های جعلی تولید کند. هدف کوکی SYN رمزگذاری آن است seqnum پارامترهای اتصال به عنوان هش از آدرس ها، پورت ها و تغییر نمک. اگر ACK موفق شد قبل از تغییر نمک وارد شود، می توانید دوباره هش را محاسبه کرده و آن را با acknum. ساختن acknum مهاجم نمی تواند، زیرا نمک شامل راز است، و به دلیل کانال محدود، زمانی برای مرتب کردن آن نخواهد داشت.

کوکی SYN مدت‌هاست که در هسته لینوکس پیاده‌سازی شده است و حتی اگر SYN خیلی سریع و انبوه برسد، می‌تواند به‌طور خودکار فعال شود.

برنامه آموزشی در مورد TCP handshake

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. مشتری با یک بسته ACK به SYNACK پاسخ می دهد، جایی که 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 handshake

تجزیه بسته و تأیید کد

ما به ساختارهای هدر شبکه نیاز داریم: اترنت (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) و یک disassembler وجود دارد که خطوط کد منبع را نشان می دهد:

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 این مشکل را نداشت. این امکان وجود دارد که کامپایلر به یک متغیر محلی متفاوت از یک فیلد دسترسی داشته باشد. اخلاقیات داستان: ساده کردن کد می تواند در مواقعی که تودرتو زیاد است کمک کند.

بعدی بررسی های معمول طول برای شکوه تأیید کننده است. O 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() - یک ماکرو که ذاتی Clang را پنهان می کند.

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() با توجه به RFC 32، یک جمع 16 بیتی از کلمات 791 بیتی ایجاد می کند.

تایید دست دادن TCP

فیلتر به درستی با آن ارتباط برقرار می کند netcat، ACK نهایی را از دست داد، که لینوکس با یک بسته RST به آن پاسخ داد، زیرا پشته شبکه SYN را دریافت نکرد - به SYNACK تبدیل شد و برگردانده شد - و از نقطه نظر سیستم عامل، بسته ای رسید که به open مربوط نمی شد. اتصالات

$ 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 به وضوح فراتر از محدوده این مقاله است.

برای TODO های جدید مرتبط با ارتباطات خارجی معرفی شده است:

  • برنامه 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

در حالی که هیچ لیستی از 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 و سایر گزینه های بای پس هسته. از طرف دیگر، XDP به شما امکان می دهد منطق کاملاً پیچیده ای را پیاده سازی کنید، که علاوه بر این، به راحتی بدون وقفه در پردازش ترافیک به روز می شود. تأیید کننده مشکلات بزرگی ایجاد نمی کند؛ شخصاً برای بخش هایی از کد فضای کاربران این کار را رد نمی کنم.

در قسمت دوم، اگر موضوع جالب باشد، جدول کلاینت های تایید شده و قطع ارتباط را تکمیل می کنیم، شمارنده ها را پیاده سازی می کنیم و یک ابزار userspace برای مدیریت فیلتر می نویسیم.

لینک ها:

منبع: www.habr.com

اضافه کردن نظر