Бяцхан хүүхдүүдэд зориулсан BPF, тэг хэсэг: сонгодог BPF

Berkeley Packet Filters (BPF) нь Линуксийн цөмийн технологи бөгөөд англи хэл дээрх технологийн хэвлэлүүдийн нүүрэнд хэдэн жилийн турш байсаар ирсэн. Бага хурал нь BPF-ийн хэрэглээ, хөгжлийн талаархи тайлангаар дүүрэн байдаг. Линукс сүлжээний дэд системийн засварлагч Дэвид Миллер Linux Plumbers 2018 дээр илтгэл тавьж байна. "Энэ яриа XDP-ийн тухай биш" (XDP бол BPF-ийн хэрэглээний нэг тохиолдол юм). Брендан Грегг дуудаж илтгэл тавьж байна Linux BPF Superpowers. Токе Хойланд-Йоргенсен инээвцөм нь одоо микро цөм болсон. Энэ санааг Томас Граф дэмжиж байна BPF нь цөмд зориулсан javascript юм.

Habré дээрх BPF-ийн талаар системчилсэн тайлбар хараахан гараагүй байгаа тул би цуврал нийтлэлд технологийн түүхийн талаар ярих, архитектур, хөгжлийн хэрэгслүүдийг тайлбарлах, BPF-ийн хэрэглээний талбар, практикийг тоймлохыг хичээх болно. Цуврал дахь "XNUMX" нийтлэл нь сонгодог BPF-ийн түүх, архитектурын тухай өгүүлэхээс гадна түүний үйл ажиллагааны зарчмуудын нууцыг илчилдэг. tcpdump, seccomp, strace, болон бусад олон.

BPF-ийн хөгжлийг Линукс сүлжээний нийгэмлэг хянадаг бөгөөд BPF-ийн одоо байгаа үндсэн програмууд нь сүлжээнүүдтэй холбоотой байдаг тул зөвшөөрөлтэй байдаг. @eucariot, Би цувралыг "Бяцхан хүүхдүүдэд зориулсан BPF" гэж нэрлэсэн "Бяцхан хүүхдүүдэд зориулсан сүлжээ".

BPF-ийн түүхийн богино курс(c)

Орчин үеийн BPF технологи нь төөрөгдлөөс зайлсхийхийн тулд одоо сонгодог BPF гэж нэрлэгддэг ижил нэртэй хуучин технологийн сайжруулсан, өргөтгөсөн хувилбар юм. Сонгодог BPF дээр үндэслэн алдартай хэрэгсэл бүтээгдсэн tcpdump, механизм seccomp, түүнчлэн бага мэддэг модулиуд xt_bpf нь iptables ба ангилагч cls_bpf. Орчин үеийн Линукс дээр сонгодог BPF програмууд автоматаар шинэ хэлбэрт хөрвүүлэгддэг боловч хэрэглэгчийн үүднээс авч үзвэл API хэвээр байгаа бөгөөд сонгодог BPF-ийн шинэ хэрэглээг бид энэ нийтлэлээс үзэх болно. Энэ шалтгааны улмаас, мөн Линукс дээр сонгодог BPF-ийн хөгжлийн түүхийг дагаж энэ нь хэрхэн, яагаад орчин үеийн хэлбэрт шилжсэн нь илүү тодорхой болох тул би сонгодог BPF-ийн тухай өгүүллээр эхлэхээр шийдлээ.

Өнгөрсөн зууны наяад оны сүүлээр алдарт Лоуренс Беркли лабораторийн инженерүүд өнгөрсөн зууны наяад оны сүүлчээр орчин үеийн техник хангамж дээр сүлжээний пакетуудыг хэрхэн зөв шүүх вэ гэсэн асуултыг сонирхож эхэлсэн. CSPF (CMU/Stanford Packet Filter) технологид анх хэрэглэгдэж байсан шүүлтийн үндсэн санаа нь шаардлагагүй пакетуудыг аль болох эрт шүүх явдал байв. цөмийн орон зайд, учир нь энэ нь шаардлагагүй өгөгдлийг хэрэглэгчийн орон зайд хуулахаас зайлсхийдэг. Цөмийн зайд хэрэглэгчийн кодыг ажиллуулахын тулд ажиллах үеийн аюулгүй байдлыг хангахын тулд хамгаалагдсан хязгаарлагдмал виртуал машин ашигласан.

Гэсэн хэдий ч одоо байгаа шүүлтүүрт зориулсан виртуал машинууд нь стек дээр суурилсан машинууд дээр ажиллахаар бүтээгдсэн бөгөөд шинэ RISC машинууд дээр тийм ч үр дүнтэй ажилладаггүй. Үүний үр дүнд Berkeley Labs-ийн инженерүүдийн хүчин чармайлтаар BPF (Berkeley Packet Filters) шинэ технологийг боловсруулж, виртуал машины архитектурыг Motorola 6502 процессор - зэрэг алдартай бүтээгдэхүүнүүдийн үндсэн дээр бүтээсэн. Apple II буюу зэмсгүүд. Шинэ виртуал машин нь шүүлтүүрийн гүйцэтгэлийг одоо байгаа шийдлүүдтэй харьцуулахад хэдэн арван дахин нэмэгдүүлсэн.

BPF машины бүтэц

Бид жишээн дээр дүн шинжилгээ хийх замаар архитектуртай танилцах болно. Гэсэн хэдий ч, эхлээд энэ машин нь хэрэглэгчдэд хүртээмжтэй 32 битийн хоёр регистр, аккумлятортой байсан гэж үзье. A болон индексийн бүртгэл X, 64 байт санах ой (16 үг), бичих болон дараа нь унших боломжтой, эдгээр объектуудтай ажиллах командын жижиг системтэй. Нөхцөлт илэрхийллийг хэрэгжүүлэх үсрэх заавар нь програмуудад бас байдаг боловч хөтөлбөрийг цаг тухайд нь дуусгахын тулд үсрэлтийг зөвхөн урагшлуулж болно, тухайлбал гогцоо үүсгэхийг хориглосон.

Машиныг эхлүүлэх ерөнхий схем нь дараах байдалтай байна. Хэрэглэгч нь BPF архитектурт зориулсан програмыг бий болгож, ашиглан зарим нь цөмийн механизм (системийн дуудлага гэх мэт), програмыг ачаалж, холбодог заримд нь цөм дэх үйл явдлын генератор руу (жишээлбэл, үйл явдал нь сүлжээний карт дээрх дараагийн пакет ирэх явдал юм). Үйл явдал тохиолдоход цөм нь програмыг ажиллуулдаг (жишээлбэл, орчуулагч дээр), машины санах ой нь заримд нь цөмийн санах ойн бүс (жишээлбэл, ирж буй пакетийн өгөгдөл).

Дээрх нь жишээг үзэж эхлэхэд хангалттай байх болно: шаардлагатай бол бид систем болон тушаалын форматтай танилцах болно. Хэрэв та виртуал машины командын системийг нэн даруй судалж, түүний бүх боломжийн талаар мэдэхийг хүсч байвал анхны нийтлэлийг уншиж болно. BSD пакет шүүлтүүр ба/эсвэл файлын эхний хагас Documentation/networking/filter.txt цөмийн баримтаас. Үүнээс гадна та илтгэлийг судалж болно libpcap: Пакет барихад зориулсан архитектур ба оновчлолын арга зүй, үүнд BPF-ийн зохиогчдын нэг МакКэнн бүтээлийн түүхийн талаар ярьдаг libpcap.

Одоо бид Линукс дээр сонгодог BPF ашиглах бүх чухал жишээг авч үзэх болно. tcpdump (libpcap), seccomp, xt_bpf, cls_bpf.

tcpdump

BPF-ийг хөгжүүлэх нь багц шүүлтүүрийн урд талын программыг боловсруулахтай зэрэгцэн явагдсан - алдартай хэрэгсэл tcpdump. Энэ бол олон үйлдлийн системүүд дээр байдаг сонгодог BPF ашиглах хамгийн эртний бөгөөд хамгийн алдартай жишээ тул бид технологийн талаар судалж эхлэх болно.

(Би энэ нийтлэл дэх бүх жишээг Линукс дээр ажиллуулсан 5.6.0-rc6. Уншихад илүү хялбар болгох үүднээс зарим командын гаралтыг зассан.)

Жишээ нь: IPv6 пакетуудыг ажиглах

Бид интерфэйс дээрх бүх IPv6 пакетуудыг харахыг хүсч байна гэж төсөөлөөд үз дээ eth0. Үүнийг хийхийн тулд бид програмыг ажиллуулж болно tcpdump энгийн шүүлтүүрээр ip6:

$ sudo tcpdump -i eth0 ip6

Иймээс tcpdump шүүлтүүрийг эмхэтгэдэг ip6 BPF архитектурын байт код руу оруулаад цөм рүү илгээнэ үү (хэсэг дэх дэлгэрэнгүй мэдээллийг үзнэ үү Tcpdump: ачаалж байна). Ачаалагдсан шүүлтүүрийг интерфэйсээр дамжих пакет бүрт ажиллуулна eth0. Хэрэв шүүлтүүр нь тэгээс өөр утгыг буцаавал n, дараа нь хүртэл n пакетийн байт нь хэрэглэгчийн орон зайд хуулах бөгөөд бид үүнийг гаралт дээр харах болно tcpdump.

Бяцхан хүүхдүүдэд зориулсан BPF, тэг хэсэг: сонгодог BPF

Цөм рүү аль байт код илгээгдсэнийг бид хялбархан олж мэдэх боломжтой болж байна 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 бит) 12" хаяг дээр байрладаг бөгөөд цорын ганц асуулт бол бид ямар санах ойд хандаж байна вэ? Хариулт нь тэр цагт x эхэлдэг (x+1)Шинжилсэн сүлжээний багцын байт. Бид Ethernet интерфейсээс пакетуудыг уншдаг eth0, бөгөөд энэ нь гэсэн үгпакет нь иймэрхүү харагдаж байна (хялбар болгохын тулд бид багцад VLAN шошго байхгүй гэж үздэг):

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

Тиймээс тушаалыг гүйцэтгэсний дараа ldh [12] бүртгэлд A талбай байх болно Ether Type — энэ Ethernet фреймд дамжуулагдсан пакетийн төрөл. 1-р мөрөнд бид бүртгэлийн агуулгыг харьцуулна A (багцын төрөл) c 0x86dd, бөгөөд энэ нь бас байдаг Бидний сонирхож буй төрөл бол IPv6. 1-р мөрөнд харьцуулах командаас гадна өөр хоёр багана байна - jt 2 и jf 3 - харьцуулалт амжилттай болсон тохиолдолд очих ёстой тэмдэгтүүд (A == 0x86dd) мөн амжилтгүй болсон. Тиймээс, амжилттай тохиолдолд (IPv6) бид 2-р мөрөнд, амжилтгүй тохиолдолд 3-р мөрөнд очно. 3-р мөрөнд програм 0 кодоор дуусгавар болно (пакетийг хуулж болохгүй), 2-р мөрөнд програм кодоор дуусгавар болно. 262144 (хамгийн ихдээ 256 килобайт багцыг надад хуулна уу).

Илүү төвөгтэй жишээ: бид TCP пакетуудыг очих портоор нь хардаг

Зорилтот порт 666-тай бүх TCP пакетуудыг хуулдаг шүүлтүүр ямар байхыг харцгаая. 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 пакет мөн эсэхийг аль хэдийн шалгасан (Ether Type = 0x800) болон бүртгэлд ачаална уу A Пакетийн 24 байт. Манай багц иймэрхүү харагдаж байна

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

Энэ нь бид бүртгэлд ачаална гэсэн үг юм A IP толгой хэсгийн Protocol талбар нь логик юм, учир нь бид зөвхөн TCP пакетуудыг хуулахыг хүсч байна. Бид протоколыг харьцуулж байна 0x6 (IPPROTO_TCP) 3-р мөрөнд.

4, 5-р мөрөнд бид 20-р хаяг дээр байрлах хагас үгийг ачаалж, командыг ашиглана jset гурвын аль нэг нь тохируулагдсан эсэхийг шалгана уу тугнууд - гаргасан маск зүүсэн jset хамгийн чухал гурван бит арилсан. Гурван битийн хоёр нь пакет нь хуваагдсан IP пакетийн нэг хэсэг мөн эсэхийг, хэрэв тийм бол энэ нь сүүлчийн фрагмент мөн эсэхийг хэлж өгдөг. Гурав дахь бит нь хадгалагдсан бөгөөд тэг байх ёстой. Бид дутуу эсвэл эвдэрсэн пакетуудыг шалгахыг хүсэхгүй байгаа тул бүх гурван битийг шалгадаг.

Энэ жагсаалтын 6-р мөр нь хамгийн сонирхолтой юм. Илэрхийлэл ldxb 4*([14]&0xf) бид бүртгэлд ачаална гэсэн үг X Пакетийн арван тав дахь байтын хамгийн бага ач холбогдолтой дөрвөн битийг 4-өөр үржүүлсэн. Арван тав дахь байтын хамгийн бага ач холбогдолтой дөрвөн бит нь талбар юм. Интернет толгойн урт IPv4 толгой, толгойн уртыг үгээр хадгалдаг тул та дараа нь 4-өөр үржүүлэх хэрэгтэй. Сонирхолтой нь илэрхийлэл 4*([14]&0xf) Энэ нь зөвхөн энэ хэлбэрээр, зөвхөн бүртгэлд ашиглах боломжтой тусгай хаяглалтын схемийн тэмдэглэгээ юм X, өөрөөр хэлбэл бид бас хэлж чадахгүй ldb 4*([14]&0xf) үгүй ldxb 5*([14]&0xf) (бид зөвхөн өөр офсетийг зааж өгч болно, жишээлбэл, ldxb 4*([16]&0xf)). Энэхүү хаягжуулалтын схемийг хүлээн авахын тулд BPF-д яг нэмж оруулсан нь тодорхой байна X (индекс бүртгэл) IPv4 толгойн урт.

Тиймээс 7-р мөрөнд бид хагас үгийг ачаалахыг хичээдэг (X+16). Ethernet толгой нь 14 байт эзэлдэг гэдгийг санаарай, мөн X IPv4 толгойн уртыг агуулж байгаа тул бид үүнийг ойлгож байна A TCP очих порт ачаалагдсан:

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

Эцэст нь 8-р мөрөнд бид очих портыг хүссэн утгатай харьцуулж, 9 эсвэл 10-р мөрөнд багцыг хуулах эсэхээс үл хамааран үр дүнг буцаана.

Tcpdump: ачаалж байна

Өмнөх жишээнүүдэд бид BPF байт кодыг пакет шүүлтүүрийн цөмд яг хэрхэн ачаалах талаар дэлгэрэнгүй авч үзээгүй. Ерөнхийдөө, tcpdump олон системд шилжүүлж, шүүлтүүртэй ажиллахад зориулагдсан tcpdump номын санг ашигладаг libpcap. Товчхондоо, ашиглан интерфэйс дээр шүүлтүүр байрлуулах libpcap, та дараах зүйлийг хийх хэрэгтэй.

  • төрөл тодорхойлогч үүсгэх pcap_t интерфэйсийн нэрээс: pcap_create,
  • интерфэйсийг идэвхжүүлэх: pcap_activate,
  • эмхэтгэх шүүлтүүр: pcap_compile,
  • шүүлтүүрийг холбох: pcap_setfilter.

Функц хэрхэн ажилладагийг харахын тулд pcap_setfilter Линукс дээр хэрэгжсэн, бид ашигладаг 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 (бүх пакетуудыг хаях), залгуурыг блоклохгүй горимд шилжүүлж, өмнөх шүүлтүүрүүдээс үлдэж болох бүх пакетуудыг хасахыг оролддог.

Линукс дээрх багцуудыг сонгодог BPF ашиглан шүүхийн тулд та ийм бүтэцтэй шүүлтүүртэй байх хэрэгтэй. struct sock_fprog мөн нээлттэй залгуур, үүний дараа шүүлтүүрийг системийн дуудлага ашиглан залгуурт холбож болно setsockopt.

Сонирхолтой нь шүүлтүүрийг зөвхөн түүхий биш, ямар ч залгуурт холбож болно. Энд жишээ нь Бүх ирж буй UDP датаграмаас эхний хоёр байтаас бусад бүх зүйлийг таслах програм. (Би нийтлэлийг эмх замбараагүй болгохгүйн тулд кодын тайлбарыг нэмсэн.)

Хэрэглээний талаарх дэлгэрэнгүй мэдээлэл setsockopt шүүлтүүрийг холбохын тулд үзнэ үү залгуур (7), гэхдээ өөрийн шүүлтүүрийг бичих тухай гэх мэт struct sock_fprog тусламжгүйгээр tcpdump хэсэгт бид ярилцах болно Өөрийнхөө гараар BPF програмчлах.

Сонгодог BPF ба XNUMX-р зуун

BPF нь 1997 онд Линукс системд багтсан бөгөөд удаан хугацааны туршид ажилчин хэвээр байна libpcap ямар ч тусгай өөрчлөлтгүйгээр (Мэдээжийн хэрэг, Линуксийн онцлог өөрчлөлтүүд, Энэ нь байсан, гэхдээ тэд дэлхийн дүр зургийг өөрчилсөнгүй). BPF хөгжих анхны ноцтой шинж тэмдгүүд нь 2011 онд Эрик Думазет санал болгосноор гарч ирэв. нөхөөс, энэ нь цөмд Just In Time Compiler-ийг нэмдэг - BPF байт кодыг эх рүү хөрвүүлэх орчуулагч. x86_64 код.

JIT хөрвүүлэгч нь өөрчлөлтүүдийн гинжин хэлхээний анхных нь байсан: 2012 онд гарч ирэв шүүлтүүр бичих чадвар секкомпьютер, BPF ашиглан 2013 оны XNUMX-р сард байсан нэмсэн модуль xt_bpf, энэ нь танд дүрэм бичих боломжийг олгодог iptables BPF-ийн тусламжтайгаар 2013 оны XNUMX-р сард болсон нэмсэн мөн модуль cls_bpf, энэ нь танд BPF ашиглан хөдөлгөөний ангилагч бичих боломжийг олгодог.

Бид удахгүй эдгээр бүх жишээг илүү дэлгэрэнгүй авч үзэх болно, гэхдээ эхлээд номын сангаас өгсөн боломжууд нь BPF-д дурын програм бичиж, эмхэтгэхийг сурах нь бидэнд ашигтай байх болно. libpcap хязгаарлагдмал (энгийн жишээ: шүүлтүүр үүсгэсэн libpcap зөвхөн хоёр утгыг буцааж болно - 0 эсвэл 0x40000) эсвэл ерөнхийдөө seccomp-ийн хувьд хамаарахгүй.

Өөрийнхөө гараар BPF програмчлах

BPF зааврын хоёртын форматтай танилцацгаая, энэ нь маш энгийн:

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

Заавар бүр 64 бит эзэлдэг бөгөөд эхний 16 бит нь зааврын код, дараа нь хоёр найман битийн доголтой, jt и jf, мөн аргументийн хувьд 32 бит 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),
}

Гэсэн хэдий ч энэ сонголт нь тийм ч тохиромжтой биш юм. Линуксийн цөмийн программистууд ийм үндэслэлтэй байсан тул лавлахад ийм байна tools/bpf цөмүүдээс та сонгодог 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

Анхдагч байдлаар ассемблер нь кодыг форматаар үүсгэдэг <количество инструкций>,<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,

Си програмистуудын тав тухыг хангах үүднээс өөр гаралтын форматыг ашиглаж болно:

$ 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-ээс гадна Линукс болон 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 програмуудыг хэрхэн бичихийг аль хэдийн мэддэг болсон бөгөөд шинэ жишээнүүдийг үзэхэд бэлэн байгаа бөгөөд тэдгээрийн эхнийх нь BPF шүүлтүүрийг ашиглан системийн дуудлагын аргументуудын багц болон багцыг удирдах боломжийг олгодог seccomp технологи юм. өгөгдсөн үйл явц ба түүний үр удам.

Seccomp-ийн анхны хувилбар нь 2005 онд цөмд нэмэгдсэн бөгөөд тийм ч түгээмэл биш байсан, учир нь энэ нь зөвхөн нэг л сонголтоор хангагдсан байсан - процесст ашиглах боломжтой системийн дуудлагын багцыг дараах байдлаар хязгаарлах: read, write, exit и sigreturn, мөн дүрмийг зөрчсөн үйл явцыг ашиглан амь насаа алдсан SIGKILL. Гэсэн хэдий ч, 2012 онд seccomp нь BPF шүүлтүүрийг ашиглах боломжийг нэмсэн нь зөвшөөрөгдсөн системийн дуудлагын багцыг тодорхойлох, тэр ч байтугай тэдгээрийн аргументуудыг шалгах боломжийг олгодог. (Сонирхолтой нь, Chrome нь энэ функцийг ашигласан анхны хэрэглэгчдийн нэг байсан бөгөөд Chrome-ын хүмүүс одоогоор BPF-ийн шинэ хувилбар дээр суурилсан KRSI механизмыг хөгжүүлж, Линукс аюулгүй байдлын модулиудыг өөрчлөх боломжийг олгож байна.) Нэмэлт баримт бичгийн холбоосыг төгсгөлд нь харж болно. нийтлэлийн.

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 битийн тохируулгаар хандах боломжтой бөгөөд шүүлтүүрийг ачаалах гэж оролдох үед та хагас үг эсвэл байт ачаалах боломжгүй. ldh [0] системийн дуудлага seccomp буцаж ирнэ EINVAL. Функц нь ачаалагдсан шүүлтүүрүүдийг шалгадаг seccomp_check_filter() цөм. (Инээдтэй зүйл бол 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 Линукс дээрх процессуудын зан төлөвийг судлах зайлшгүй хэрэгсэл юм. Гэсэн хэдий ч олон хүн энэ тухай сонссон гүйцэтгэлийн асуудлууд энэ хэрэгслийг ашиглах үед. Бодит байдал ийм л байна 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 -аас олж болно сүүлийн үеийн тайлан. Бидний хувьд хамгийн сонирхолтой баримт бол seccomp-ээр илэрхийлэгддэг сонгодог BPF-ийг өнөөг хүртэл ашиглаж байгаа явдал юм.

xt_bpf

Одоо сүлжээний ертөнц рүү буцаж орцгооё.

Сэдвийн мэдээлэл: эрт дээр үед 2007 онд гол нь байсан нэмсэн модуль xt_u32 сүлжээ шүүлтүүрийн хувьд. Үүнийг илүү эртний замын хөдөлгөөний ангилагчтай зүйрлэж бичсэн cls_u32 Дараах энгийн үйлдлүүдийг ашиглан iptables-ийн дурын хоёртын дүрмийг бичих боломжийг танд олгоно: багцаас 32 бит ачаалж, тэдгээр дээр арифметик үйлдлийн багцыг гүйцэтгэх. Жишээлбэл,

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

32-р дүүргэлтээс эхлэн IP толгойн 6 битийг ачаалж, тэдгээрт маск тавина. 0xFF (бага байтыг аваарай). Энэ талбар protocol IP толгой ба бид үүнийг 1 (ICMP) -тай харьцуулдаг. Нэг дүрмээр та олон шалгалтыг нэгтгэж болно, мөн та операторыг ажиллуулж болно @ — X байтыг баруун тийш шилжүүлнэ. Жишээлбэл, дүрэм

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

TCP дарааллын дугаар тэнцүү биш эсэхийг шалгана 0x29. Ийм дүрмийг гараар бичих нь тийм ч тохиромжтой биш гэдэг нь тодорхой болсон тул би цааш дэлгэрэнгүй ярихгүй. Нийтлэлд BPF - мартагдсан байт код, ашиглах, дүрэм үүсгэх жишээ бүхий хэд хэдэн холбоосууд байдаг xt_u32. Мөн энэ нийтлэлийн төгсгөлд байгаа холбоосыг үзнэ үү.

2013 оноос хойш модулийн оронд модуль xt_u32 Та BPF дээр суурилсан модулийг ашиглаж болно xt_bpf. Үүнийг уншсан хэн бүхэн түүний үйл ажиллагааны зарчмыг аль хэдийн тодорхой мэдэж байх ёстой: BPF байт кодыг iptables дүрмээр ажиллуул. Та шинэ дүрэм үүсгэж болно, жишээ нь:

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

энд <байткод> - энэ бол ассемблерийн гаралтын формат дахь код юм 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 програмаас утгыг буцаана логикхаана false пакет таарахгүй байна гэсэн үг.

Энэ нь модуль нь тодорхой байна xt_bpf дээрх жишээнээс илүү төвөгтэй шүүлтүүрүүдийг дэмждэг. Cloudfare-ийн бодит жишээнүүдийг харцгаая. Саяхныг хүртэл тэд модулийг ашиглаж байсан xt_bpf DDoS халдлагаас хамгаалах. Нийтлэлд BPF хэрэгслүүдийг танилцуулж байна Тэд BPF шүүлтүүрийг хэрхэн (болон яагаад) үүсгэдэг талаар тайлбарлаж, ийм шүүлтүүр үүсгэх хэрэгслүүдийн холбоосыг нийтэлдэг. Жишээлбэл, хэрэгслийг ашиглах bpfgen та нэрний DNS асуулгад тохирох BPF програмыг үүсгэж болно 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 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 бууруулах.

cls_bpf

Цөмд сонгодог BPF ашиглах сүүлийн жишээ бол ангилагч юм cls_bpf Линукс дээрх замын хөдөлгөөний удирдлагын дэд системд 2013 оны сүүлээр Линукс дээр нэмж, эртний системийг орлуулсан. cls_u32.

Гэсэн хэдий ч бид одоо энэ ажлыг тайлбарлахгүй cls_bpf, сонгодог BPF-ийн талаархи мэдлэгийн үүднээс энэ нь бидэнд юу ч өгөхгүй - бид бүх функцтэй аль хэдийн танилцсан. Нэмж дурдахад, Extended BPF-ийн тухай дараагийн нийтлэлүүдэд бид энэ ангилагчтай нэгээс олон удаа уулзах болно.

Сонгодог BPF ашиглах талаар ярихгүй байх бас нэг шалтгаан c cls_bpf Асуудал нь Өргөтгөсөн BPF-тэй харьцуулахад энэ тохиолдолд хэрэглэх хүрээ эрс нарийссан: сонгодог програмууд нь багцын агуулгыг өөрчлөх боломжгүй бөгөөд дуудлага хоорондын төлөвийг хадгалах боломжгүй юм.

Тиймээс сонгодог BPF-тэй баяртай гэж хэлээд ирээдүйгээ харах цаг болжээ.

Сонгодог BPF-тэй баяртай

Бид ерээд оны эхээр бүтээгдсэн BPF технологи хэрхэн дөрөвний нэг зууны турш амжилттай амьдарч, эцсээ хүртэл шинэ хэрэглээг олж авсныг харлаа. Гэсэн хэдий ч сонгодог BPF-ийг хөгжүүлэхэд түлхэц болсон стек машинаас RISC руу шилжихтэй адил 32-аад онд 64 битийн машинаас XNUMX битийн машин руу шилжиж, сонгодог BPF хуучирч эхэлсэн. Нэмж дурдахад, сонгодог BPF-ийн боломжууд маш хязгаарлагдмал бөгөөд хуучирсан архитектураас гадна бид BPF програм руу залгасан дуудлагын хоорондох төлөвийг хадгалах чадваргүй, хэрэглэгчтэй шууд харилцах боломж байхгүй, харилцан үйлчлэх боломж байхгүй. цөөн тооны бүтцийн талбаруудыг уншихаас бусад тохиолдолд цөмтэй sk_buff болон хамгийн энгийн туслах функцуудыг ажиллуулснаар та пакетуудын агуулгыг өөрчилж, дахин чиглүүлэх боломжгүй.

Үнэн хэрэгтээ одоогийн байдлаар Линукс дээрх сонгодог BPF-ээс үлдсэн бүх зүйл нь API интерфейс бөгөөд цөм дотор залгуур шүүлтүүр эсвэл seccomp шүүлтүүр гэх мэт бүх сонгодог програмууд автоматаар Өргөтгөсөн BPF формат руу хөрвүүлэгддэг. (Энэ нь яг яаж болдог талаар бид дараагийн өгүүллээр ярих болно.)

2013 онд Алексей Старовойтов BPF-ийн шинэчлэлийн схемийг санал болгосноор шинэ архитектурт шилжих ажил эхэлсэн. 2014 онд холбогдох засварууд гарч эхэлсэн цөмд. Миний ойлгож байгаагаар анхны төлөвлөгөө нь зөвхөн 64 битийн машинууд дээр илүү үр дүнтэй ажиллахын тулд архитектур болон JIT хөрвүүлэгчийг оновчтой болгох байсан боловч оронд нь эдгээр оновчлолууд нь Линуксыг хөгжүүлэх шинэ бүлгийн эхлэлийг тавьсан юм.

Энэ цувралын цаашдын нийтлэлүүд нь эхлээд дотоод BPF, дараа нь өргөтгөсөн BPF, одоо зүгээр л BPF гэгддэг шинэ технологийн архитектур, хэрэглээг хамарна.

лавлагаа

  1. Стивен МакКэнн, Ван Жэйкобсон, "BSD пакет шүүлтүүр: Хэрэглэгчийн түвшний пакет барих шинэ архитектур", https://www.tcpdump.org/papers/bpf-usenix93.pdf
  2. Стивен МакКэнн, "libpcap: Пакет барихад зориулсан архитектур ба оновчлолын арга зүй", 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 - мартагдсан байт код: 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. Пол Чайнон, "strace --seccomp-bpf: юүдэн доорх харц", https://fosdem.org/2020/schedule/event/debugging_strace_bpf/
  13. netsniff-ng: http://netsniff-ng.org/

Эх сурвалж: www.habr.com

сэтгэгдэл нэмэх