לדלג לתוכן

מסלול רשת מתקדם ובחירת שכבת עיבוד

אותה מדיניות יכולה להופיע בכמה מקומות

נניח שהשירות שלנו צריך להגביל עומס. אפשר לדחות בקשה אחרי פענוח בפרוטוקול היישום, לסנן חבילה לפני שהיא מגיעה לשקע או להגביל תור במערכת. כל בחירה רואה מידע אחר ומשלמת מחיר אחר. דחייה מוקדמת חוסכת עבודה, אבל אינה בהכרח מכירה זהות משתמש או משמעות בקשה שמוצפנת בתעבורה.

לא נבחר מנגנון רק מפני שהוא קרוב יותר לחומרה. נתחיל בשאלה איזה מידע דרוש להחלטה ומהו חוזה הכשל. אם ההחלטה דורשת פענוח מלא והחזרת שגיאת יישום, מסנן חבילות מוקדם אינו תחליף למימוש בשירות. אם נדרש להגן על משאבי קליטה בפני תעבורה שאין צורך לקבל, נקודה מוקדמת יכולה להתאים.

הדרך מהתור של הכרטיס לשקע

התקן רשת מקבל נתונים לתורי קליטה ומודיע למעבד על עבודה זמינה. הדרייבר משתמש במנגנוני פסיקות ודגימה כדי לעבד קבוצות חבילות. במסלול נתמך, תוכנית XDP יכולה לפעול מוקדם על נתוני החבילה לפני יצירת חלק מן המבנים של מחסנית הרשת. קיימים מצבי הרצה שונים, ולא לכל התקן אותן יכולות.

בהמשך קיימים מבני חבילות של הקרנל, נקודות עיבוד של בקרת תעבורה, ניתוב, סינון, פרוטוקולי תעבורה ושקעים. פעולות איחוד ופיצול יכולות לשנות את היחס בין חבילה על החוט לבין אובייקט שנצפה בכלי תוכנה. לכן השוואת מונים משתי שכבות דורשת לדעת מה כל מונה סופר.

מנגנון TC מתאים בין השאר לסיווג, תזמון ועיצוב תעבורה בנקודות הכניסה והיציאה המתאימות. מנגנון מוקדם כמו XDP אינו רואה בהכרח את כל ההקשר שזמין מאוחר יותר. החלטה נכונה יכולה להיות כלל תצורה פשוט במקום תוכנית חדשה, במיוחד כאשר אין צורך בלוגיקה מיוחדת.

מסנן מעבדה קטן

נכתוב תוכנית שמסננת הודעות בקרה בגרסה הרביעית של פרוטוקול האינטרנט וסופרת אותן. היא מיועדת לרשת וירטואלית מבודדת, ולא להתקנה על ממשק העבודה. הסינון מבוסס רק על כותרת הפרוטוקול, ואינו מימוש כללי של חומת אש.

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/in.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __uint(max_entries, 1);
    __type(key, __u32);
    __type(value, __u64);
} dropped SEC(".maps");

SEC("xdp")
int filter(struct xdp_md *context)
{
    void *end = (void *)(long)context->data_end;
    void *data = (void *)(long)context->data;
    struct ethhdr *ethernet = data;
    struct iphdr *ip;
    __u32 key = 0;
    __u64 *counter;
    if ((void *)(ethernet + 1) > end)
        return XDP_PASS;
    if (ethernet->h_proto != bpf_htons(ETH_P_IP))
        return XDP_PASS;
    ip = (void *)(ethernet + 1);
    if ((void *)(ip + 1) > end)
        return XDP_PASS;
    if (ip->version != 4 || ip->ihl < 5)
        return XDP_PASS;
    if ((void *)ip + ip->ihl * 4 > end)
        return XDP_PASS;
    if (ip->protocol != IPPROTO_ICMP)
        return XDP_PASS;
    counter = bpf_map_lookup_elem(&dropped, &key);
    if (counter)
        __sync_fetch_and_add(counter, 1);
    return XDP_DROP;
}
char license[] SEC("license") = "GPL";

כל קריאה לכותרת קודמת לה בדיקת תחום שהמאמת יכול לעקוב אחריה. עדיין אין כאן טיפול בתגיות רשת מקומית או במנהור. חבילות כאלה עוברות בלי סינון בהתאם לקוד. זו מגבלה מפורשת של הניסוי ולא הוכחה שכל הודעת בקרה בעולם תיחסם.

נבנה באמצעות מהדר שתומך ביעד התוכניות ובכותרות הפיתוח של הספרייה. בהפצות מסוימות נדרש להוסיף גם נתיב כותרות ייעודי לארכיטקטורה, בהתאם לשגיאת הבנייה ולהתקנת החבילות.

clang -O2 -g -target bpf -c filter.c -o filter.o

רשת קטנה עם נקודת תצפית ברורה

נריץ באורח מעבדה את התסריט הבא. הוא יוצר שני מרחבי רשת, זוג ממשקים וכתובות ניסוי, ומסיר את המשאבים שיצר בסיום. נקודת הסינון נמצאת בכניסה של הצד השני, ולכן בקשת הבדיקה יוצאת מהראשון אך אינה מגיעה למחסנית הרשת של השני.

set -eu
a=amit-xa-$$
b=amit-xb-$$
left=vxa$$
right=vxb$$
sudo ip netns add "$a"
cleanup() {
    sudo ip netns del "$a" 2>/dev/null || true
    sudo ip netns del "$b" 2>/dev/null || true
}
trap cleanup EXIT
sudo ip netns add "$b"
sudo ip link add "$left" netns "$a" type veth peer name "$right" netns "$b"
sudo ip -n "$a" addr add 192.0.2.1/24 dev "$left"
sudo ip -n "$b" addr add 192.0.2.2/24 dev "$right"
sudo ip -n "$a" link set lo up
sudo ip -n "$b" link set lo up
sudo ip -n "$a" link set "$left" up
sudo ip -n "$b" link set "$right" up
sudo ip netns exec "$a" ping -c 2 -W 1 192.0.2.2
sudo ip -n "$b" link set dev "$right" xdpgeneric obj filter.o sec xdp
sudo ip netns exec "$a" ping -c 2 -W 1 192.0.2.2 || true
sudo bpftool map show
sudo ip -n "$b" link set dev "$right" xdpgeneric off
sudo ip netns exec "$a" ping -c 2 -W 1 192.0.2.2

ההרצה משתמשת במצב תוכנה כללי במפורש, ולכן אינה מודדת יתרון של נקודת הרצה בחומרה או בדרייבר מסוים. לאחר זיהוי המפה ששייכת לתוכנית שלנו נקרא אותה עם bpftool map dump id והמזהה שנצפה. נסכם את הערכים לכל המעבדים ולא נניח שערך אחד הוא המונה הכולל.

העברת חבילות למרחב משתמש

כאשר נבחר AF_XDP, היישום משתף אזור זיכרון ומעביר בעלות באמצעות טבעות מילוי, קליטה, שידור והשלמה. באפר שנמסר לשידור אינו פנוי למחזור עד שההשלמה המתאימה מחזירה אותו. אם נשכח להחזיר באפרים לטבעת המילוי, הקליטה יכולה להיעצר גם כאשר יש זיכרון פנוי בתהליך.

מצב ללא העתקה תלוי בתמיכת הדרייבר ובתצורה. הממשק אינו מבטיח אותו בכל סביבה. גם כאשר נמנעת העתקה, נשארות עלויות ניהול תורים, סנכרון, מטמונים ובדיקת חבילות. נמדוד את המסלול בפועל ונשווה גם לפתרון פשוט עם שקעים ומנות עבודה מרובות.

בבחירת שכבה נרשום ארבעה דברים: מידע זמין, עלות משאבים, סמכות נדרשת ומורכבות תפעול. אם שינוי כלל תצורה עונה על הדרישה, נעדיף לבחון אותו לפני הוספת רכיב שדורש טעינת תוכנית, מעקב גרסאות וניהול מפות לאורך זמן.

להרחבה ניעזר בתיעוד מסלול חבילות למרחב משתמש ובמקורות כלי האימות והדרייבר של גרסת הקרנל במעבדה.