
آلة افتراضية بلغة Rust ومترجم JIT لبرامج eBPF
آلة افتراضية (في مساحة المستخدم) بلغة Rust لـ eBPF
تحتوي هذه الحزمة على آلة افتراضية لتنفيذ برامج eBPF. إن BPF، أي Berkeley Packet Filter، هي لغة شبيهة بلغة التجميع طُوِّرت في الأصل لأنظمة BSD، وذلك لتصفية الحزم في النواة باستخدام أدوات مثل tcpdump لتجنب النسخ غير الضروري إلى مساحة المستخدم. وقد نُقلت إلى Linux، حيث تطورت إلى eBPF (الموسَّعة BPF)، وهي نسخة أسرع بميزات أكثر. وبينما صُمِّمت برامج BPF في الأصل لتعمل في النواة، فإن الآلة الافتراضية في هذه الحزمة تتيح تشغيلها في تطبيقات مساحة المستخدم؛ فهي تحتوي على مُفسِّر، ومُصرِّف JIT من نوع x86_64 لبرامج eBPF، بالإضافة إلى مُفكِّك تجميع.
وهي مبنية على برنامج uBPF الخاص بـ Rich Lane، والذي يفعل الشيء نفسه تقريبًا، لكنه مكتوب بلغة C.
من المفترض أن تُصرَّف هذه الحزمة وتعمل على Linux وMacOS X وWindows، رغم أن مُصرِّف JIT لا يعمل مع Windows في الوقت الحالي.
هذه الحزمة متاحة من crates.io، لذا
من المفترض أن تعمل مباشرةً بمجرد إضافتها كاعتمادية في ملف Cargo.toml
الخاص بك:
[dependencies]
rbpf = "0.4.1"
يمكنك أيضًا استخدام نسخة التطوير من مستودع GitHub هذا. يجب أن يكون هذا بسيطًا مثل وضع هذا داخل Cargo.toml الخاص بك:
[dependencies]
rbpf = { git = "https://github.com/qmonnet/rbpf" }
بالطبع، إذا كنت تفضل ذلك، يمكنك استنساخه محليًا، وربما تعديل الـ crate،
ثم الإشارة إلى مسار نسختك المحلية في Cargo.toml:
[dependencies]
rbpf = { path = "path/to/rbpf" }
ثم أشر في شيفرة المصدر الخاصة بك إلى أنك تريد استخدام الـ crate:
extern crate rbpf;
واجهة برمجة التطبيقات موثّقة جيدًا داخل الكود المصدري. يجب أن تكون قادرًا أيضًا على الوصول إلى نسخة إلكترونية من التوثيق من هنا، يتم إنشاؤها تلقائيًا من إصدار crates.io (قد لا تكون محدّثة مع الفرع الرئيسي). كما ينبغي أن تكون الأمثلة واختبارات الوحدة مفيدة أيضًا. فيما يلي ملخص لكيفية استخدام الحزمة (crate).
إليك الخطوات التي يجب اتباعها لتشغيل برنامج eBPF باستخدام rbpf:
صُمّم eBPF في البداية لتصفية الحزم (الآن لديه بعض الخطافات الأخرى في نواة Linux، مثل kprobes، لكن هذا غير مغطى بواسطة rbpf). ونتيجة لذلك، تُنفَّذ معظم تعليمات التحميل والتخزين في البرنامج على منطقة ذاكرة تمثّل بيانات الحزمة. ومع ذلك، في نواة Linux، لا يصل برنامج eBPF مباشرةً إلى منطقة البيانات هذه: في البداية، لديه وصول إلى struct sk_buff بلغة C بدلاً من ذلك، وهو مخزن مؤقت يحتوي على بيانات وصفية حول الحزمة—بما في ذلك عناوين الذاكرة لبداية ونهاية منطقة بيانات الحزمة. لذا يقوم البرنامج أولاً بتحميل تلك المؤشرات من sk_buff، ثم يمكنه الوصول إلى بيانات الحزمة.
يمكن محاكاة هذا السلوك باستخدام rbpf، لكنه ليس إلزاميًا. لهذا السبب، لدينا عدة بنى (structs) تمثّل أنواعًا مختلفة من الآلات الافتراضية:
struct EbpfVmMbuffer يحاكي النواة. عند تشغيل البرنامج، سيكون العنوان المُمرَّر إلى أول سجل eBPF الخاص به هو عنوان مخزن مؤقت للبيانات الوصفية يوفّره المستخدم، ويُتوقع أن يحتوي على مؤشرات إلى بداية ونهاية منطقة ذاكرة بيانات الحزمة.
struct EbpfVmFixedMbuff له غرض واحد: تمكين تنفيذ البرامج المُنشأة لتكون متوافقة مع النواة، مع توفير الجهد على المستخدم في التعامل اليدوي مع مخزن البيانات الوصفية. في الواقع، تحتوي هذه البنية على مخزن مؤقت داخلي ثابت يُمرَّر إلى البرنامج. على المستخدم أن يحدد قيم الإزاحة التي يتوقع برنامج eBPF عندها العثور على بداية ونهاية بيانات الحزمة في المخزن المؤقت. عند استدعاء الدالة التي تشغّل البرنامج (سواء كان مُترجمًا بـ JIT أم لا)، تقوم البنية تلقائيًا بتحديث العناوين في هذا المخزن المؤقت الثابت، عند الإزاحات المحددة، لبداية ونهاية بيانات الحزمة التي يُستدعى البرنامج من أجلها.
struct EbpfVmRaw مخصص للبرامج التي تريد العمل مباشرةً على بيانات الحزمة. لا يوجد مخزن بيانات وصفية متضمن، يتلقى برنامج eBPF مباشرةً عنوان بيانات الحزمة في أول سجل له. هذا هو سلوك uBPF.
struct EbpfVmNoData لا يأخذ أي بيانات. لا يأخذ برنامج eBPF أي وسيط على الإطلاق وقيمته المُرجعة حتمية. لست متأكدًا تمامًا من وجود حالة استخدام صالحة لذلك، لكن على الأقل، هذا مفيد جدًا لاختبارات الوحدة.
كل هذه البنى تنفّذ نفس الدوال العامة:
// called with EbpfVmMbuff:: prefix
pub fn new(prog: &'a [u8]) -> Result<EbpfVmMbuff<'a>, Error>
// called with EbpfVmFixedMbuff:: prefix
pub fn new(prog: &'a [u8],
data_offset: usize,
data_end_offset: usize) -> Result<EbpfVmFixedMbuff<'a>, Error>
// called with EbpfVmRaw:: prefix
pub fn new(prog: &'a [u8]) -> Result<EbpfVmRaw<'a>, Error>
// called with EbpfVmNoData:: prefix
pub fn new(prog: &'a [u8]) -> Result<EbpfVmNoData<'a>, Error>
يُستخدم هذا لإنشاء نسخة جديدة من آلة افتراضية (VM). يعتمد نوع الإرجاع على البنية (struct) التي تُستدعى منها الدالة. على سبيل المثال، rbpf::EbpfVmRaw::new(Some(my_program)) سيُرجع نسخة من struct rbpf::EbpfVmRaw (مغلّفة في Result). عند تحميل برنامج، يتم التحقق منه باستخدام مُدقّق (verifier) بسيط جدًا (لا يقترب من ذلك الخاص بنواة Linux). يمكن للمستخدمين أيضًا استبداله بمُدقّق مخصص.
بالنسبة لـ struct EbpfVmFixedMbuff، يجب تمرير وسيطتين إضافيتين إلى المُنشئ (constructor): data_offset و data_end_offset. وهما الإزاحة (رقم البايت) التي سيتم تخزين المؤشرات إلى بداية ونهاية، على التوالي، منطقة الذاكرة الخاصة ببيانات الحزمة (packet data) عندها في مخزن البيانات الوصفية (metadata buffer) الداخلي في كل مرة يتم فيها تنفيذ البرنامج. لا تستخدم البنى الأخرى هذه الآلية ولا تحتاج إلى تلك الإزاحات.
// for struct EbpfVmMbuff, struct EbpfVmRaw and struct EbpfVmRawData
pub fn set_program(&mut self, prog: &'a [u8]) -> Result<(), Error>
// for struct EbpfVmFixedMbuff
pub fn set_program(&mut self, prog: &'a [u8],
data_offset: usize,
data_end_offset: usize) -> Result<(), Error>
يمكنك استخدام على سبيل المثال my_vm.set_program(my_program); لتغيير البرنامج المحمّل
بعد إنشاء نسخة VM. يتم فحص هذا البرنامج بواسطة
المدقّق المرفق بـ VM. يمكن تغيير دالة التحقق الخاصة بـ VM في
أي لحظة.
pub type Verifier = fn(prog: &[u8]) -> Result<(), Error>;
pub fn set_verifier(&mut self,
verifier: Verifier) -> Result<(), Error>
لاحظ أنه إذا تم تحميل برنامج بالفعل في الآلة الافتراضية، فإن تعيين مدقق جديد يؤدي أيضًا إلى تشغيله فورًا على البرنامج المحمّل. ومع ذلك، لا يتم تشغيل المدقق إذا لم يتم تحميل أي برنامج (إذا تم تمرير None إلى طريقة new() عند إنشاء الآلة الافتراضية).