| BPF classifier and actions in tc(8) | Linux | BPF classifier and actions in tc(8) |
نام (NAME)
tc-bpf - ردهبند و کنشهای برنامهپذیر BPF برای انضباطهای صف ورودی/خروجی
خلاصه دستور (SYNOPSIS)
ردهبند (فیلتر) یا کنش eBPF:
tc filter ... bpf [ object-file OBJ_FILE ] [
section CLS_NAME ] [ export UDS_FILE ] [ verbose ] [
direct-action | da ] [ skip_hw | skip_sw ] [
police POLICE_SPEC ] [ action ACTION_SPEC ] [ classid
CLASSID ]
tc action ... bpf [ object-file OBJ_FILE ] [ section
CLS_NAME ] [ export UDS_FILE ] [ verbose ]
ردهبند (فیلتر) یا کنش cBPF:
tc filter ... bpf [ bytecode-file BPF_FILE |
bytecode BPF_BYTECODE ] [ police POLICE_SPEC ] [ action
ACTION_SPEC ] [ classid CLASSID ]
tc action ... bpf [ bytecode-file BPF_FILE | bytecode
BPF_BYTECODE ]
توضیحات (DESCRIPTION)
فیلتر بسته برکلی گسترشیافته ( eBPF ) و فیلتر بسته برکلی کلاسیک (که در اصل با نام BPF شناخته میشود و برای تمایز بهتر در اینجا با نام cBPF به آن اشاره میشود) هر دو بهعنوان ردهبندها و کنشهای کاملاً برنامهپذیر و بسیار کارآمد در دسترس هستند. هر دوی آنها یک مجموعه دستورالعمل کمینه برای پیادهسازی برنامههای کوچک ارائه میدهند که میتوانند با ایمنی بالا در هسته بارگذاری شده و درون یک ماشین مجازی کوچک در فضای هسته اجرا شوند. یک اعتبارسنج درون هسته تضمین میکند که برنامه مشخصشده همواره خاتمه مییابد و نه از کار میافتد و نه دادهای را از هسته به بیرون درز میدهد.
در لینوکس، عموماً در نظر گرفته میشود که eBPF جانشین cBPF است. هسته بهصورت درونی عبارات cBPF را به عبارات eBPF تبدیل کرده و عبارات دومی را اجرا میکند. اجرای آنها میتواند در یک مفسر صورت گیرد یا در زمان راهاندازی، بهصورت درجا (JIT) کامپایل شوند تا بهصورت کد ماشین بومی اجرا گردند.
در حال حاضر، کامپایلر JIT برای eBPF روی معماریهای زیر در دسترس است:
- x86_64 (از لینوکس 3.18)
- arm64 (از لینوکس 3.18)
- s390 (از لینوکس 4.1)
- ppc64 (از لینوکس 4.8)
- sparc64 (از لینوکس 4.12)
- mips64 (از لینوکس 4.13)
- arm32 (از لینوکس 4.14)
- x86_32 (از لینوکس 4.18)
در حالی که معماریهای زیر دارای cBPF هستند، اما (هنوز) به پشتیبانی از eBPF JIT تغییر نیافتهاند:
- ppc32
- sparc32
- mips32
مجموعه دستورالعملهای eBPF دارای اصول اساسی مشابهی با مجموعه دستورالعملهای cBPF است، با این حال برای تقلید بهتر از مجموعههای دستورالعمل بومی و با هدف دستیابی به کارایی زمان اجرای بهتر، نزدیکتر به معماری زیرین مدلسازی شده است. این مجموعه طوری طراحی شده است که با نگاشت یکبهیک کامپایل JIT شود، که این امر همچنین امکان تولید کد بهینهشده eBPF توسط کامپایلرها را از طریق یک بکاند eBPF فراهم میسازد، کدی که تقریباً به همان سرعت کد کامپایلشده بومی اجرا میشود. با توجه به اینکه LLVM چنین بکاند eBPF را ارائه میدهد، برنامههای eBPF را میتوان به سادگی در زیرمجموعهای از زبان C برنامهنویسی کرد. افزون بر این، زیرساخت eBPF با ساختاری به نام «نگاشتها» (maps) همراه است. نگاشتهای eBPF ذخیرهسازهای کلید/مقدار هستند که بین چندین برنامه eBPF و همچنین بین برنامههای eBPF و برنامههای فضای کاربری به اشتراک گذاشته میشوند.
برای زیرسیستم کنترل ترافیک، ردهبند و کنشهایی که میتوانند به qdiscهای ورودی (ingress) و خروجی (egress) متصل شوند، میتوانند در eBPF یا cBPF نوشته شوند. مزیت این رویکرد نسبت به سایر ردهبندها و کنشها این است که eBPF/cBPF چارچوبی عمومی فراهم میکند، در حالی که کاربران میتوانند موارد استفاده بسیار تخصصی خود را به شکلی کارآمد پیادهسازی کنند. این بدان معناست که ردهبند یا کنش نوشتهشده بدین روش دچار پفکردگی امکانات (feature bloat) نخواهد شد و بنابراین وظیفه خود را با کارایی بسیار بالا به انجام میرساند. این ساختار امکان ردهبندی غیرخطی و حتی ادغام بخش کنش در ردهبندی را فراهم میکند. در ترکیب با ساختار دادههای کارآمد نگاشت eBPF، فضای کاربری میتواند سیاستهای جدیدی مانند classidها را بدون بارگذاری مجدد ردهبند به هسته ارسال کند، یا آمارهایی جمعآوری نماید که در یک نگاشت ثبت شده و از نگاشتی دیگر برای تعادل بار پویای ترافیک بر اساس بار مشخصشده استفاده کند؛ و اینها تنها چند مثال محدود هستند.
پارامترها (PARAMETERS)
object-file
به یک فایل شیء (object file) اشاره میکند که دارای قالب اجرایی و پیوندپذیر (ELF) بوده و شامل کدهای عملیاتی eBPF و تعاریف نگاشت eBPF است. زیرساخت کامپایلر LLVM با clang(1) بهعنوان فرانتاند زبان C، پروژهای است که از صدور فایلهای شیء eBPF قابل ارسال به ردهبند eBPF پشتیبانی میکند (جزئیات بیشتر در بخش مثالها (EXAMPLES) آمده است). این گزینه هنگام بارگذاری یک ردهبند یا کنش eBPF الزامی است.
section
نام بخش ELF در فایل شیء است که ردهبند یا کنش eBPF در آن قرار دارد. بهطور پیشفرض، نام بخش برای ردهبند "classifier" و برای کنش "action" است. با توجه به اینکه یک فایل شیء واحد میتواند شامل چندین ردهبند و کنش باشد، در صورت تفاوت با مقادیر پیشفرض، باید نام بخش متناظر مشخص گردد.
export
به یک فایل سوکت دامنه یونیکس (Unix domain socket) اشاره میکند. در صورتی که فایل شیء eBPF شامل بخشی به نام "maps" حاوی مشخصات نگاشت eBPF نیز باشد، توصیفکنندههای فایل (file descriptors) نگاشت میتوانند از طریق سوکت دامنه یونیکس به یک «عامل» (agent) eBPF تحویل داده شوند که مدیریت تمام توصیفکنندهها را پس از پایان عمر tc بر عهده میگیرد. این عامل میتواند برنامهای جانبی باشد که بخش IPC متناظر برای ایمپورت را پیادهسازی کرده و از آنها برای فراخوانی فراخوان سیستمی bpf(2) جهت خواندن یا بهروزرسانی دادههای نگاشت eBPF از فضای کاربری استفاده میکند؛ برای نمونه جهت مقاصد مانیتورینگ یا اعمال سیاستهای جدید.
verbose
در صورت تنظیم، خروجی اعتبارسنج eBPF را حتی در صورت موفقیتآمیز بودن بارگذاری برنامه eBPF نمایش میدهد. بهطور پیشفرض، لاگ اعتبارسنج تنها هنگام بروز خطا به کاربر نشان داده میشود.
direct-action | da
به ردهبند eBPF دستور میدهد که کنشهای خارجی TC را فراخوانی نکند و در عوض از کدهای بازگشتی کنشهای TC (مانند TC_ACT_OK، TC_ACT_SHOT و غیره) برای ردهبندها استفاده کند.
skip_hw | skip_sw
فلگهای کنترل برونسپاری سختافزاری (hardware offload). بهطور پیشفرض، TC در صورت امکان سعی میکند فیلترها را به سختافزار برونسپاری کند. skip_hw تلاش برای برونسپاری را صراحتاً غیرفعال میکند. skip_sw برونسپاری را اجباری کرده و اجرای برنامه eBPF را در هسته غیرفعال میسازد. اگر برونسپاری سختافزاری امکانپذیر نباشد و این فلگ تنظیم شده باشد، هسته خطا گزارش میدهد و فیلتر اصلاً نصب نخواهد شد.
police
پارامتری اختیاری برای یک ردهبند eBPF/cBPF است که یک پلیس (police) را در tc(1) مشخص میکند که به ردهبند متصل است، برای نمونه، روی یک qdisc ورودی (ingress).
action
پارامتری اختیاری برای یک ردهبند eBPF/cBPF است که یک کنش بعدی را در tc(1) مشخص میکند که به یک ردهبند متصل شده است.
classid
flowid
شناسه کلاس (class identifier) پیشفرض کنترل ترافیک را برای این ردهبند eBPF/cBPF ارائه میدهد. شناسه کلاس پیشفرض همچنین میتواند توسط کد بازگشتی برنامه eBPF/cBPF بازنویسی شود. کد بازگشتی پیشفرض -1 مشخص میکند که شناسه کلاس پیشفرض ارائهشده در اینجا باید استفاده شود. کد بازگشتی 0 در برنامه eBPF/cBPF بدین معناست که هیچ تطابقی رخ نداده است، و کد بازگشتی غیر از این دو مقدار، classid پیشفرض را بازنویسی خواهد کرد. این ویژگی امکان ردهبندی کارآمد و غیرخطی را تنها با یک برنامه منفرد eBPF/cBPF فراهم میسازد، برخلاف داشتن چندین برنامه مجزا برای شناسههای کلاس گوناگون که نیازمند تجزیه مجدد محتویات بسته خواهند بود.
bytecode
تنها برای بارگذاری ردهبند و کنشهای cBPF استفاده میشود. بایتکد cBPF مستقیماً بهصورت یک رشته متنی در قالب 's,c t f k,c t f k,c t f k,...' پاس داده میشود، که در آن s تعداد 4-تاییهای بعدی را نشان میدهد. هر یک از این 4-تاییها شامل اعداد دهدهی c t f k است، که در آن c نشاندهنده کد عملیاتی cBPF، t مقصد آفست پرش در صورت درستی (jump true offset target)، f مقصد آفست پرش در صورت نادرستی (jump false offset target) و k مقدار ثابت/لفظی فوری (immediate constant/literal) است. ابزارهای مختلفی وجود دارند که کد را در این قالب قابل بارگذاری تولید میکنند، برای نمونه، bpf_asm که همراه با درخت سورس هسته لینوکس تحت مسیر tools/net/ عرضه میشود، بنابراین مسلماً انتظار نمیرود که این مورد را به صورت دستی کدنویسی کنید. گزینه bytecode یا bytecode-file هنگام بارگذاری یک ردهبند یا کنش cBPF الزامی است.
bytecode-file
نیز برای بارگذاری یک ردهبند یا کنش cBPF استفاده میشود. این گزینه در عمل همانند bytecode است، با این تفاوت که بایتکد cBPF مستقیماً از طریق خط فرمان پاس داده نمیشود، بلکه درون یک فایل متنی قرار دارد.
مثالها (EXAMPLES)
ابزارهای eBPF (eBPF TOOLING)
یک مثال کامل شامل کد عامل eBPF را میتوان درون بسته سورس iproute2 تحت مسیر زیر یافت: examples/bpf/
بهعنوان پیشنیاز، هسته باید فراخوان سیستمی eBPF یعنی bpf(2) را فعال داشته باشد و ماژولهای هسته cls_bpf و act_bpf را برای زیرسیستم کنترل ترافیک عرضه کند. برای فعالسازی پشتیبانی از eBPF/eBPF JIT، بسته به اینکه معماری مد نظر از کدامیک پشتیبانی میکند:
echo 1 > /proc/sys/net/core/bpf_jit_enable
یک فایل مفروض با زبان C محدودشده میتواند از طریق LLVM بدین صورت کامپایل شود:
clang -O2 -emit-llvm -c bpf.c -o - | llc -march=bpf -filetype=obj -o
bpf.o
فراخوانی
کامپایلر
ممکن است در
آینده
سادهتر
شود،
بنابراین
در حال حاضر
بسیار مفید
است که برای
این ساختار
به نوعی نام
مستعار (alias)
تعریف
کنید، برای
نمونه:
__bcc() {
clang -O2 -emit-llvm -c $1 -o - | llc -march=bpf -filetype=obj -o "`basename $1 .c`.o"
}
alias bcc=__bcc
یک واحد کمینه و مستقل، که روی تمام ترافیک با classid پیشفرض (کد بازگشتی -1) تطبیق مییابد، بدین صورت است:
#include <linux/bpf.h>
#ifndef __section
# define __section(x) __attribute__((section(x), used))
#endif
__section("classifier") int cls_main(struct __sk_buff *skb)
{
return -1;
}
char __license[] __section("license") = "GPL";
مثالهای بیشتر را میتوان در زیربخش برنامهنویسی eBPF (eBPF PROGRAMMING) در ادامه مشاهده کرد، چرا که تمرکز در اینجا بر ابزارها خواهد بود.
میتواند بخشهای مختلف دیگری نیز وجود داشته باشد، برای نمونه برای کنشها. بدین ترتیب، یک فایل شیء در eBPF میتواند شامل چندین نقطه ورود باشد. با این حال، هنگام پیکربندی با tc همواره باید یک نقطه ورود مشخص معین شود. مجوز باید بخشی از کد C محدودشده باشد و نحو رشته مجوز مشابه با ماژولهای هسته لینوکس است. هسته این حق را برای خود محفوظ میدارد که برخی توابع کمکی eBPF را تنها به مجوزهای سازگار با GPL محدود کند، و از این رو ممکن است هنگام رخ دادن چنین عدم تطابقی در مجوز، از بارگذاری برنامه در هسته جلوگیری نماید.
فایل شیء حاصل از کامپایل را میتوان با مجموعه ابزارهای معمول که روی فایلهای شیء عادی نیز کار میکنند بازبینی کرد، برای مثال objdump(1) برای بازبینی هدرهای بخش ELF:
objdump -h bpf.o
[...]
3 classifier 000007f8 0000000000000000 0000000000000000 00000040 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
4 action-mark 00000088 0000000000000000 0000000000000000 00000838 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
5 action-rand 00000098 0000000000000000 0000000000000000 000008c0 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
6 maps 00000030 0000000000000000 0000000000000000 00000958 2**2
CONTENTS, ALLOC, LOAD, DATA
7 license 00000004 0000000000000000 0000000000000000 00000988 2**0
CONTENTS, ALLOC, LOAD, DATA
[...]
افزودن یک ردهبند eBPF از یک فایل شیء که شامل ردهبند در بخش پیشفرض ELF است بسیار ساده است (توجه داشته باشید که به جای "object-file" میتوان از نامهای کوتاه مانند "obj" نیز استفاده کرد):
bcc bpf.c
tc filter add dev em1 parent 1: bpf obj bpf.o flowid 1:1
در صورتی که ردهبند در بخش ELF با نام "mycls" قرار داشته باشد، همان دستور باید بدین صورت فراخوانی شود:
tc filter add dev em1 parent 1: bpf obj bpf.o sec mycls flowid 1:1
استخراج پیکربندی ردهبند، مکان ردهبند را مشخص میکند؛ به بیان دیگر نشان میدهد که از فایل شیء "bpf.o" تحت بخش "mycls" است:
tc filter show dev em1
filter parent 1: protocol all pref 49152 bpf
filter parent 1: protocol all pref 49152 bpf handle 0x1 flowid 1:1
bpf.o:[mycls]
همین برنامه را میتوان برخلاف خروجی، روی سمت qdisc ورودی نیز نصب کرد ...
tc qdisc add dev em1 handle ffff: ingress
tc filter add dev em1 parent ffff: bpf obj bpf.o sec mycls flowid
ffff:1
... و دوباره از آنجا استخراج کرد:
tc filter show dev em1 parent ffff:
filter protocol all pref 49152 bpf
filter protocol all pref 49152 bpf handle 0x1 flowid ffff:1
bpf.o:[mycls]
اتصال ردهبند و کنش در ورودی (ingress) این محدودیت را دارد که فاقد انضباط صفبندی واقعی در زیرساخت خود است. کاری که ورودی میتواند انجام دهد ردهبندی، دستکاری (mangle)، تغییر مسیر یا دور انداختن (drop) بستهها است. زمانی که صفبندی در سمت ورودی لازم باشد، ورودی باید بستهها را به دستگاه ifb تغییر مسیر دهد، در غیر این صورت میتوان از پلیسینگ (policing) استفاده کرد. علاوه بر این، ورودی میتواند بهعنوان یک نقطه دور انداختن زودهنگام بستههای ناخواسته قبل از رسیدن به لایههای بالاتر پشته شبکه، انجام حسابرسی شبکه با نگاشتهای eBPF که میتوانند با خروجی به اشتراک گذاشته شوند، یا نقطه دستکاری و/یا تغییر مسیر زودهنگام به دستگاههای شبکه مختلف استفاده شود.
میتوان چندین کنش و ردهبند eBPF را در یک فایل شیء واحد در بخشهای گوناگون قرار داد. در این صورت، باید نامهای غیرپیشفرض بخشها ارائه شوند، که در این مثال برای هر دو کنش چنین است:
tc filter add dev em1 parent 1: bpf obj bpf.o flowid 1:1 \
action bpf obj bpf.o sec action-mark \
action bpf obj bpf.o sec action-rand ok
مزیت این کار این است که ردهبند و دو کنش در صورت پیادهسازی در برنامهها میتوانند نگاشتهای eBPF را با یکدیگر به اشتراک بگذارند.
بهمنظور دسترسی به نگاشتهای eBPF از فضای کاربری فراتر از دوره حیات راهاندازی tc(8)، مالکیت را میتوان از طریق سوکتهای دامنه یونیکس به یک عامل eBPF منتقل کرد. دو روش برای پیادهسازی این کار وجود دارد:
1) پیادهسازی یک عامل eBPF اختصاصی که راهاندازی سوکت دامنه یونیکس و پیادهسازی پروتکل دیکتهشده توسط tc(8) را بر عهده میگیرد. نمونه کد این مورد را میتوان درون بسته سورس iproute2 تحت مسیر زیر یافت: examples/bpf/
2) استفاده از tc exec برای انتقال توصیفکنندههای فایل نگاشت eBPF از طریق سوکت دامنه یونیکس، و اجرای برنامهای مانند sh(1) . مزیت این رویکرد این است که tc توصیفکنندههای فایل را در متغیرهای محیطی قرار میدهد و در نتیجه آنها را درست مانند توصیفکنندههای فایل stdin، stdout و stderr در دسترس میگذارد؛ بدین معنا که اگر برنامههای کاربردی از درون این شلِ مالک fd اجرا شوند، میتوانند بدون از دست دادن توصیفکنندههای فایل نگاشتهای eBPF خاتمه یافته و دوباره راهاندازی شوند. مثال فراخوانی با ترکیب ردهبند و کنش قبلی:
tc exec bpf imp /tmp/bpf
tc filter add dev em1 parent 1: bpf obj bpf.o exp /tmp/bpf flowid 1:1 \
action bpf obj bpf.o sec action-mark \
action bpf obj bpf.o sec action-rand ok
با فرض اینکه نگاشتهای eBPF بین ردهبند و کنشها به اشتراک گذاشته شدهاند، کافی است آنها را یک بار، مثلاً از درون دستور ردهبند یا کنش صادر (export) کنید. tc تمام توصیفکنندههای فایل نگاشت eBPF را در زمانی که فایل شیء برای نخستین بار تجزیه میشود، راهاندازی خواهد کرد.
هنگامی که یک شل اجرا شد، محیط حاوی چند متغیر مرتبط با eBPF خواهد بود. BPF_NUM_MAPS تعداد کل نگاشتهایی را که از طریق سوکت دامنه یونیکس منتقل شدهاند ارائه میدهد. مقدار BPF_MAP<X> شماره توصیفکننده فایلی است که میتوان در برنامههای عامل eBPF به آن دسترسی داشت؛ به عبارت دیگر، میتوان مستقیماً از آن بهعنوان مقدار توصیفکننده فایل برای فراخوان سیستمی bpf(2) جهت بازیابی یا تغییر مقادیر نگاشت eBPF استفاده کرد. <X> شناسه نگاشت eBPF را نشان میدهد. این شناسه متناظر با عضو id از struct bpf_elf_map در مشخصات نگاشت eBPF در tc است.
محیط در این مثال به شکل زیر است:
sh# env | grep BPF
BPF_NUM_MAPS=3
BPF_MAP1=6
BPF_MAP0=5
BPF_MAP2=7
sh# ls -la /proc/self/fd
[...]
lrwx------. 1 root root 64 Apr 14 16:46 5 -> anon_inode:bpf-map
lrwx------. 1 root root 64 Apr 14 16:46 6 -> anon_inode:bpf-map
lrwx------. 1 root root 64 Apr 14 16:46 7 -> anon_inode:bpf-map
sh# my_bpf_agent
عاملهای eBPF بسیار مفید هستند زیرا میتوانند نگاشتهای eBPF را از فضای کاربری از پیش مقداردهی کنند، آمارها را از طریق نگاشتها مانیتور کرده و بر اساس آن بازخورد، برای نمونه، مقادیر classid را در نگاشتهای eBPF در زمان اجرا بازنویسی نمایند. با توجه به اینکه عاملهای eBPF بهصورت برنامههای عادی پیادهسازی میشوند، میتوانند سیاستهای کنترل ترافیک را بهطور پویا از کنترلکنندههای خارجی دریافت کرده و آنها را به نگاشتهای eBPF ارسال کنند تا به شکل پویا با شرایط شبکه سازگار شوند. افزون بر این، نگاشتهای eBPF را میتوان با سایر انواع برنامههای eBPF (مانند ردیابی یا tracing) به اشتراک گذاشت، بنابراین میتوان ترکیبهای بسیار قدرتمندی را پیادهسازی نمود.
برنامهنویسی eBPF (eBPF PROGRAMMING)
ردهبند و کنشهای eBPF در نحو زبان C محدودشده پیادهسازی میشوند (در آینده ممکن است فرانتاندهای زبانهای جدیدی نیز پشتیبانی شوند).
فایل هدر linux/bpf.h توابع کمکی eBPF را ارائه میدهد که میتوانند از یک برنامه eBPF فراخوانی شوند. این صفحه راهنما تنها دو مثال حداقلی و مستقل ارائه میدهد؛ برای مشاهده یک مثال کامل از کالبدشکافی جریان (flow dissector) جهت درک بهتر برخی از امکانات eBPF، به مسیر examples/bpf از بسته سورس iproute2 مراجعه کنید.
کدهای
بازگشتی ۳۲
بیتی
پشتیبانیشده
برای
ردهبند از
برنامه C و
معانی
آنها:
0 ،
نشاندهنده
عدم تطابق
است
-1 ،
نشاندهنده
classid پیشفرضی
است که از خط
فرمان
پیکربندی
شده است
else ، هر مقدار
دیگری classid
پیشفرض را
بازنویسی
خواهد کرد
تا بستری
برای تطبیق
غیرخطی
فراهم شود
کدهای
بازگشتی ۳۲
بیتی
پشتیبانیشده
برای کنش از
برنامه C و
معانی
آنها ( linux/pkt_cls.h ):
TC_ACT_OK (0) ، خط
لوله
پردازش
بسته را
خاتمه داده
و اجازه
عبور بسته
را میدهد
TC_ACT_SHOT (2) ، خط
لوله
پردازش
بسته را
خاتمه داده
و بسته را
دور
میاندازد
(drop)
TC_ACT_UNSPEC (-1) ، از کنش
پیشفرض
پیکربندیشده
در tc استفاده
خواهد کرد
(مشابه
بازگرداندن
-1 از یک
ردهبند)
TC_ACT_PIPE (3) ، به کنش
بعدی
میرود (در
صورت وجود)
TC_ACT_RECLASSIFY (1) ، خط
لوله
پردازش
بسته را
خاتمه داده
و ردهبندی
را از ابتدا
آغاز
میکند
else ، هر مقدار
دیگری یک کد
بازگشتی
نامشخص است
هر دو نوع کد بازگشتی ردهبند و کنش در برنامههای eBPF و cBPF پشتیبانی میشوند.
برای نمایش نحو C محدودشده، یک مثال ساده از ردهبند ارائه شده است که فرض میکند بستههای خروجی، برای نمونه نشاتگرفته از یک کانتینر، پیشتر در بازه [0, 255] علامتگذاری (mark) شدهاند. این برنامه آمار علامتهای مختلف را برای فضای کاربری نگه میدارد و classid را با خودِ علامتگذاری بهعنوان شناسه فرعی (minor handle)، به qdisc ریشه نگاشت میکند:
#include <stdint.h>
#include <asm/types.h>
#include <linux/bpf.h>
#include <linux/pkt_sched.h>
#include "helpers.h"
struct tuple {
long packets;
long bytes;
};
#define BPF_MAP_ID_STATS 1 /* agent's map identifier */
#define BPF_MAX_MARK 256
struct bpf_elf_map __section("maps") map_stats = {
.type = BPF_MAP_TYPE_ARRAY,
.id = BPF_MAP_ID_STATS,
.size_key = sizeof(uint32_t),
.size_value = sizeof(struct tuple),
.max_elem = BPF_MAX_MARK,
.pinning = PIN_GLOBAL_NS,
};
static inline void cls_update_stats(const struct __sk_buff *skb,
uint32_t mark)
{
struct tuple *tu;
tu = bpf_map_lookup_elem(&map_stats, &mark);
if (likely(tu)) {
__sync_fetch_and_add(&tu->packets, 1);
__sync_fetch_and_add(&tu->bytes, skb->len);
}
}
__section("cls") int cls_main(struct __sk_buff *skb)
{
uint32_t mark = skb->mark;
if (unlikely(mark >= BPF_MAX_MARK))
return 0;
cls_update_stats(skb, mark);
return TC_H_MAKE(TC_H_ROOT, mark);
}
char __license[] __section("license") = "GPL";
مثال کوچک دیگر یک تغییرمسیردهنده درگاه (port redirector) است که پورت مقصد 80 را تحت هدایت RSS به بازه [8080, 8087] تفکیک (demux) میکند، که سپس میتواند به qdisc ورودی (ingress) متصل شود. تمرین افزودن بخش متناظر خروجی و پشتیبانی از IPv6 به عهده خواننده گذاشته شده است:
#include <asm/types.h>
#include <asm/byteorder.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/in.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include "helpers.h"
static inline void set_tcp_dport(struct __sk_buff *skb, int nh_off,
__u16 old_port, __u16 new_port)
{
bpf_l4_csum_replace(skb, nh_off + offsetof(struct tcphdr, check),
old_port, new_port, sizeof(new_port));
bpf_skb_store_bytes(skb, nh_off + offsetof(struct tcphdr, dest),
&new_port, sizeof(new_port), 0);
}
static inline int lb_do_ipv4(struct __sk_buff *skb, int nh_off)
{
__u16 dport, dport_new = 8080, off;
__u8 ip_proto, ip_vl;
ip_proto = load_byte(skb, nh_off +
offsetof(struct iphdr, protocol));
if (ip_proto != IPPROTO_TCP)
return 0;
ip_vl = load_byte(skb, nh_off);
if (likely(ip_vl == 0x45))
nh_off += sizeof(struct iphdr);
else
nh_off += (ip_vl & 0xF) << 2;
dport = load_half(skb, nh_off + offsetof(struct tcphdr, dest));
if (dport != 80)
return 0;
off = skb->queue_mapping & 7;
set_tcp_dport(skb, nh_off - BPF_LL_OFF, __constant_htons(80),
__cpu_to_be16(dport_new + off));
return -1;
}
__section("lb") int lb_main(struct __sk_buff *skb)
{
int ret = 0, nh_off = BPF_LL_OFF + ETH_HLEN;
if (likely(skb->protocol == __constant_htons(ETH_P_IP)))
ret = lb_do_ipv4(skb, nh_off);
return ret;
}
char __license[] __section("license") = "GPL";
فایل هدر کمکی مربوطه helpers.h در هر دو مثال به این صورت بود:
/* Misc helper macros. */
#define __section(x) __attribute__((section(x), used))
#define offsetof(x, y) __builtin_offsetof(x, y)
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
/* Object pinning settings */
#define PIN_NONE 0
#define PIN_OBJECT_NS 1
#define PIN_GLOBAL_NS 2
/* ELF map definition */
struct bpf_elf_map {
__u32 type;
__u32 size_key;
__u32 size_value;
__u32 max_elem;
__u32 flags;
__u32 id;
__u32 pinning;
__u32 inner_id;
__u32 inner_idx;
};
/* Some used BPF function calls. */
static int (*bpf_skb_store_bytes)(void *ctx, int off, void *from,
int len, int flags) =
(void *) BPF_FUNC_skb_store_bytes;
static int (*bpf_l4_csum_replace)(void *ctx, int off, int from,
int to, int flags) =
(void *) BPF_FUNC_l4_csum_replace;
static void *(*bpf_map_lookup_elem)(void *map, void *key) =
(void *) BPF_FUNC_map_lookup_elem;
/* Some used BPF intrinsics. */
unsigned long long load_byte(void *skb, unsigned long long off)
asm ("llvm.bpf.load.byte");
unsigned long long load_half(void *skb, unsigned long long off)
asm ("llvm.bpf.load.half");
بهعنوان بهترین شیوه (best practice)، ما توصیه میکنیم تنها یک ردهبند eBPF منفرد در tc بارگذاری کنید و تمام تطبیقها و دستکاریهای لازم را از همانجا انجام دهید، به جای اینکه از فهرستی از ردهبندهای مجزا و کنشهای جداگانه استفاده کنید. تنها یک ردهبند منفرد متناسب با یک کاربرد مشخص، بالاترین کارایی را در اجرا خواهد داشت.
اشکالزدایی eBPF (eBPF DEBUGGING)
هر دو دستور filter و action در tc برای bpf از یک پارامتر اختیاری verbose پشتیبانی میکنند که میتواند برای بازبینی لاگ اعتبارسنج eBPF استفاده شود. این لاگ بهطور پیشفرض در صورت بروز خطا نمایش داده میشود.
در صورتی که کامپایلر JIT برای eBPF/cBPF فعال شده باشد، میتوان به آن دستور داد تا یک خروجی اشکالزدایی از تصویر کدهای عملیاتی حاصل را به لاگ هسته ارسال کند، که میتوان آن را از طریق dmesg(1) خواند:
echo 2 > /proc/sys/net/core/bpf_jit_enable
درخت سورس هسته لینوکس علاوه بر این تحت مسیر tools/net/ ابزار کمکی کوچکی به نام bpf_jit_disasm عرضه میکند که تصویر کد عملیاتی را از لاگ هسته خوانده و دیساسمبلی حاصل را نمایش میدهد:
bpf_jit_disasm -o
گذشته از آن، هسته لینوکس همچنین شامل یک ماژول مجموعه آزمون جامع eBPF/cBPF به نام test_bpf است. پس از ...
modprobe test_bpf
... این ماژول مجموعهای متنوع از موارد آزمون را اجرا کرده و نتایج را در لاگ هسته ثبت میکند که با dmesg(1) قابل بازبینی است. نتایج ممکن است بسته به فعال بودن یا نبودن کامپایلر JIT متفاوت باشد. در صورت شکست در موارد آزمون، بارگذاری ماژول با خطا مواجه خواهد شد. در چنین مواردی، توصیه میکنیم گزارش خطا را به نویسندگان مربوطه JIT و فهرستهای پستی هسته لینوکس و شبکه ارسال فرمایید.
cBPF
اگرچه ما عموماً توصیه میکنیم به پیادهسازی ردهبند و کنشهای eBPF روی آورید، اما برای کامل بودن بحث، چند کلمهای درباره نحوه برنامهنویسی در cBPF در اینجا آورده میشود.
به همین ترتیب، سوییچ bpf_jit_enable میتواند همانطور که پیشتر گفته شد فعال شود. ابزارهایی مانند bpf_jit_disasm نیز مستقل از اینکه کد eBPF یا cBPF بارگذاری شده باشد عمل میکنند.
برخلاف eBPF، ردهبند و کنش در C محدودشده پیادهسازی نمیشوند، بلکه در یک زبان کمینه شبهاسمبلی یا با کمک ابزارهای دیگر پیادهسازی میگردند.
رابط خام با tc کدهای عملیاتی را مستقیماً دریافت میکند. برای نمونه، کمینهترین ردهبندی که با هر بستهای تطبیق یافته و منجر به classid پیشفرض 1:1 میشود به شکل زیر است:
tc filter add dev em1 parent 1: bpf bytecode '1,6 0 0 4294967295,' flowid
1:1
نخستین عدد دهدهی از دنباله بایتکد، تعداد 4-تاییهای بعدی از کدهای عملیاتی cBPF را نشان میدهد. همانطور که ذکر شد، چنین 4-تاییای شامل اعداد دهدهی c t f k است، که در آن c نشاندهنده کد عملیاتی cBPF، t مقصد آفست پرش در صورت درستی، f مقصد آفست پرش در صورت نادرستی و k مقدار ثابت/لفظی فوری است. در اینجا، این نشاندهنده یک بازگشت غیرمشروط از برنامه با مقدار فوری -1 است.
بنابراین، برای ردهبندی خروجی (egress)، ویلم د بروین (Willem de Bruijn) یک ابزار کمکی کمینه و مستقل تحت مجوز عمومی همگانی گنو نسخه ۲ برای افزونه BPF در iptables(8) پیادهسازی کرد که از کامپایلر BPF کلاسیک داخلی libpcap استفاده میکند؛ کد او در اینجا برای استفاده با tc(8) اقتباس شده است:
#include <pcap.h>
#include <stdio.h>
int main(int argc, char **argv)
{
struct bpf_program prog;
struct bpf_insn *ins;
int i, ret, dlt = DLT_RAW;
if (argc < 2 || argc > 3)
return 1;
if (argc == 3) {
dlt = pcap_datalink_name_to_val(argv[1]);
if (dlt == -1)
return 1;
}
ret = pcap_compile_nopcap(-1, dlt, &prog, argv[argc - 1],
1, PCAP_NETMASK_UNKNOWN);
if (ret)
return 1;
printf("%d,", prog.bf_len);
ins = prog.bf_insns;
for (i = 0; i < prog.bf_len - 1; ++ins, ++i)
printf("%u %u %u %u,", ins->code,
ins->jt, ins->jf, ins->k);
printf("%u %u %u %u",
ins->code, ins->jt, ins->jf, ins->k);
pcap_freecode(&prog);
return 0;
}
با در دست داشتن این ابزار کمکی کوچک، هر عبارت فیلتر tcpdump(8) میتواند بهعنوان یک ردهبند به کار رود که تطبیق آن منجر به classid پیشفرض خواهد شد:
bpftool EN10MB 'tcp[tcpflags] & tcp-syn != 0' > /var/bpf/tcp-syn
tc filter add dev em1 parent 1: bpf bytecode-file /var/bpf/tcp-syn flowid
1:1
در اصل، چنین مولد کمینهای معادل دستور زیر است:
tcpdump -iem1 -ddd 'tcp[tcpflags] & tcp-syn != 0' | tr ' ','
> /var/bpf/tcp-syn
از آنجا که libpcap از تمام افزونههای خاص cBPF لینوکس در کامپایلر خود پشتیبانی نمیکند، هسته لینوکس همچنین تحت مسیر tools/net/ یک اسمبلر کمینه BPF به نام bpf_asm برای ارائه کنترل کامل عرضه میکند. برای نحو و معناشناسی تفصیلی در پیادهسازی دستی چنین برنامههایی، مراجع موجود در بخش مطالعه بیشتر (FURTHER READING) را ببینید.
مثال ساده در bpf_asm برای ردهبندی بستههای IPv4/TCP، ذخیرهشده در یک فایل متنی به نام foobar:
ldh [12] jne #0x800, drop ldb [23] jneq #6, drop ret #-1 drop: ret #0
بهطور مشابه، چنین ردهبندی میتواند بدین صورت بارگذاری شود:
bpf_asm foobar > /var/bpf/tcp-syn
tc filter add dev em1 parent 1: bpf bytecode-file /var/bpf/tcp-syn flowid
1:1
برای ردهبندهای BPF، هسته لینوکس علاوه بر این تحت مسیر tools/net/ یک اشکالزدای کوچک BPF به نام bpf_dbg ارائه میدهد که میتواند برای آزمایش ردهبند روی فایلهای pcap، اجرای گامبهگام یا افزودن نقاط توقف مختلف در برنامه ردهبند و استخراج محتویات رجیسترها در زمان اجرا استفاده شود.
پیادهسازی یک کنش در BPF کلاسیک از این جهت که دستکاری بسته پشتیبانی نمیشود نسبتاً محدود است. از این رو، عموماً توصیه میشود که در صورت امکان به eBPF مهاجرت نمایید.
مطالعه بیشتر (FURTHER READING)
جزئیات بیشتر و فنیتر درباره معماری BPF را میتوان در درخت سورس هسته لینوکس تحت مسیر Documentation/networking/filter.txt یافت.
جزئیات بیشتر پیرامون مثالهای eBPF در tc(8) را میتوان در درخت سورس iproute2 تحت مسیر examples/bpf/ مشاهده کرد.
همچنین ببینید (SEE ALSO)
نویسندگان (AUTHORS)
صفحه راهنما توسط دانیل بورکمان (Daniel Borkmann) نوشته شده است.
لطفاً تصحیحات یا پیشنهادهای بهبود را به فهرست پستی شبکه هسته لینوکس گزارش دهید: <netdev@vger.kernel.org>
| 18 May 2015 | iproute2 |