BPF για τα μικρά, μέρος μηδέν: κλασικό BPF

Το Berkeley Packet Filters (BPF) είναι μια τεχνολογία πυρήνα Linux που βρίσκεται στις πρώτες σελίδες των αγγλόφωνων τεχνολογικών εκδόσεων εδώ και αρκετά χρόνια. Τα συνέδρια είναι γεμάτα με αναφορές σχετικά με τη χρήση και την ανάπτυξη του BPF. Ο David Miller, συντηρητής υποσυστήματος δικτύου Linux, καλεί την ομιλία του στο Linux Plumbers 2018 "Αυτή η συζήτηση δεν είναι για XDP" (Το XDP είναι μία περίπτωση χρήσης για το BPF). Ο Μπρένταν Γκρεγκ δίνει ομιλίες με τίτλο Linux BPF Superpowers. Toke Høiland-Jørgensen γελάειότι ο πυρήνας είναι πλέον μικροπυρήνας. Ο Thomas Graf προωθεί την ιδέα ότι Το BPF είναι javascript για τον πυρήνα.

Δεν υπάρχει ακόμα συστηματική περιγραφή του BPF στο Habré, και επομένως σε μια σειρά άρθρων θα προσπαθήσω να μιλήσω για την ιστορία της τεχνολογίας, να περιγράψω την αρχιτεκτονική και τα εργαλεία ανάπτυξης και να περιγράψω τους τομείς εφαρμογής και πρακτικής χρήσης του BPF. Αυτό το άρθρο, μηδέν, της σειράς, αφηγείται την ιστορία και την αρχιτεκτονική του κλασικού BPF και επίσης αποκαλύπτει τα μυστικά των αρχών λειτουργίας του. tcpdump, seccomp, strace, και πολλα ΑΚΟΜΑ.

Η ανάπτυξη του BPF ελέγχεται από την κοινότητα δικτύων Linux, οι κύριες υπάρχουσες εφαρμογές του BPF σχετίζονται με δίκτυα και επομένως, με άδεια @eucariot, ονόμασα τη σειρά “BPF για τους μικρούς”, προς τιμήν της μεγάλης σειράς "Δίκτυα για τους μικρούς".

Μια σύντομη πορεία στην ιστορία του BPF(c)

Η σύγχρονη τεχνολογία BPF είναι μια βελτιωμένη και διευρυμένη έκδοση της παλιάς τεχνολογίας με το ίδιο όνομα, που τώρα ονομάζεται κλασική BPF για αποφυγή σύγχυσης. Ένα γνωστό βοηθητικό πρόγραμμα δημιουργήθηκε με βάση το κλασικό BPF tcpdump, μηχανισμός seccomp, καθώς και λιγότερο γνωστές ενότητες xt_bpf για iptables και ταξινομητής cls_bpf. Στο σύγχρονο Linux, τα κλασικά προγράμματα BPF μεταφράζονται αυτόματα στη νέα μορφή, ωστόσο, από πλευράς χρήστη, το API έχει παραμείνει στη θέση του και νέες χρήσεις για το κλασικό BPF, όπως θα δούμε σε αυτό το άρθρο, εξακολουθούν να βρίσκονται. Για αυτόν τον λόγο, αλλά και επειδή ακολουθώντας την ιστορία της ανάπτυξης του κλασικού BPF στο Linux, θα γίνει πιο σαφές πώς και γιατί εξελίχθηκε στη σύγχρονη μορφή του, αποφάσισα να ξεκινήσω με ένα άρθρο για το κλασικό BPF.

Στα τέλη της δεκαετίας του ογδόντα του περασμένου αιώνα, οι μηχανικοί από το διάσημο εργαστήριο Lawrence Berkeley ενδιαφέρθηκαν για το πώς να φιλτράρουν σωστά τα πακέτα δικτύου σε υλικό που ήταν σύγχρονο στα τέλη της δεκαετίας του ογδόντα του περασμένου αιώνα. Η βασική ιδέα του φιλτραρίσματος, που αρχικά εφαρμόστηκε στην τεχνολογία CSPF (CMU/Stanford Packet Filter), ήταν να φιλτράρει τα περιττά πακέτα όσο το δυνατόν νωρίτερα, δηλ. στον χώρο του πυρήνα, καθώς έτσι αποφεύγεται η αντιγραφή περιττών δεδομένων στο χώρο του χρήστη. Για την παροχή ασφάλειας χρόνου εκτέλεσης για την εκτέλεση του κώδικα χρήστη στο χώρο του πυρήνα, χρησιμοποιήθηκε μια εικονική μηχανή sandboxed.

Ωστόσο, οι εικονικές μηχανές για τα υπάρχοντα φίλτρα σχεδιάστηκαν για να λειτουργούν σε μηχανές που βασίζονται σε στοίβα και δεν λειτουργούσαν τόσο αποτελεσματικά σε νεότερες μηχανές RISC. Ως αποτέλεσμα, μέσω των προσπαθειών μηχανικών από το Berkeley Labs, αναπτύχθηκε μια νέα τεχνολογία BPF (Berkeley Packet Filters), η αρχιτεκτονική της εικονικής μηχανής της οποίας σχεδιάστηκε με βάση τον επεξεργαστή Motorola 6502 - το άλογο γνωστών προϊόντων όπως Apple II ή NES. Η νέα εικονική μηχανή αύξησε την απόδοση του φίλτρου δεκάδες φορές σε σύγκριση με τις υπάρχουσες λύσεις.

Αρχιτεκτονική μηχανής BPF

Θα εξοικειωθούμε με την αρχιτεκτονική λειτουργικά, αναλύοντας παραδείγματα. Ωστόσο, για να ξεκινήσουμε, ας πούμε ότι το μηχάνημα είχε δύο καταχωρητές 32-bit προσβάσιμους στο χρήστη, έναν συσσωρευτή A και καταχωρητής ευρετηρίου X, 64 byte μνήμης (16 λέξεις), διαθέσιμα για γραφή και επακόλουθη ανάγνωση, και ένα μικρό σύστημα εντολών για εργασία με αυτά τα αντικείμενα. Οδηγίες μετάβασης για την υλοποίηση παραστάσεων υπό όρους ήταν επίσης διαθέσιμες στα προγράμματα, αλλά για να εξασφαλιστεί η έγκαιρη ολοκλήρωση του προγράμματος, τα άλματα μπορούσαν να γίνουν μόνο προς τα εμπρός, δηλαδή, συγκεκριμένα, απαγορεύτηκε η δημιουργία βρόχων.

Το γενικό σχέδιο για την εκκίνηση του μηχανήματος έχει ως εξής. Ο χρήστης δημιουργεί ένα πρόγραμμα για την αρχιτεκτονική BPF και, χρησιμοποιώντας μερικοί μηχανισμός πυρήνα (όπως μια κλήση συστήματος), φορτώνει και συνδέει το πρόγραμμα με για κάποιους στη γεννήτρια συμβάντων στον πυρήνα (για παράδειγμα, ένα συμβάν είναι η άφιξη του επόμενου πακέτου στην κάρτα δικτύου). Όταν συμβαίνει ένα συμβάν, ο πυρήνας εκτελεί το πρόγραμμα (για παράδειγμα, σε έναν διερμηνέα) και η μνήμη του μηχανήματος αντιστοιχεί σε για κάποιους περιοχή μνήμης πυρήνα (για παράδειγμα, δεδομένα ενός εισερχόμενου πακέτου).

Τα παραπάνω θα είναι αρκετά για να αρχίσουμε να εξετάζουμε παραδείγματα: θα εξοικειωθούμε με το σύστημα και τη μορφή εντολών όπως απαιτείται. Εάν θέλετε να μελετήσετε αμέσως το σύστημα εντολών μιας εικονικής μηχανής και να μάθετε για όλες τις δυνατότητές της, τότε μπορείτε να διαβάσετε το αρχικό άρθρο Το φίλτρο πακέτου BSD και/ή το πρώτο μισό του αρχείου Τεκμηρίωση/δικτύωση/φίλτρο.txt από την τεκμηρίωση του πυρήνα. Επιπλέον, μπορείτε να μελετήσετε την παρουσίαση libpcap: Μια Μεθοδολογία Αρχιτεκτονικής και Βελτιστοποίησης για Καταγραφή Πακέτων, στο οποίο ο McCanne, ένας από τους συγγραφείς του BPF, μιλά για την ιστορία της δημιουργίας libpcap.

Προχωράμε τώρα για να εξετάσουμε όλα τα σημαντικά παραδείγματα χρήσης του κλασικού BPF σε Linux: tcpdump (libpcap), seccomp, xt_bpf, cls_bpf.

tcpdump

Η ανάπτυξη του BPF πραγματοποιήθηκε παράλληλα με την ανάπτυξη του frontend για φιλτράρισμα πακέτων - ένα πολύ γνωστό βοηθητικό πρόγραμμα tcpdump. Και, καθώς αυτό είναι το παλαιότερο και πιο διάσημο παράδειγμα χρήσης του κλασικού BPF, διαθέσιμο σε πολλά λειτουργικά συστήματα, θα ξεκινήσουμε τη μελέτη μας για την τεχνολογία με αυτό.

(Έτρεξα όλα τα παραδείγματα σε αυτό το άρθρο στο Linux 5.6.0-rc6. Η έξοδος ορισμένων εντολών έχει τροποποιηθεί για καλύτερη αναγνωσιμότητα.)

Παράδειγμα: παρατήρηση πακέτων IPv6

Ας φανταστούμε ότι θέλουμε να δούμε όλα τα πακέτα IPv6 σε μια διεπαφή eth0. Για να το κάνουμε αυτό μπορούμε να τρέξουμε το πρόγραμμα tcpdump με ένα απλό φίλτρο ip6:

$ sudo tcpdump -i eth0 ip6

Σε αυτή την περίπτωση, tcpdump μεταγλωττίζει το φίλτρο ip6 στον bytecode της αρχιτεκτονικής BPF και στείλτε τον στον πυρήνα (δείτε λεπτομέρειες στην ενότητα Tcpdump: φόρτωση). Το φορτωμένο φίλτρο θα εκτελείται για κάθε πακέτο που διέρχεται από τη διεπαφή eth0. Εάν το φίλτρο επιστρέψει μια μη μηδενική τιμή n, μετά μέχρι n byte του πακέτου θα αντιγραφούν στο χώρο χρήστη και θα το δούμε στην έξοδο tcpdump.

BPF για τα μικρά, μέρος μηδέν: κλασικό BPF

Αποδεικνύεται ότι μπορούμε εύκολα να βρούμε ποιο bytecode στάλθηκε στον πυρήνα tcpdump με τη βοήθεια του tcpdump, αν το τρέξουμε με την επιλογή -d:

$ sudo tcpdump -i eth0 -d ip6
(000) ldh      [12]
(001) jeq      #0x86dd          jt 2    jf 3
(002) ret      #262144
(003) ret      #0

Στη γραμμή μηδέν εκτελούμε την εντολή ldh [12], που σημαίνει «φόρτωση στο μητρώο A μισή λέξη (16 bit) που βρίσκεται στη διεύθυνση 12" και το μόνο ερώτημα είναι σε τι είδους μνήμη απευθυνόμαστε; Η απάντηση είναι ότι στο x αρχίζει (x+1)το byte του αναλυθέντος πακέτου δικτύου. Διαβάζουμε πακέτα από τη διεπαφή Ethernet eth0, και αυτό μέσαότι το πακέτο μοιάζει με αυτό (για απλότητα, υποθέτουμε ότι δεν υπάρχουν ετικέτες VLAN στο πακέτο):

       6              6          2
|Destination MAC|Source MAC|Ether Type|...|

Μετά την εκτέλεση λοιπόν της εντολής ldh [12] στο μητρώο A θα υπάρχει χωράφι Ether Type — τον τύπο του πακέτου που μεταδίδεται σε αυτό το πλαίσιο Ethernet. Στη γραμμή 1 συγκρίνουμε τα περιεχόμενα του μητρώου A (τύπος συσκευασίας) γ 0x86dd, και αυτό και έχετε Ο τύπος που μας ενδιαφέρει είναι IPv6. Στη γραμμή 1, εκτός από την εντολή σύγκρισης, υπάρχουν δύο ακόμη στήλες - jt 2 и jf 3 — σημάδια στα οποία πρέπει να πάτε εάν η σύγκριση είναι επιτυχής (A == 0x86dd) και ανεπιτυχής. Έτσι, σε μια επιτυχημένη περίπτωση (IPv6) πηγαίνουμε στη γραμμή 2 και σε μια ανεπιτυχή περίπτωση - στη γραμμή 3. Στη γραμμή 3 το πρόγραμμα τερματίζεται με τον κωδικό 0 (μην αντιγράψετε το πακέτο), στη γραμμή 2 το πρόγραμμα τερματίζεται με κωδικό 262144 (αντιγράψτε μου το μέγιστο πακέτο 256 kilobytes).

Ένα πιο περίπλοκο παράδειγμα: εξετάζουμε τα πακέτα TCP ανά θύρα προορισμού

Ας δούμε πώς μοιάζει ένα φίλτρο που αντιγράφει όλα τα πακέτα TCP με τη θύρα προορισμού 666. Θα εξετάσουμε την περίπτωση IPv4, καθώς η περίπτωση IPv6 είναι απλούστερη. Αφού μελετήσετε αυτό το παράδειγμα, μπορείτε να εξερευνήσετε μόνοι σας το φίλτρο IPv6 ως άσκηση (ip6 and tcp dst port 666) και ένα φίλτρο για τη γενική περίπτωση (tcp dst port 666). Έτσι, το φίλτρο που μας ενδιαφέρει μοιάζει με αυτό:

$ sudo tcpdump -i eth0 -d ip and tcp dst port 666
(000) ldh      [12]
(001) jeq      #0x800           jt 2    jf 10
(002) ldb      [23]
(003) jeq      #0x6             jt 4    jf 10
(004) ldh      [20]
(005) jset     #0x1fff          jt 10   jf 6
(006) ldxb     4*([14]&0xf)
(007) ldh      [x + 16]
(008) jeq      #0x29a           jt 9    jf 10
(009) ret      #262144
(010) ret      #0

Γνωρίζουμε ήδη τι κάνουν οι γραμμές 0 και 1. Στη γραμμή 2 έχουμε ήδη ελέγξει ότι πρόκειται για πακέτο IPv4 (Τύπος αιθέρα = 0x800) και φορτώστε το στο μητρώο A 24ο byte του πακέτου. Το πακέτο μας μοιάζει

       14            8      1     1
|ethernet header|ip fields|ttl|protocol|...|

που σημαίνει ότι φορτώνουμε στο μητρώο A το πεδίο Πρωτόκολλο της κεφαλίδας IP, το οποίο είναι λογικό, επειδή θέλουμε να αντιγράψουμε μόνο πακέτα TCP. Συγκρίνουμε το Πρωτόκολλο με 0x6 (IPPROTO_TCP) στη γραμμή 3.

Στις γραμμές 4 και 5 φορτώνουμε τις ημιλέξεις που βρίσκονται στη διεύθυνση 20 και χρησιμοποιούμε την εντολή jset ελέγξτε αν έχει ρυθμιστεί ένα από τα τρία σημαίες - φορώντας τη μάσκα που εκδόθηκε jset τα τρία πιο σημαντικά bit διαγράφονται. Δύο από τα τρία bit μας λένε εάν το πακέτο είναι μέρος ενός κατακερματισμένου πακέτου IP και εάν ναι, εάν είναι το τελευταίο τμήμα. Το τρίτο bit είναι δεσμευμένο και πρέπει να είναι μηδέν. Δεν θέλουμε να ελέγξουμε ούτε ημιτελή ούτε σπασμένα πακέτα, γι' αυτό ελέγχουμε και τα τρία bit.

Η γραμμή 6 είναι η πιο ενδιαφέρουσα σε αυτήν την καταχώριση. Εκφραση ldxb 4*([14]&0xf) σημαίνει ότι φορτώνουμε στο μητρώο X τα λιγότερο σημαντικά τέσσερα bit του δέκατου πέμπτου byte του πακέτου πολλαπλασιαζόμενα επί 4. Τα λιγότερο σημαντικά τέσσερα bit του δέκατου πέμπτου byte είναι το πεδίο Μήκος κεφαλίδας Διαδικτύου Επικεφαλίδα IPv4, η οποία αποθηκεύει το μήκος της κεφαλίδας σε λέξεις, επομένως πρέπει στη συνέχεια να πολλαπλασιάσετε με το 4. Είναι ενδιαφέρον ότι η έκφραση 4*([14]&0xf) είναι ένας προσδιορισμός για ένα ειδικό σχήμα διευθύνσεων που μπορεί να χρησιμοποιηθεί μόνο σε αυτήν τη φόρμα και μόνο για ένα μητρώο X, δηλ. ούτε μπορούμε να πούμε ldb 4*([14]&0xf) ή ldxb 5*([14]&0xf) (μπορούμε μόνο να καθορίσουμε διαφορετική μετατόπιση, για παράδειγμα, ldxb 4*([16]&0xf)). Είναι σαφές ότι αυτό το σύστημα διευθυνσιοδότησης προστέθηκε στο BPF ακριβώς για να λάβει X (καταχωρητής ευρετηρίου) Μήκος κεφαλίδας IPv4.

Έτσι, στη γραμμή 7 προσπαθούμε να φορτώσουμε μισή λέξη στο (X+16). Υπενθυμίζοντας ότι 14 byte καταλαμβάνονται από την κεφαλίδα Ethernet και X περιέχει το μήκος της κεφαλίδας IPv4, καταλαβαίνουμε ότι σε A Η θύρα προορισμού TCP φορτώνεται:

       14           X           2             2
|ethernet header|ip header|source port|destination port|

Τέλος, στη γραμμή 8 συγκρίνουμε τη θύρα προορισμού με την επιθυμητή τιμή και στις γραμμές 9 ή 10 επιστρέφουμε το αποτέλεσμα - αν θα αντιγράψουμε το πακέτο ή όχι.

Tcpdump: φόρτωση

Στα προηγούμενα παραδείγματα, δεν αναφερθήκαμε λεπτομερώς στο πώς ακριβώς φορτώνουμε τον bytecode BPF στον πυρήνα για φιλτράρισμα πακέτων. Γενικά μιλώντας, tcpdump μεταφέρεται σε πολλά συστήματα και για εργασία με φίλτρα tcpdump χρησιμοποιεί τη βιβλιοθήκη libpcap. Εν συντομία, για να τοποθετήσετε ένα φίλτρο σε μια διεπαφή χρησιμοποιώντας libpcap, πρέπει να κάνετε τα εξής:

  • δημιουργήστε έναν περιγραφέα τύπου pcap_t από το όνομα διεπαφής: pcap_create,
  • ενεργοποίηση διεπαφής: pcap_activate,
  • μεταγλώττιση φίλτρου: pcap_compile,
  • φίλτρο σύνδεσης: pcap_setfilter.

Για να δείτε πώς λειτουργεί pcap_setfilter υλοποιείται στο Linux, χρησιμοποιούμε strace (ορισμένες γραμμές έχουν αφαιρεθεί):

$ sudo strace -f -e trace=%network tcpdump -p -i eth0 ip
socket(AF_PACKET, SOCK_RAW, 768)        = 3
bind(3, {sa_family=AF_PACKET, sll_protocol=htons(ETH_P_ALL), sll_ifindex=if_nametoindex("eth0"), sll_hatype=ARPHRD_NETROM, sll_pkttype=PACKET_HOST, sll_halen=0}, 20) = 0
setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, {len=4, filter=0xb00bb00bb00b}, 16) = 0
...

Στις δύο πρώτες γραμμές εξόδου δημιουργούμε ακατέργαστη υποδοχή για να διαβάσετε όλα τα πλαίσια Ethernet και να τα συνδέσετε στη διεπαφή eth0. Από το πρώτο μας παράδειγμα ξέρουμε ότι το φίλτρο ip θα αποτελείται από τέσσερις οδηγίες BPF και στην τρίτη γραμμή βλέπουμε πώς χρησιμοποιείται η επιλογή SO_ATTACH_FILTER κλήση συστήματος setsockopt φορτώνουμε και συνδέουμε ένα φίλτρο μήκους 4. Αυτό είναι το φίλτρο μας.

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

Κρυμμένη Αλήθεια

Μια ελαφρώς πιο ολοκληρωμένη έκδοση της εξόδου μοιάζει με αυτό:

$ sudo strace -f -e trace=%network tcpdump -p -i eth0 ip
socket(AF_PACKET, SOCK_RAW, 768)        = 3
bind(3, {sa_family=AF_PACKET, sll_protocol=htons(ETH_P_ALL), sll_ifindex=if_nametoindex("eth0"), sll_hatype=ARPHRD_NETROM, sll_pkttype=PACKET_HOST, sll_halen=0}, 20) = 0
setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, {len=1, filter=0xbeefbeefbeef}, 16) = 0
recvfrom(3, 0x7ffcad394257, 1, MSG_TRUNC, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, {len=4, filter=0xb00bb00bb00b}, 16) = 0
...

Όπως αναφέρθηκε παραπάνω, φορτώνουμε και συνδέουμε το φίλτρο μας στην υποδοχή της γραμμής 5, αλλά τι συμβαίνει στις γραμμές 3 και 4; Αποδεικνύεται ότι αυτό libpcap φροντίζει για εμάς - έτσι ώστε η έξοδος του φίλτρου μας να μην περιλαμβάνει πακέτα που δεν το ικανοποιούν, η βιβλιοθήκη συνδέει εικονικό φίλτρο ret #0 (αποθέστε όλα τα πακέτα), αλλάζει την υποδοχή σε λειτουργία μη αποκλεισμού και προσπαθεί να αφαιρέσει όλα τα πακέτα που θα μπορούσαν να παραμείνουν από τα προηγούμενα φίλτρα.

Συνολικά, για να φιλτράρετε πακέτα στο Linux χρησιμοποιώντας το κλασικό BPF, πρέπει να έχετε ένα φίλτρο με τη μορφή δομής όπως struct sock_fprog και μια ανοιχτή πρίζα, μετά την οποία το φίλτρο μπορεί να συνδεθεί στην πρίζα χρησιμοποιώντας μια κλήση συστήματος setsockopt.

Είναι ενδιαφέρον ότι το φίλτρο μπορεί να συνδεθεί σε οποιαδήποτε πρίζα, όχι μόνο σε ακατέργαστο. Εδώ παράδειγμα ένα πρόγραμμα που αποκόπτει όλα εκτός από τα δύο πρώτα byte από όλα τα εισερχόμενα datagrams UDP. (Πρόσθεσα σχόλια στον κώδικα για να μην μπερδεύω το άρθρο.)

Περισσότερες λεπτομέρειες σχετικά με τη χρήση setsockopt για τη σύνδεση φίλτρων, βλ πρίζα (7), αλλά για τη σύνταξη των δικών σας φίλτρων όπως struct sock_fprog χωρίς βοήθεια tcpdump θα μιλήσουμε στην ενότητα Προγραμματισμός BPF με τα χέρια μας.

Το κλασικό BPF και ο XNUMXος αιώνας

Το BPF συμπεριλήφθηκε στο Linux το 1997 και παρέμεινε άλογο για μεγάλο χρονικό διάστημα libpcap χωρίς ιδιαίτερες αλλαγές (αλλαγές ειδικά για το Linux, φυσικά, Ήμασταν, αλλά δεν άλλαξαν την παγκόσμια εικόνα). Τα πρώτα σοβαρά σημάδια ότι η BPF θα εξελισσόταν ήρθαν το 2011, όταν ο Eric Dumazet πρότεινε patch, που προσθέτει τον μεταγλωττιστή Just In Time στον πυρήνα - έναν μεταφραστή για τη μετατροπή του bytecode BPF σε εγγενή x86_64 κώδικας.

Ο μεταγλωττιστής JIT ήταν ο πρώτος στην αλυσίδα των αλλαγών: το 2012 εμφανίστηκε δυνατότητα εγγραφής φίλτρων για σεξ, χρησιμοποιώντας BPF, τον Ιανουάριο του 2013 υπήρχε προστέθηκε μονάδα xt_bpf, που σας επιτρέπει να γράψετε κανόνες για iptables με τη βοήθεια της BPF, και τον Οκτώβριο του 2013 ήταν προστέθηκε επίσης μια ενότητα cls_bpf, το οποίο σας επιτρέπει να γράφετε ταξινομητές κυκλοφορίας χρησιμοποιώντας BPF.

Θα εξετάσουμε όλα αυτά τα παραδείγματα με περισσότερες λεπτομέρειες σύντομα, αλλά πρώτα θα είναι χρήσιμο για εμάς να μάθουμε πώς να γράφουμε και να μεταγλωττίζουμε αυθαίρετα προγράμματα για το BPF, καθώς οι δυνατότητες που παρέχει η βιβλιοθήκη libpcap περιορισμένο (απλό παράδειγμα: δημιουργήθηκε φίλτρο libpcap μπορεί να επιστρέψει μόνο δύο τιμές - 0 ή 0x40000) ή γενικά, όπως στην περίπτωση του seccomp, δεν ισχύουν.

Προγραμματισμός BPF με τα χέρια μας

Ας εξοικειωθούμε με τη δυαδική μορφή των οδηγιών BPF, είναι πολύ απλό:

   16    8    8     32
| code | jt | jf |  k  |

Κάθε εντολή καταλαμβάνει 64 bit, στα οποία τα πρώτα 16 bit είναι ο κώδικας εντολής, μετά υπάρχουν δύο εσοχές οκτώ bit, jt и jf, και 32 bit για το όρισμα K, ο σκοπός του οποίου διαφέρει από εντολή σε εντολή. Για παράδειγμα, η εντολή ret, που τερματίζει το πρόγραμμα έχει τον κωδικό 6, και η τιμή επιστροφής λαμβάνεται από τη σταθερά K. Στο C, μια μεμονωμένη εντολή BPF αναπαρίσταται ως δομή

struct sock_filter {
        __u16   code;
        __u8    jt;
        __u8    jf;
        __u32   k;
}

και όλο το πρόγραμμα έχει τη μορφή δομής

struct sock_fprog {
        unsigned short len;
        struct sock_filter *filter;
}

Έτσι, μπορούμε ήδη να γράφουμε προγράμματα (για παράδειγμα, γνωρίζουμε τους κωδικούς εντολών από [1]). Έτσι θα μοιάζει το φίλτρο ip6 του το πρώτο μας παράδειγμα:

struct sock_filter code[] = {
        { 0x28, 0, 0, 0x0000000c },
        { 0x15, 0, 1, 0x000086dd },
        { 0x06, 0, 0, 0x00040000 },
        { 0x06, 0, 0, 0x00000000 },
};
struct sock_fprog prog = {
        .len = ARRAY_SIZE(code),
        .filter = code,
};

πρόγραμμα prog μπορούμε να χρησιμοποιήσουμε νόμιμα σε μια κλήση

setsockopt(sk, SOL_SOCKET, SO_ATTACH_FILTER, &prog, sizeof(prog))

Η σύνταξη προγραμμάτων με τη μορφή κωδικών μηχανής δεν είναι πολύ βολική, αλλά μερικές φορές είναι απαραίτητη (για παράδειγμα, για εντοπισμό σφαλμάτων, δημιουργία δοκιμών μονάδας, σύνταξη άρθρων στο Habré κ.λπ.). Για ευκολία, στο αρχείο <linux/filter.h> Ορίζονται βοηθητικές μακροεντολές - το ίδιο παράδειγμα όπως παραπάνω θα μπορούσε να ξαναγραφτεί ως

struct sock_filter code[] = {
        BPF_STMT(BPF_LD|BPF_H|BPF_ABS, 12),
        BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, ETH_P_IPV6, 0, 1),
        BPF_STMT(BPF_RET|BPF_K, 0x00040000),
        BPF_STMT(BPF_RET|BPF_K, 0),
}

Ωστόσο, αυτή η επιλογή δεν είναι πολύ βολική. Αυτό σκέφτηκαν οι προγραμματιστές του πυρήνα του Linux, και επομένως στον κατάλογο tools/bpf στους πυρήνες μπορείτε να βρείτε έναν assembler και έναν εντοπισμό σφαλμάτων για εργασία με το κλασικό BPF.

Η γλώσσα συναρμολόγησης μοιάζει πολύ με την έξοδο εντοπισμού σφαλμάτων tcpdump, αλλά επιπλέον μπορούμε να καθορίσουμε συμβολικές ετικέτες. Για παράδειγμα, εδώ είναι ένα πρόγραμμα που απορρίπτει όλα τα πακέτα εκτός από το TCP/IPv4:

$ cat /tmp/tcp-over-ipv4.bpf
ldh [12]
jne #0x800, drop
ldb [23]
jneq #6, drop
ret #-1
drop: ret #0

Από προεπιλογή, το assembler δημιουργεί κώδικα στη μορφή <количество инструкций>,<code1> <jt1> <jf1> <k1>,..., για το παράδειγμά μας με το TCP θα είναι

$ tools/bpf/bpf_asm /tmp/tcp-over-ipv4.bpf
6,40 0 0 12,21 0 3 2048,48 0 0 23,21 0 1 6,6 0 0 4294967295,6 0 0 0,

Για τη διευκόλυνση των προγραμματιστών C, μπορεί να χρησιμοποιηθεί διαφορετική μορφή εξόδου:

$ tools/bpf/bpf_asm -c /tmp/tcp-over-ipv4.bpf
{ 0x28,  0,  0, 0x0000000c },
{ 0x15,  0,  3, 0x00000800 },
{ 0x30,  0,  0, 0x00000017 },
{ 0x15,  0,  1, 0x00000006 },
{ 0x06,  0,  0, 0xffffffff },
{ 0x06,  0,  0, 0000000000 },

Αυτό το κείμενο μπορεί να αντιγραφεί στον ορισμό της δομής τύπου struct sock_filter, όπως κάναμε στην αρχή αυτής της ενότητας.

Επεκτάσεις Linux και netsniff-ng

Εκτός από το τυπικό BPF, το Linux και tools/bpf/bpf_asm υποστήριξη και μη τυποποιημένο σύνολο. Βασικά, οι οδηγίες χρησιμοποιούνται για την πρόσβαση στα πεδία μιας δομής struct sk_buff, το οποίο περιγράφει ένα πακέτο δικτύου στον πυρήνα. Ωστόσο, υπάρχουν και άλλοι τύποι βοηθητικών οδηγιών, για παράδειγμα ldw cpu θα φορτωθεί στο μητρώο A αποτέλεσμα της εκτέλεσης μιας συνάρτησης πυρήνα raw_smp_processor_id(). (Στη νέα έκδοση του BPF, αυτές οι μη τυπικές επεκτάσεις έχουν επεκταθεί για να παρέχουν προγράμματα με ένα σύνολο βοηθών πυρήνα για πρόσβαση στη μνήμη, τις δομές και τη δημιουργία συμβάντων.) Ακολουθεί ένα ενδιαφέρον παράδειγμα φίλτρου στο οποίο αντιγράφουμε μόνο το κεφαλίδες πακέτων στο χώρο χρήστη χρησιμοποιώντας την επέκταση poff, μετατόπιση ωφέλιμου φορτίου:

ld poff
ret a

Οι επεκτάσεις BPF δεν μπορούν να χρησιμοποιηθούν σε tcpdump, αλλά αυτός είναι ένας καλός λόγος για να εξοικειωθείτε με το πακέτο βοηθητικών προγραμμάτων netsniff-ng, το οποίο, μεταξύ άλλων, περιέχει ένα προηγμένο πρόγραμμα netsniff-ng, το οποίο, εκτός από το φιλτράρισμα με χρήση BPF, περιέχει επίσης έναν αποτελεσματικό παράγοντα δημιουργίας κίνησης και πιο προηγμένο από tools/bpf/bpf_asm, κάλεσε ένας συναρμολογητής BPF bpfc. Το πακέτο περιέχει αρκετά λεπτομερή τεκμηρίωση, δείτε επίσης τους συνδέσμους στο τέλος του άρθρου.

σεξ

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

Η πρώτη έκδοση του seccomp προστέθηκε στον πυρήνα το 2005 και δεν ήταν πολύ δημοφιλής, καθώς παρείχε μόνο μία επιλογή - τον περιορισμό του συνόλου των κλήσεων συστήματος που είναι διαθέσιμες σε μια διεργασία στα εξής: read, write, exit и sigreturn, και η διαδικασία που παραβίαζε τους κανόνες σκοτώθηκε χρησιμοποιώντας SIGKILL. Ωστόσο, το 2012, το seccomp πρόσθεσε τη δυνατότητα χρήσης φίλτρων BPF, επιτρέποντάς σας να ορίσετε ένα σύνολο επιτρεπόμενων κλήσεων συστήματος και ακόμη και να πραγματοποιήσετε ελέγχους στα ορίσματά τους. (Είναι ενδιαφέρον ότι το Chrome ήταν ένας από τους πρώτους χρήστες αυτής της λειτουργίας και οι άνθρωποι του Chrome αναπτύσσουν αυτήν τη στιγμή έναν μηχανισμό KRSI που βασίζεται σε μια νέα έκδοση του BPF και επιτρέπει την προσαρμογή των λειτουργικών μονάδων ασφαλείας Linux.) Στο τέλος μπορείτε να βρείτε συνδέσμους για πρόσθετη τεκμηρίωση του άρθρου.

Σημειώστε ότι υπάρχουν ήδη άρθρα στο hub σχετικά με τη χρήση του seccomp, ίσως κάποιος θα θελήσει να τα διαβάσει πριν (ή αντί να διαβάσει) τις παρακάτω υποενότητες. Στο άρθρο Εμπορευματοκιβώτια και ασφάλεια: seccomp παρέχει παραδείγματα χρήσης του seccomp, τόσο της έκδοσης 2007 όσο και της έκδοσης που χρησιμοποιεί BPF (τα φίλτρα δημιουργούνται χρησιμοποιώντας το libseccomp), μιλά για τη σύνδεση του seccomp με το Docker και παρέχει επίσης πολλούς χρήσιμους συνδέσμους. Στο άρθρο Απομόνωση δαιμόνων με systemd ή "δεν χρειάζεσαι το Docker για αυτό!" Καλύπτει, ειδικότερα, τον τρόπο προσθήκης μαύρων λιστών ή λευκών λιστών κλήσεων συστήματος για δαίμονες που εκτελούν το systemd.

Στη συνέχεια θα δούμε πώς γράφουμε και φορτώνουμε φίλτρα για seccomp στο γυμνό C και χρησιμοποιώντας τη βιβλιοθήκη libseccomp και ποια είναι τα πλεονεκτήματα και τα μειονεκτήματα κάθε επιλογής και, τέλος, ας δούμε πώς χρησιμοποιείται το seccomp από το πρόγραμμα strace.

Εγγραφή και φόρτωση φίλτρων για το seccomp

Γνωρίζουμε ήδη πώς να γράφουμε προγράμματα BPF, οπότε ας δούμε πρώτα τη διεπαφή προγραμματισμού seccomp. Μπορείτε να ορίσετε ένα φίλτρο σε επίπεδο διεργασίας και όλες οι θυγατρικές διεργασίες θα κληρονομήσουν τους περιορισμούς. Αυτό γίνεται χρησιμοποιώντας μια κλήση συστήματος seccomp(2):

seccomp(SECCOMP_SET_MODE_FILTER, flags, &filter)

όπου &filter - αυτός είναι ένας δείκτης σε μια δομή που είναι ήδη γνωστή σε εμάς struct sock_fprog, δηλ. Πρόγραμμα BPF.

Πώς διαφέρουν τα προγράμματα για το seccomp από τα προγράμματα για πρίζες; Μεταδιδόμενο πλαίσιο. Στην περίπτωση των υποδοχών, μας δόθηκε μια περιοχή μνήμης που περιέχει το πακέτο και στην περίπτωση του seccomp μας δόθηκε μια δομή όπως

struct seccomp_data {
    int   nr;
    __u32 arch;
    __u64 instruction_pointer;
    __u64 args[6];
};

Εδώ nr είναι ο αριθμός της κλήσης συστήματος που πρόκειται να ξεκινήσει, arch - τρέχουσα αρχιτεκτονική (περισσότερα για αυτό παρακάτω), args - έως έξι ορίσματα κλήσεων συστήματος και instruction_pointer είναι ένας δείκτης στην εντολή χώρου χρήστη που έκανε την κλήση του συστήματος. Έτσι, για παράδειγμα, για να φορτώσετε τον αριθμό κλήσης συστήματος στο μητρώο A έχουμε να πούμε

ldw [0]

Υπάρχουν άλλες δυνατότητες για τα προγράμματα seccomp, για παράδειγμα, η πρόσβαση στο περιβάλλον είναι δυνατή μόνο με ευθυγράμμιση 32 bit και δεν μπορείτε να φορτώσετε μισή λέξη ή byte - όταν προσπαθείτε να φορτώσετε ένα φίλτρο ldh [0] κλήση συστήματος seccomp θα επιστρέψει EINVAL. Η λειτουργία ελέγχει τα φορτωμένα φίλτρα seccomp_check_filter() πυρήνες. (Το αστείο είναι ότι στο αρχικό commit που πρόσθεσε τη λειτουργία seccomp, ξέχασαν να προσθέσουν άδεια χρήσης της εντολής σε αυτήν τη συνάρτηση mod (υπόλοιπο διαίρεσης) και δεν είναι πλέον διαθέσιμο για προγράμματα seccomp BPF, από την προσθήκη του θα σπάσει ABI.)

Βασικά, γνωρίζουμε ήδη τα πάντα για να γράφουμε και να διαβάζουμε προγράμματα seccomp. Συνήθως η λογική του προγράμματος διατάσσεται ως λευκή ή μαύρη λίστα κλήσεων συστήματος, για παράδειγμα το πρόγραμμα

ld [0]
jeq #304, bad
jeq #176, bad
jeq #239, bad
jeq #279, bad
good: ret #0x7fff0000 /* SECCOMP_RET_ALLOW */
bad: ret #0

ελέγχει μια μαύρη λίστα τεσσάρων κλήσεων συστήματος με αριθμό 304, 176, 239, 279. Τι είναι αυτές οι κλήσεις συστήματος; Δεν μπορούμε να πούμε με βεβαιότητα, αφού δεν γνωρίζουμε για ποια αρχιτεκτονική γράφτηκε το πρόγραμμα. Ως εκ τούτου, οι συντάκτες του seccomp προσφορά ξεκινήστε όλα τα προγράμματα με έλεγχο αρχιτεκτονικής (η τρέχουσα αρχιτεκτονική υποδεικνύεται στο πλαίσιο ως πεδίο arch τη δομή struct seccomp_data). Με επιλεγμένη την αρχιτεκτονική, η αρχή του παραδείγματος θα μοιάζει με αυτό:

ld [4]
jne #0xc000003e, bad_arch ; SCMP_ARCH_X86_64

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

Γράφουμε και φορτώνουμε φίλτρα για το seccomp χρησιμοποιώντας libseccomp

Η εγγραφή φίλτρων σε εγγενή κώδικα ή σε συναρμολόγηση BPF σάς επιτρέπει να έχετε τον πλήρη έλεγχο του αποτελέσματος, αλλά ταυτόχρονα, μερικές φορές είναι προτιμότερο να έχετε φορητό ή/και αναγνώσιμο κώδικα. Η βιβλιοθήκη θα μας βοηθήσει σε αυτό libseccomp, το οποίο παρέχει μια τυπική διεπαφή για τη σύνταξη μαύρων ή λευκών φίλτρων.

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

#include <seccomp.h>
#include <unistd.h>
#include <err.h>

static int sys_numbers[] = {
        __NR_mount,
        __NR_umount2,
       // ... еще 40 системных вызовов ...
        __NR_vmsplice,
        __NR_perf_event_open,
};

int main(int argc, char **argv)
{
        scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);

        for (size_t i = 0; i < sizeof(sys_numbers)/sizeof(sys_numbers[0]); i++)
                seccomp_rule_add(ctx, SCMP_ACT_TRAP, sys_numbers[i], 0);

        seccomp_load(ctx);

        execvp(argv[1], &argv[1]);
        err(1, "execlp: %s", argv[1]);
}

Αρχικά ορίζουμε έναν πίνακα sys_numbers από 40+ αριθμούς κλήσεων συστήματος για αποκλεισμό. Στη συνέχεια, αρχικοποιήστε το πλαίσιο ctx και πείτε στη βιβλιοθήκη τι θέλουμε να επιτρέψουμε (SCMP_ACT_ALLOW) όλες οι κλήσεις συστήματος από προεπιλογή (είναι πιο εύκολο να δημιουργήσετε μαύρες λίστες). Στη συνέχεια, μία προς μία, προσθέτουμε όλες τις κλήσεις συστήματος από τη μαύρη λίστα. Σε απόκριση σε μια κλήση συστήματος από τη λίστα, ζητάμε SCMP_ACT_TRAP, σε αυτήν την περίπτωση το seccomp θα στείλει ένα σήμα στη διαδικασία SIGSYS με περιγραφή του ποια κλήση συστήματος παραβίασε τους κανόνες. Τέλος, φορτώνουμε το πρόγραμμα στον πυρήνα χρησιμοποιώντας seccomp_load, το οποίο θα μεταγλωττίσει το πρόγραμμα και θα το επισυνάψει στη διαδικασία χρησιμοποιώντας μια κλήση συστήματος seccomp(2).

Για επιτυχή μεταγλώττιση, το πρόγραμμα πρέπει να είναι συνδεδεμένο με τη βιβλιοθήκη libseccomp, για παράδειγμα:

cc -std=c17 -Wall -Wextra -c -o seccomp_lib.o seccomp_lib.c
cc -o seccomp_lib seccomp_lib.o -lseccomp

Παράδειγμα επιτυχημένης εκκίνησης:

$ ./seccomp_lib echo ok
ok

Παράδειγμα αποκλεισμένης κλήσης συστήματος:

$ sudo ./seccomp_lib mount -t bpf bpf /tmp
Bad system call

Χρησιμοποιούμε straceγια λεπτομέρειες:

$ sudo strace -e seccomp ./seccomp_lib mount -t bpf bpf /tmp
seccomp(SECCOMP_SET_MODE_FILTER, 0, {len=50, filter=0x55d8e78428e0}) = 0
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0xboobdeadbeef, si_syscall=__NR_mount, si_arch=AUDIT_ARCH_X86_64} ---
+++ killed by SIGSYS (core dumped) +++
Bad system call

πώς μπορούμε να γνωρίζουμε ότι το πρόγραμμα τερματίστηκε λόγω της χρήσης παράνομης κλήσης συστήματος mount(2).

Έτσι, γράψαμε ένα φίλτρο χρησιμοποιώντας τη βιβλιοθήκη libseccomp, χωρώντας τον μη τετριμμένο κώδικα σε τέσσερις γραμμές. Στο παραπάνω παράδειγμα, εάν υπάρχει μεγάλος αριθμός κλήσεων συστήματος, ο χρόνος εκτέλεσης μπορεί να μειωθεί αισθητά, καθώς ο έλεγχος είναι απλώς μια λίστα συγκρίσεων. Για βελτιστοποίηση, το libseccomp είχε πρόσφατα περιλαμβάνεται έμπλαστρο, το οποίο προσθέτει υποστήριξη για το χαρακτηριστικό φίλτρου SCMP_FLTATR_CTL_OPTIMIZE. Η ρύθμιση αυτού του χαρακτηριστικού σε 2 θα μετατρέψει το φίλτρο σε ένα πρόγραμμα δυαδικής αναζήτησης.

Αν θέλετε να δείτε πώς λειτουργούν τα δυαδικά φίλτρα αναζήτησης, ρίξτε μια ματιά απλό σενάριο, το οποίο δημιουργεί τέτοια προγράμματα στη συναρμολόγηση BPF καλώντας αριθμούς κλήσης συστήματος, για παράδειγμα:

$ echo 1 3 6 8 13 | ./generate_bin_search_bpf.py
ld [0]
jeq #6, bad
jgt #6, check8
jeq #1, bad
jeq #3, bad
ret #0x7fff0000
check8:
jeq #8, bad
jeq #13, bad
ret #0x7fff0000
bad: ret #0

Δεν θα μπορείτε να γράψετε τίποτα πολύ πιο γρήγορα, καθώς τα προγράμματα BPF δεν μπορούν να εκτελέσουν άλματα εσοχών (δεν μπορούμε να κάνουμε, για παράδειγμα, jmp A ή jmp [label+X]) και επομένως όλες οι μεταβάσεις είναι στατικές.

seccomp και strace

Όλοι γνωρίζουν τη χρησιμότητα strace είναι ένα απαραίτητο εργαλείο για τη μελέτη της συμπεριφοράς των διαδικασιών στο Linux. Ωστόσο, πολλοί έχουν επίσης ακούσει για ζητήματα επιδόσεων όταν χρησιμοποιείτε αυτό το βοηθητικό πρόγραμμα. Γεγονός είναι ότι strace υλοποιείται χρησιμοποιώντας ptrace(2), και σε αυτόν τον μηχανισμό δεν μπορούμε να προσδιορίσουμε σε ποιο σύνολο κλήσεων συστήματος χρειαζόμαστε για να σταματήσουμε τη διαδικασία, δηλαδή, για παράδειγμα, εντολές

$ time strace du /usr/share/ >/dev/null 2>&1

real    0m3.081s
user    0m0.531s
sys     0m2.073s

и

$ time strace -e open du /usr/share/ >/dev/null 2>&1

real    0m2.404s
user    0m0.193s
sys     0m1.800s

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

Νέα επιλογή --seccomp-bpf, προστέθηκε strace έκδοση 5.3, σας επιτρέπει να επιταχύνετε τη διαδικασία πολλές φορές και ο χρόνος εκκίνησης υπό το ίχνος μιας κλήσης συστήματος είναι ήδη συγκρίσιμος με τον χρόνο μιας κανονικής εκκίνησης:

$ time strace --seccomp-bpf -e open du /usr/share/ >/dev/null 2>&1

real    0m0.148s
user    0m0.017s
sys     0m0.131s

$ time du /usr/share/ >/dev/null 2>&1

real    0m0.140s
user    0m0.024s
sys     0m0.116s

(Εδώ, φυσικά, υπάρχει μια μικρή εξαπάτηση στο ότι δεν ανιχνεύουμε την κύρια κλήση συστήματος αυτής της εντολής. Αν ιχνηλάτησαμε, για παράδειγμα, newfsstat, Στη συνέχεια strace θα φρενάρει το ίδιο δυνατά όπως και χωρίς --seccomp-bpf.)

Πώς λειτουργεί αυτή η επιλογή; Χωρίς αυτήν strace συνδέεται με τη διαδικασία και ξεκινά τη χρήση της PTRACE_SYSCALL. Όταν μια διαχειριζόμενη διεργασία εκδίδει μια (οποιαδήποτε) κλήση συστήματος, ο έλεγχος μεταφέρεται σε strace, το οποίο εξετάζει τα ορίσματα της κλήσης συστήματος και το εκτελεί με PTRACE_SYSCALL. Μετά από κάποιο χρονικό διάστημα, η διαδικασία ολοκληρώνει την κλήση συστήματος και κατά την έξοδο από αυτήν, ο έλεγχος μεταφέρεται ξανά strace, το οποίο εξετάζει τις επιστρεφόμενες τιμές και ξεκινά τη διαδικασία χρησιμοποιώντας PTRACE_SYSCALL, και ούτω καθεξής.

BPF για τα μικρά, μέρος μηδέν: κλασικό BPF

Με το seccomp, ωστόσο, αυτή η διαδικασία μπορεί να βελτιστοποιηθεί ακριβώς όπως θα θέλαμε. Δηλαδή, αν θέλουμε να δούμε μόνο την κλήση συστήματος X, τότε μπορούμε να γράψουμε ένα φίλτρο BPF που για X επιστρέφει μια τιμή SECCOMP_RET_TRACE, και για κλήσεις που δεν μας ενδιαφέρουν - SECCOMP_RET_ALLOW:

ld [0]
jneq #X, ignore
trace: ret #0x7ff00000
ignore: ret #0x7fff0000

Σε αυτή την περίπτωση strace ξεκινά αρχικά τη διαδικασία ως PTRACE_CONT, το φίλτρο μας υποβάλλεται σε επεξεργασία για κάθε κλήση συστήματος, εάν η κλήση συστήματος δεν είναι X, τότε η διαδικασία συνεχίζει να εκτελείται, αλλά αν αυτό X, τότε το seccomp θα μεταφέρει τον έλεγχο straceτο οποίο θα εξετάσει τα επιχειρήματα και θα ξεκινήσει τη διαδικασία όπως PTRACE_SYSCALL (καθώς το seccomp δεν έχει τη δυνατότητα να εκτελέσει ένα πρόγραμμα κατά την έξοδο από μια κλήση συστήματος). Όταν επιστρέψει η κλήση συστήματος, strace θα επανεκκινήσει τη διαδικασία χρησιμοποιώντας PTRACE_CONT και θα περιμένει νέα μηνύματα από το seccomp.

BPF για τα μικρά, μέρος μηδέν: κλασικό BPF

Όταν χρησιμοποιείτε την επιλογή --seccomp-bpf υπάρχουν δύο περιορισμοί. Πρώτον, δεν θα είναι δυνατή η συμμετοχή σε μια ήδη υπάρχουσα διαδικασία (επιλογή -p προγράμματα strace), καθώς αυτό δεν υποστηρίζεται από το seccomp. Δεύτερον, δεν υπάρχει δυνατότητα όχι κοιτάξτε τις θυγατρικές διεργασίες, καθώς τα φίλτρα seccomp κληρονομούνται από όλες τις θυγατρικές διεργασίες χωρίς τη δυνατότητα απενεργοποίησης αυτού.

Λίγο περισσότερες λεπτομέρειες για το πώς ακριβώς strace δουλεύει με seccomp μπορεί να βρεθεί από πρόσφατη αναφορά. Για εμάς, το πιο ενδιαφέρον γεγονός είναι ότι το κλασικό BPF που αντιπροσωπεύεται από το seccomp χρησιμοποιείται ακόμα και σήμερα.

xt_bpf

Ας επιστρέψουμε τώρα στον κόσμο των δικτύων.

Ιστορικό: πριν από πολύ καιρό, το 2007, ο πυρήνας ήταν προστέθηκε μονάδα xt_u32 για φίλτρο δικτύου. Γράφτηκε κατ' αναλογία με έναν ακόμη πιο αρχαίο ταξινομητή κυκλοφορίας cls_u32 και σας επέτρεψε να γράψετε αυθαίρετους δυαδικούς κανόνες για iptable χρησιμοποιώντας τις ακόλουθες απλές πράξεις: φορτώστε 32 bit από ένα πακέτο και εκτελέστε ένα σύνολο αριθμητικών πράξεων σε αυτά. Για παράδειγμα,

sudo iptables -A INPUT -m u32 --u32 "6&0xFF=1" -j LOG --log-prefix "seen-by-xt_u32"

Φορτώνει τα 32 bit της κεφαλίδας IP, ξεκινώντας από το padding 6, και εφαρμόζει μια μάσκα σε αυτά 0xFF (πάρτε το χαμηλό byte). Αυτό το πεδίο protocol Επικεφαλίδα IP και τη συγκρίνουμε με 1 (ICMP). Μπορείτε να συνδυάσετε πολλούς ελέγχους σε έναν κανόνα και μπορείτε επίσης να εκτελέσετε τον τελεστή @ — μετακινήστε X byte προς τα δεξιά. Για παράδειγμα, ο κανόνας

iptables -m u32 --u32 "6&0xFF=0x6 && 0>>22&0x3C@4=0x29"

ελέγχει εάν ο αριθμός ακολουθίας TCP δεν είναι ίσος 0x29. Δεν θα υπεισέλθω σε λεπτομέρειες, καθώς είναι ήδη σαφές ότι η σύνταξη τέτοιων κανόνων με το χέρι δεν είναι πολύ βολική. Στο άρθρο BPF - ο ξεχασμένος bytecode, υπάρχουν αρκετοί σύνδεσμοι με παραδείγματα χρήσης και δημιουργίας κανόνων για xt_u32. Δείτε επίσης τους συνδέσμους στο τέλος αυτού του άρθρου.

Από το 2013 ενότητα αντί για ενότητα xt_u32 μπορείτε να χρησιμοποιήσετε μια μονάδα που βασίζεται στο BPF xt_bpf. Όποιος έχει διαβάσει μέχρι εδώ θα πρέπει να είναι ήδη ξεκάθαρος σχετικά με την αρχή της λειτουργίας του: εκτελέστε το BPF bytecode ως κανόνες iptables. Μπορείτε να δημιουργήσετε έναν νέο κανόνα, για παράδειγμα, ως εξής:

iptables -A INPUT -m bpf --bytecode <байткод> -j LOG

εδώ <байткод> - αυτός είναι ο κώδικας σε μορφή εξόδου assembler bpf_asm από προεπιλογή, για παράδειγμα,

$ cat /tmp/test.bpf
ldb [9]
jneq #17, ignore
ret #1
ignore: ret #0

$ bpf_asm /tmp/test.bpf
4,48 0 0 9,21 0 1 17,6 0 0 1,6 0 0 0,

# iptables -A INPUT -m bpf --bytecode "$(bpf_asm /tmp/test.bpf)" -j LOG

Σε αυτό το παράδειγμα φιλτράρουμε όλα τα πακέτα UDP. Πλαίσιο για ένα πρόγραμμα BPF σε μια ενότητα xt_bpfΤο , φυσικά, δείχνει στα δεδομένα του πακέτου, στην περίπτωση των iptables, στην αρχή της κεφαλίδας IPv4. Επιστρεφόμενη τιμή από το πρόγραμμα BPF booleanΌπου false σημαίνει ότι το πακέτο δεν ταιριάζει.

Είναι σαφές ότι η ενότητα xt_bpf υποστηρίζει πιο σύνθετα φίλτρα από το παραπάνω παράδειγμα. Ας δούμε πραγματικά παραδείγματα από το Cloudfare. Μέχρι πρόσφατα χρησιμοποιούσαν τη μονάδα xt_bpf για προστασία από επιθέσεις DDoS. Στο άρθρο Παρουσίαση των Εργαλείων BPF εξηγούν πώς (και γιατί) δημιουργούν φίλτρα BPF και δημοσιεύουν συνδέσμους σε ένα σύνολο βοηθητικών προγραμμάτων για τη δημιουργία τέτοιων φίλτρων. Για παράδειγμα, χρησιμοποιώντας το βοηθητικό πρόγραμμα bpfgen μπορείτε να δημιουργήσετε ένα πρόγραμμα BPF που ταιριάζει με ένα ερώτημα DNS για ένα όνομα habr.com:

$ ./bpfgen --assembly dns -- habr.com
ldx 4*([0]&0xf)
ld #20
add x
tax

lb_0:
    ld [x + 0]
    jneq #0x04686162, lb_1
    ld [x + 4]
    jneq #0x7203636f, lb_1
    ldh [x + 8]
    jneq #0x6d00, lb_1
    ret #65535

lb_1:
    ret #0

Στο πρόγραμμα φορτώνουμε πρώτα στον καταχωρητή X διεύθυνση έναρξης γραμμής x04habrx03comx00 μέσα σε ένα datagram UDP και, στη συνέχεια, ελέγξτε το αίτημα: 0x04686162 <-> "x04hab" κλπ.

Λίγο αργότερα, το Cloudfare δημοσίευσε τον κώδικα μεταγλωττιστή p0f -> BPF. Στο άρθρο Παρουσιάζουμε τον μεταγλωττιστή p0f BPF μιλούν για το τι είναι το p0f και πώς να μετατρέψετε τις υπογραφές p0f σε BPF:

$ ./bpfgen p0f -- 4:64:0:0:*,0::ack+:0
39,0 0 0 0,48 0 0 8,37 35 0 64,37 0 34 29,48 0 0 0,
84 0 0 15,21 0 31 5,48 0 0 9,21 0 29 6,40 0 0 6,
...

Αυτήν τη στιγμή δεν χρησιμοποιείται πλέον το Cloudfare xt_bpf, δεδομένου ότι μετακόμισαν στο XDP - μία από τις επιλογές για τη χρήση της νέας έκδοσης του BPF, βλ. L4Drop: XDP DDoS Mitigations.

cls_bpf

Το τελευταίο παράδειγμα χρήσης του κλασικού BPF στον πυρήνα είναι ο ταξινομητής cls_bpf για το υποσύστημα ελέγχου κυκλοφορίας στο Linux, που προστέθηκε στο Linux στα τέλη του 2013 και αντικαθιστά εννοιολογικά το αρχαίο cls_u32.

Ωστόσο, δεν θα περιγράψουμε τώρα το έργο cls_bpf, αφού από την άποψη της γνώσης για το κλασικό BPF αυτό δεν θα μας δώσει τίποτα - έχουμε ήδη εξοικειωθεί με όλη τη λειτουργικότητα. Επιπλέον, σε επόμενα άρθρα που μιλούν για το Extended BPF, θα συναντήσουμε αυτόν τον ταξινομητή περισσότερες από μία φορές.

Ένας άλλος λόγος για να μην μιλήσουμε για τη χρήση του κλασικού BPF c cls_bpf Το πρόβλημα είναι ότι, σε σύγκριση με το Extended BPF, το πεδίο εφαρμογής σε αυτήν την περίπτωση είναι ριζικά περιορισμένο: τα κλασικά προγράμματα δεν μπορούν να αλλάξουν τα περιεχόμενα των πακέτων και δεν μπορούν να αποθηκεύσουν κατάσταση μεταξύ των κλήσεων.

Ήρθε λοιπόν η ώρα να αποχαιρετήσουμε το κλασικό BPF και να κοιτάξουμε στο μέλλον.

Αντίο στο κλασικό BPF

Εξετάσαμε πώς η τεχνολογία BPF, που αναπτύχθηκε στις αρχές της δεκαετίας του '32, έζησε με επιτυχία για ένα τέταρτο του αιώνα και μέχρι το τέλος βρήκε νέες εφαρμογές. Ωστόσο, παρόμοια με τη μετάβαση από τις μηχανές στοίβας στο RISC, που χρησίμευσε ως ώθηση για την ανάπτυξη του κλασικού BPF, τη δεκαετία του 64 έγινε μια μετάβαση από μηχανές XNUMX-bit σε μηχανές XNUMX-bit και το κλασικό BPF άρχισε να γίνεται ξεπερασμένο. Επιπλέον, οι δυνατότητες του κλασικού BPF είναι πολύ περιορισμένες και εκτός από την ξεπερασμένη αρχιτεκτονική - δεν έχουμε τη δυνατότητα αποθήκευσης κατάστασης μεταξύ κλήσεων σε προγράμματα BPF, δεν υπάρχει δυνατότητα άμεσης αλληλεπίδρασης με τον χρήστη, δεν υπάρχει δυνατότητα αλληλεπίδρασης με τον πυρήνα, εκτός από την ανάγνωση περιορισμένου αριθμού πεδίων δομής sk_buff και εκκινώντας τις απλούστερες βοηθητικές λειτουργίες, δεν μπορείτε να αλλάξετε τα περιεχόμενα των πακέτων και να τα ανακατευθύνετε.

Στην πραγματικότητα, επί του παρόντος το μόνο που απομένει από το κλασικό BPF στο Linux είναι η διεπαφή API, και μέσα στον πυρήνα όλα τα κλασικά προγράμματα, είτε πρόκειται για φίλτρα υποδοχής είτε φίλτρα seccomp, μεταφράζονται αυτόματα σε μια νέα μορφή, το Extended BPF. (Θα μιλήσουμε για το πώς ακριβώς συμβαίνει αυτό στο επόμενο άρθρο.)

Η μετάβαση σε μια νέα αρχιτεκτονική ξεκίνησε το 2013, όταν ο Alexey Starovoitov πρότεινε ένα πρόγραμμα ενημέρωσης BPF. Το 2014 τα αντίστοιχα patches άρχισε να εμφανίζεται στον πυρήνα. Από όσο καταλαβαίνω, το αρχικό σχέδιο ήταν μόνο η βελτιστοποίηση της αρχιτεκτονικής και του μεταγλωττιστή JIT ώστε να εκτελούνται πιο αποτελεσματικά σε μηχανές 64-bit, αλλά αντίθετα αυτές οι βελτιστοποιήσεις σηματοδοτούσαν την αρχή ενός νέου κεφαλαίου στην ανάπτυξη Linux.

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

παραπομπές

  1. Steven McCanne και Van Jacobson, "The BSD Packet Filter: A New Architecture for User-level Packet Capture", https://www.tcpdump.org/papers/bpf-usenix93.pdf
  2. Steven McCanne, "libpcap: An Architecture and Optimization Methodology for Packet Capture", https://sharkfestus.wireshark.org/sharkfest.11/presentations/McCanne-Sharkfest'11_Keynote_Address.pdf
  3. tcpdump, libpcap: https://www.tcpdump.org/
  4. Εκμάθηση αγώνα IPtable U32.
  5. BPF - ο ξεχασμένος bytecode: https://blog.cloudflare.com/bpf-the-forgotten-bytecode/
  6. Παρουσιάζοντας το εργαλείο BPF: https://blog.cloudflare.com/introducing-the-bpf-tools/
  7. bpf_cls: http://man7.org/linux/man-pages/man8/tc-bpf.8.html
  8. Μια δεύτερη επισκόπηση: https://lwn.net/Articles/656307/
  9. https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/seccomp_filter.rst
  10. habr: Εμπορευματοκιβώτια και ασφάλεια: seccomp
  11. habr: Απομόνωση δαιμόνων με systemd ή "δεν χρειάζεσαι το Docker για αυτό!"
  12. Paul Chaignon, "strace --seccomp-bpf: a look under the hood", https://fosdem.org/2020/schedule/event/debugging_strace_bpf/
  13. netsniff-ng: http://netsniff-ng.org/

Πηγή: www.habr.com

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