Γράφουμε προστασία από επιθέσεις DDoS στο XDP. Πυρηνικό μέρος

Η τεχνολογία eXpress Data Path (XDP) επιτρέπει την εκτέλεση τυχαίας επεξεργασίας κίνησης σε διεπαφές Linux πριν τα πακέτα εισέλθουν στη στοίβα δικτύου πυρήνα. Εφαρμογή XDP - προστασία από επιθέσεις DDoS (CloudFlare), σύνθετα φίλτρα, συλλογή στατιστικών στοιχείων (Netflix). Τα προγράμματα XDP εκτελούνται από την εικονική μηχανή eBPF, επομένως έχουν περιορισμούς τόσο στον κώδικα τους όσο και στις διαθέσιμες λειτουργίες του πυρήνα ανάλογα με τον τύπο του φίλτρου.

Το άρθρο προορίζεται να καλύψει τις ελλείψεις πολλών υλικών στο XDP. Πρώτον, παρέχουν έτοιμο κώδικα που παρακάμπτει αμέσως τις δυνατότητες του XDP: είναι προετοιμασμένος για επαλήθευση ή είναι πολύ απλός για να προκαλέσει προβλήματα. Όταν στη συνέχεια προσπαθείτε να γράψετε τον κώδικά σας από την αρχή, δεν έχετε ιδέα τι να κάνετε με τυπικά σφάλματα. Δεύτερον, δεν καλύπτονται τρόποι τοπικής δοκιμής XDP χωρίς VM και υλικό, παρά το γεγονός ότι έχουν τις δικές τους παγίδες. Το κείμενο προορίζεται για προγραμματιστές εξοικειωμένους με τη δικτύωση και το Linux που ενδιαφέρονται για XDP και eBPF.

Σε αυτό το μέρος, θα κατανοήσουμε λεπτομερώς πώς συναρμολογείται το φίλτρο XDP και πώς να το δοκιμάσουμε, στη συνέχεια θα γράψουμε μια απλή έκδοση του γνωστού μηχανισμού cookies SYN σε επίπεδο επεξεργασίας πακέτων. Δεν θα δημιουργήσουμε ακόμη «λευκή λίστα».
επαληθευμένους πελάτες, κρατήστε μετρητές και διαχειριστείτε το φίλτρο - αρκετά αρχεία καταγραφής.

Θα γράψουμε σε C - δεν είναι της μόδας, αλλά είναι πρακτικό. Όλος ο κώδικας είναι διαθέσιμος στο GitHub μέσω του συνδέσμου στο τέλος και χωρίζεται σε δεσμεύσεις σύμφωνα με τα στάδια που περιγράφονται στο άρθρο.

Αποποίηση ευθυνών. Κατά τη διάρκεια αυτού του άρθρου, θα αναπτύξω μια μίνι λύση για να αποτρέψω τις επιθέσεις DDoS, επειδή αυτή είναι μια ρεαλιστική εργασία για το XDP και τον τομέα εξειδίκευσής μου. Ωστόσο, ο κύριος στόχος είναι να κατανοήσουμε την τεχνολογία· αυτός δεν είναι ένας οδηγός για τη δημιουργία έτοιμης προστασίας. Ο κώδικας εκμάθησης δεν είναι βελτιστοποιημένος και παραλείπει ορισμένες αποχρώσεις.

Σύντομη επισκόπηση XDP

Θα περιγράψω μόνο τα βασικά σημεία για να μην αντιγράψω την τεκμηρίωση και τα υπάρχοντα άρθρα.

Έτσι, ο κώδικας φίλτρου φορτώνεται στον πυρήνα. Τα εισερχόμενα πακέτα περνούν στο φίλτρο. Ως αποτέλεσμα, το φίλτρο πρέπει να πάρει μια απόφαση: να περάσει το πακέτο στον πυρήνα (XDP_PASS), απόθεση πακέτου (XDP_DROP) ή στείλτε το πίσω (XDP_TX). Το φίλτρο μπορεί να αλλάξει τη συσκευασία, αυτό ισχύει ιδιαίτερα για XDP_TX. Μπορείτε επίσης να ακυρώσετε το πρόγραμμα (XDP_ABORTED) και επαναφέρετε το πακέτο, αλλά αυτό είναι ανάλογο assert(0) - για αποσφαλμάτωση.

Η εικονική μηχανή eBPF (εκτεταμένο φίλτρο πακέτων Berkley) είναι σκόπιμα απλή, έτσι ώστε ο πυρήνας να μπορεί να ελέγξει ότι ο κώδικας δεν κάνει βρόχο και δεν καταστρέφει τη μνήμη άλλων ανθρώπων. Σωρευτικοί περιορισμοί και έλεγχοι:

  • Οι βρόχοι (προς τα πίσω) απαγορεύονται.
  • Υπάρχει μια στοίβα για δεδομένα, αλλά δεν υπάρχουν συναρτήσεις (όλες οι συναρτήσεις C πρέπει να είναι ενσωματωμένες).
  • Απαγορεύονται οι προσβάσεις στη μνήμη εκτός της στοίβας και του buffer πακέτων.
  • Το μέγεθος του κώδικα είναι περιορισμένο, αλλά στην πράξη αυτό δεν είναι πολύ σημαντικό.
  • Επιτρέπονται μόνο κλήσεις σε ειδικές λειτουργίες πυρήνα (βοηθοί eBPF).

Ο σχεδιασμός και η εγκατάσταση ενός φίλτρου μοιάζει με αυτό:

  1. Πηγαίος κώδικας (π.χ kernel.c) μεταγλωττίζεται σε αντικείμενο (kernel.o) για την αρχιτεκτονική εικονικής μηχανής eBPF. Από τον Οκτώβριο του 2019, η συλλογή στο eBPF υποστηρίζεται από την Clang και υποσχέθηκε στο GCC 10.1.
  2. Εάν αυτός ο κώδικας αντικειμένου περιέχει κλήσεις σε δομές πυρήνα (για παράδειγμα, πίνακες και μετρητές), τα αναγνωριστικά τους αντικαθίστανται από μηδενικά, πράγμα που σημαίνει ότι τέτοιος κώδικας δεν μπορεί να εκτελεστεί. Πριν φορτώσετε στον πυρήνα, πρέπει να αντικαταστήσετε αυτά τα μηδενικά με τα αναγνωριστικά συγκεκριμένων αντικειμένων που δημιουργήθηκαν μέσω κλήσεων του πυρήνα (συνδέστε τον κώδικα). Μπορείτε να το κάνετε αυτό με εξωτερικά βοηθητικά προγράμματα ή μπορείτε να γράψετε ένα πρόγραμμα που θα συνδέσει και θα φορτώσει ένα συγκεκριμένο φίλτρο.
  3. Ο πυρήνας επαληθεύει το φορτωμένο πρόγραμμα. Ελέγχεται η απουσία κύκλων και η αποτυχία υπέρβασης των ορίων πακέτων και στοίβας. Εάν ο επαληθευτής δεν μπορεί να αποδείξει ότι ο κωδικός είναι σωστός, το πρόγραμμα απορρίπτεται - πρέπει να μπορείτε να τον ευχαριστήσετε.
  4. Μετά την επιτυχή επαλήθευση, ο πυρήνας μεταγλωττίζει τον κώδικα αντικειμένου της αρχιτεκτονικής eBPF σε κώδικα μηχανής για την αρχιτεκτονική του συστήματος (ακριβώς στην ώρα).
  5. Το πρόγραμμα συνδέεται στη διεπαφή και ξεκινά την επεξεργασία πακέτων.

Εφόσον το XDP εκτελείται στον πυρήνα, ο εντοπισμός σφαλμάτων πραγματοποιείται χρησιμοποιώντας αρχεία καταγραφής ίχνους και, στην πραγματικότητα, πακέτα που φιλτράρει ή δημιουργεί το πρόγραμμα. Ωστόσο, το eBPF διασφαλίζει ότι ο ληφθέντος κώδικας είναι ασφαλής για το σύστημα, ώστε να μπορείτε να πειραματιστείτε με το XDP απευθείας στο τοπικό σας Linux.

Προετοιμασία του Περιβάλλοντος

συνέλευση

Το Clang δεν μπορεί να παράγει απευθείας κώδικα αντικειμένου για την αρχιτεκτονική eBPF, επομένως η διαδικασία αποτελείται από δύο βήματα:

  1. Μεταγλώττιση κώδικα C σε bytecode LLVM (clang -emit-llvm).
  2. Μετατροπή bytecode σε κωδικό αντικειμένου 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 εξακολουθεί να ελέγχει για παραβιάσεις εκτός ορίων στοίβας. Αξίζει να ενεργοποιήσετε αμέσως τις βελτιστοποιήσεις, επειδή το μέγεθος του bytecode 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. Πού να το δοκιμάσω τώρα;

Περίπτερο δοκιμής

Το περίπτερο πρέπει να περιλαμβάνει δύο διεπαφές: στις οποίες θα υπάρχει ένα φίλτρο και από το οποίο θα αποστέλλονται τα πακέτα. Αυτές πρέπει να είναι πλήρεις συσκευές Linux με τις δικές τους IP, προκειμένου να ελέγξουμε πώς λειτουργούν οι κανονικές εφαρμογές με το φίλτρο μας.

Οι συσκευές του τύπου veth (εικονικό Ethernet) είναι κατάλληλες για εμάς: πρόκειται για ένα ζεύγος εικονικών διεπαφών δικτύου «συνδεδεμένων» απευθείας μεταξύ τους. Μπορείτε να τις δημιουργήσετε έτσι (σε ​​αυτήν την ενότητα όλες οι εντολές 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) θα σταλεί η εισερχόμενη κίνηση. Ωστόσο, υπάρχει ένα πρόβλημα: οι διεπαφές βρίσκονται στο ίδιο μηχάνημα και το Linux δεν θα στέλνει κίνηση σε ένα από αυτά μέσω του άλλου. Μπορείτε να το λύσετε αυτό με δύσκολους κανόνες iptables, αλλά θα πρέπει να αλλάξουν πακέτα, κάτι που δεν είναι βολικό για τον εντοπισμό σφαλμάτων. Είναι προτιμότερο να χρησιμοποιείτε χώρους ονομάτων δικτύου (εφεξής netns).

Ένας χώρος ονομάτων δικτύου περιέχει ένα σύνολο διεπαφών, πινάκων δρομολόγησης και κανόνων NetFilter που είναι απομονωμένα από παρόμοια αντικείμενα σε άλλα δίκτυα. Κάθε διεργασία εκτελείται σε ένα χώρο ονομάτων και έχει πρόσβαση μόνο στα αντικείμενα αυτού του δικτύου. Από προεπιλογή, το σύστημα έχει έναν ενιαίο χώρο ονομάτων δικτύου για όλα τα αντικείμενα, ώστε να μπορείτε να εργάζεστε σε Linux και να μην γνωρίζετε για τα netns.

Ας δημιουργήσουμε έναν νέο χώρο ονομάτων xdp-test και μετακινήστε το εκεί xdp-remote.

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

Στη συνέχεια ξεκινά η διαδικασία xdp-test, δεν θα "δω" xdp-local (θα παραμείνει σε netns από προεπιλογή) και κατά την αποστολή ενός πακέτου στο 192.0.2.1 θα το περάσει από xdp-remoteεπειδή είναι η μόνη διεπαφή στο 192.0.2.0/24 που είναι προσβάσιμη σε αυτή τη διαδικασία. Αυτό λειτουργεί και προς την αντίθετη κατεύθυνση.

Όταν μετακινείστε μεταξύ δικτύων, η διεπαφή κατεβαίνει και χάνει τη διεύθυνσή της. Για να ρυθμίσετε τη διεπαφή σε 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 απαιτείται για τη σύνδεση ενός νέου προγράμματος εάν ένα άλλο είναι ήδη συνδεδεμένο. Το "No news is good news" δεν αφορά αυτήν την εντολή, το συμπέρασμα είναι σε κάθε περίπτωση ογκώδες. υποδεικνύω 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.

Το ping θα πρέπει τώρα να ενεργοποιεί μηνύματα όπως αυτό:

<...>-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 Echo και να σταματήσει να εμφανίζει το 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.

Δήλωση προβλήματος

Ας προχωρήσουμε στην δηλωμένη εργασία: γράψτε έναν μηχανισμό cookies SYN στο XDP.

Το SYN flood παραμένει μια δημοφιλής επίθεση DDoS, η ουσία της οποίας είναι η εξής. Όταν δημιουργηθεί μια σύνδεση (χειραψία TCP), ο διακομιστής λαμβάνει ένα SYN, εκχωρεί πόρους για τη μελλοντική σύνδεση, αποκρίνεται με ένα πακέτο SYNACK και περιμένει για ACK. Ο εισβολέας απλώς στέλνει χιλιάδες πακέτα SYN ανά δευτερόλεπτο από πλαστές διευθύνσεις από κάθε κεντρικό υπολογιστή σε ένα botnet πολλών χιλιάδων ισχυρών. Ο διακομιστής αναγκάζεται να εκχωρήσει πόρους αμέσως μετά την άφιξη του πακέτου, αλλά τους αποδεσμεύει μετά από μεγάλο χρονικό όριο, ως αποτέλεσμα, η μνήμη ή τα όρια εξαντλούνται, οι νέες συνδέσεις δεν γίνονται δεκτές και η υπηρεσία δεν είναι διαθέσιμη.

Εάν δεν εκχωρείτε πόρους με βάση το πακέτο SYN, αλλά απαντάτε μόνο με ένα πακέτο SYNACK, πώς μπορεί τότε ο διακομιστής να καταλάβει ότι το πακέτο ACK που έφτασε αργότερα αναφέρεται σε ένα πακέτο SYN που δεν αποθηκεύτηκε; Εξάλλου, ένας εισβολέας μπορεί επίσης να δημιουργήσει ψεύτικα ACK. Το θέμα του cookie SYN είναι να το κωδικοποιήσει seqnum παραμέτρους σύνδεσης ως κατακερματισμός διευθύνσεων, θυρών και αλλαγής αλατιού. Εάν το ACK κατάφερε να φτάσει πριν αλλάξει το αλάτι, μπορείτε να υπολογίσετε ξανά τον κατακερματισμό και να το συγκρίνετε με acknum. Σιδηρουργείο acknum ο εισβολέας δεν μπορεί, αφού το αλάτι περιλαμβάνει το μυστικό, και δεν θα έχει χρόνο να το ταξινομήσει λόγω περιορισμένου καναλιού.

Το cookie SYN έχει εφαρμοστεί εδώ και καιρό στον πυρήνα του Linux και μπορεί ακόμη και να ενεργοποιηθεί αυτόματα εάν τα SYN φτάνουν πολύ γρήγορα και μαζικά.

Εκπαιδευτικό πρόγραμμα για τη χειραψία TCP

Το TCP παρέχει μετάδοση δεδομένων ως ροή byte, για παράδειγμα, τα αιτήματα HTTP μεταδίδονται μέσω TCP. Το ρεύμα μεταδίδεται σε κομμάτια σε πακέτα. Όλα τα πακέτα TCP έχουν λογικές σημαίες και αριθμούς ακολουθίας 32 bit:

  • Ο συνδυασμός σημαιών καθορίζει το ρόλο ενός συγκεκριμένου πακέτου. Η σημαία SYN υποδεικνύει ότι αυτό είναι το πρώτο πακέτο του αποστολέα στη σύνδεση. Η σημαία ACK σημαίνει ότι ο αποστολέας έχει λάβει όλα τα δεδομένα σύνδεσης μέχρι το byte acknum. Ένα πακέτο μπορεί να έχει πολλές σημαίες και καλείται από το συνδυασμό τους, για παράδειγμα, ένα πακέτο SYNACK.

  • Ο αριθμός ακολουθίας (seqnum) καθορίζει τη μετατόπιση στη ροή δεδομένων για το πρώτο byte που μεταδίδεται σε αυτό το πακέτο. Για παράδειγμα, εάν στο πρώτο πακέτο με X byte δεδομένων αυτός ο αριθμός ήταν N, στο επόμενο πακέτο με νέα δεδομένα θα είναι N+X. Στην αρχή της σύνδεσης, κάθε πλευρά επιλέγει αυτόν τον αριθμό τυχαία.

  • Αριθμός επιβεβαίωσης (acknum) - η ίδια μετατόπιση με το seqnum, αλλά δεν καθορίζει τον αριθμό του byte που μεταδίδεται, αλλά τον αριθμό του πρώτου byte από τον παραλήπτη, το οποίο δεν είδε ο αποστολέας.

Στην αρχή της σύνδεσης, τα μέρη πρέπει να συμφωνήσουν seqnum и acknum. Ο πελάτης στέλνει μαζί του ένα πακέτο SYN seqnum = X. Ο διακομιστής απαντά με ένα πακέτο SYNACK, όπου το καταγράφει seqnum = Y και εκθέτει acknum = X + 1. Ο πελάτης απαντά στο SYNACK με ένα πακέτο ACK, όπου seqnum = X + 1, acknum = Y + 1. Μετά από αυτό, ξεκινά η πραγματική μεταφορά δεδομένων.

Εάν ο ομότιμος δεν επιβεβαιώσει την παραλαβή του πακέτου, το TCP το στέλνει ξανά μετά από ένα χρονικό όριο.

Γιατί τα SYN cookies δεν χρησιμοποιούνται πάντα;

Πρώτον, εάν χαθεί το SYNACK ή το ACK, θα πρέπει να περιμένετε να σταλεί ξανά - η ρύθμιση της σύνδεσης θα επιβραδυνθεί. Δεύτερον, στο πακέτο SYN - και μόνο σε αυτό! — μεταδίδονται διάφορες επιλογές που επηρεάζουν την περαιτέρω λειτουργία της σύνδεσης. Χωρίς να θυμάται τα εισερχόμενα πακέτα SYN, ο διακομιστής αγνοεί αυτές τις επιλογές· ο πελάτης δεν θα τις στείλει στα επόμενα πακέτα. Το TCP μπορεί να λειτουργήσει σε αυτήν την περίπτωση, αλλά τουλάχιστον στο αρχικό στάδιο η ποιότητα της σύνδεσης θα μειωθεί.

Όσον αφορά τα πακέτα, ένα πρόγραμμα XDP πρέπει να κάνει τα εξής:

  • απαντήστε στο SYN με το SYNACK με ένα cookie.
  • απαντήστε στο ACK με RST (αποσύνδεση).
  • πετάξτε τα υπόλοιπα πακέτα.

Ο ψευδοκώδικας του αλγορίθμου μαζί με την ανάλυση πακέτων:

Если это не Ethernet,
    пропустить пакет.
Если это не IPv4,
    пропустить пакет.
Если адрес в таблице проверенных,               (*)
        уменьшить счетчик оставшихся проверок,
        пропустить пакет.
Если это не TCP,
    сбросить пакет.     (**)
Если это SYN,
    ответить SYN-ACK с cookie.
Если это ACK,
    если в acknum лежит не cookie,
        сбросить пакет.
    Занести в таблицу адрес с N оставшихся проверок.    (*)
    Ответить RST.   (**)
В остальных случаях сбросить пакет.

Ενας (*) επισημαίνονται τα σημεία στα οποία πρέπει να διαχειριστείτε την κατάσταση του συστήματος - στο πρώτο στάδιο μπορείτε να τα κάνετε χωρίς αυτά εφαρμόζοντας απλώς μια χειραψία TCP με τη δημιουργία ενός cookie SYN ως seqnum.

Επί τόπου (**), ενώ δεν έχουμε τραπέζι, θα παραλείψουμε το πακέτο.

Εφαρμογή χειραψίας TCP

Ανάλυση του πακέτου και επαλήθευση του κωδικού

Θα χρειαστούμε δομές κεφαλίδας δικτύου: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) και TCP (uapi/linux/tcp.h). Δεν μπόρεσα να συνδέσω το τελευταίο λόγω σφαλμάτων που σχετίζονται με atomic64_t, έπρεπε να αντιγράψω τους απαραίτητους ορισμούς στον κώδικα.

Όλες οι συναρτήσεις που επισημαίνονται στο C για αναγνωσιμότητα πρέπει να είναι ενσωματωμένες στο σημείο κλήσης, καθώς ο επαληθευτής eBPF στον πυρήνα απαγορεύει το backtracking, δηλαδή, στην πραγματικότητα, βρόχους και κλήσεις συναρτήσεων.

#define INTERNAL static __attribute__((always_inline))

Macro 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): Υπάρχουν διαδρομές εκτέλεσης όταν το δέκατο τρίτο byte από την αρχή του buffer βρίσκεται εκτός του πακέτου. Είναι δύσκολο να καταλάβουμε από την λίστα για ποια γραμμή μιλάμε, αλλά υπάρχει ένας αριθμός εντολής (12) και ένας αποσυναρμολογητής που δείχνει τις γραμμές του πηγαίου κώδικα:

llvm-objdump -S xdp_filter.o | less

Σε αυτή την περίπτωση δείχνει στη γραμμή

LOG("Ether(proto=0x%x)", bpf_ntohs(ether->h_proto));

που καθιστά σαφές ότι το πρόβλημα είναι ether. Πάντα έτσι θα ήταν.

Απάντηση στον ΣΥΝ

Ο στόχος σε αυτό το στάδιο είναι να δημιουργηθεί ένα σωστό πακέτο SYNACK με ένα σταθερό seqnum, το οποίο θα αντικατασταθεί στο μέλλον από το cookie 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;

Εναλλάξτε θύρες 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 bit στις κεφαλίδες και το μέγεθος των κεφαλίδων γράφεται σε αυτές, δηλαδή άγνωστο κατά τη στιγμή της μεταγλώττισης. Αυτό είναι ένα πρόβλημα επειδή ο επαληθευτής δεν θα παρακάμψει τον κανονικό βρόχο στη μεταβλητή ορίου. Αλλά το μέγεθος των κεφαλίδων είναι περιορισμένο: έως 64 byte το καθένα. Μπορείτε να δημιουργήσετε έναν βρόχο με σταθερό αριθμό επαναλήψεων, ο οποίος μπορεί να τελειώσει νωρίς.

Σημειώνω ότι υπάρχει 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-bit, εφαρμόζεται μια απλούστερη έκδοση:

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() κάνει ένα άθροισμα ελέγχου από ένα άθροισμα 32-bit λέξεων 16-bit, σύμφωνα με το RFC 791.

Επαλήθευση χειραψίας TCP

Το φίλτρο δημιουργεί σωστά μια σύνδεση με netcat, χωρίς το τελικό ACK, στο οποίο το Linux απάντησε με ένα πακέτο RST, αφού η στοίβα δικτύου δεν έλαβε SYN - μετατράπηκε σε SYNACK και στάλθηκε πίσω - και από την άποψη του λειτουργικού συστήματος, έφτασε ένα πακέτο που δεν σχετιζόταν με το άνοιγμα συνδέσεις.

$ 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, η ίδια η επαλήθευση είναι ασήμαντη. Ο αλγόριθμος υπολογισμού είναι πρωτόγονος και πιθανότατα ευάλωτος σε έναν περίπλοκο εισβολέα. Ο πυρήνας Linux, για παράδειγμα, χρησιμοποιεί το κρυπτογραφικό SipHash, αλλά η υλοποίησή του για το XDP ξεφεύγει σαφώς από το πεδίο εφαρμογής αυτού του άρθρου.

Παρουσιάστηκε για νέες TODO που σχετίζονται με την εξωτερική επικοινωνία:

  • Το πρόγραμμα XDP δεν μπορεί να αποθηκεύσει cookie_seed (το μυστικό μέρος του αλατιού) σε μια καθολική μεταβλητή, χρειάζεστε αποθήκευση στον πυρήνα, η τιμή του οποίου θα ενημερώνεται περιοδικά από μια αξιόπιστη γεννήτρια.

  • Εάν το cookie 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 σάς επιτρέπει να εφαρμόσετε αρκετά περίπλοκη λογική, η οποία, επιπλέον, είναι εύκολο να ενημερώνεται χωρίς διακοπή στην επεξεργασία της κυκλοφορίας. Ο επαληθευτής δεν δημιουργεί μεγάλα προβλήματα· προσωπικά, δεν θα το αρνιόμουν για τμήματα του κωδικού χώρου χρήστη.

Στο δεύτερο μέρος, εάν το θέμα είναι ενδιαφέρον, θα συμπληρώσουμε τον πίνακα των επαληθευμένων πελατών και των αποσυνδέσεων, θα εφαρμόσουμε μετρητές και θα γράψουμε ένα βοηθητικό πρόγραμμα userpace για τη διαχείριση του φίλτρου.

Βιβλιογραφικές αναφορές:

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο