BPF dành cho trẻ nhỏ, phần XNUMX: BPF cổ điển

Bộ lọc gói Berkeley (BPF) là một công nghệ nhân Linux đã xuất hiện trên trang nhất của các ấn phẩm công nghệ bằng tiếng Anh trong vài năm nay. Các hội nghị chứa đầy các báo cáo về việc sử dụng và phát triển BPF. David Miller, người bảo trì hệ thống con mạng Linux, gọi bài phát biểu của mình tại Linux Plumbers 2018 “Cuộc nói chuyện này không phải về XDP” (XDP là một trường hợp sử dụng cho BPF). Brendan Gregg có bài nói chuyện mang tên Siêu năng lực BPF của Linux. Toke Høiland-Jørgensen cườirằng hạt nhân bây giờ là một hạt nhân vi mô. Thomas Graf thúc đẩy ý tưởng rằng BPF là javascript cho kernel.

Vẫn chưa có mô tả có hệ thống về BPF trên Habré, và do đó trong một loạt bài viết, tôi sẽ cố gắng nói về lịch sử của công nghệ, mô tả kiến ​​trúc và các công cụ phát triển cũng như phác thảo các lĩnh vực ứng dụng và thực tiễn sử dụng BPF. Bài viết này, số XNUMX, trong loạt bài này, kể về lịch sử và kiến ​​trúc của BPF cổ điển, đồng thời tiết lộ những bí mật về nguyên tắc hoạt động của nó. tcpdump, seccomp, strace, và nhiều hơn nữa.

Sự phát triển của BPF được kiểm soát bởi cộng đồng mạng Linux, các ứng dụng chính hiện có của BPF đều liên quan đến mạng và do đó, phải có sự cho phép @eucariot, Tôi gọi bộ truyện này là “BPF dành cho các bạn nhỏ”, để vinh danh bộ truyện tuyệt vời "Mạng dành cho trẻ nhỏ".

Một khóa học ngắn hạn về lịch sử của BPF(c)

Công nghệ BPF hiện đại là phiên bản cải tiến và mở rộng của công nghệ cũ cùng tên, hiện nay được gọi là BPF cổ điển để tránh nhầm lẫn. Một tiện ích nổi tiếng đã được tạo ra dựa trên BPF cổ điển tcpdump, cơ chế seccomp, cũng như các mô-đun ít được biết đến hơn xt_bpf cho iptables và phân loại cls_bpf. Trong Linux hiện đại, các chương trình BPF cổ điển được tự động dịch sang dạng mới, tuy nhiên, từ quan điểm của người dùng, API vẫn được giữ nguyên và các ứng dụng mới cho BPF cổ điển, như chúng ta sẽ thấy trong bài viết này, vẫn đang được tìm thấy. Vì lý do này, và cũng vì theo dõi lịch sử phát triển của BPF cổ điển trong Linux, sẽ trở nên rõ ràng hơn về cách thức và lý do tại sao nó phát triển thành dạng hiện đại, tôi quyết định bắt đầu bằng một bài viết về BPF cổ điển.

Vào cuối những năm tám mươi của thế kỷ trước, các kỹ sư từ Phòng thí nghiệm Lawrence Berkeley nổi tiếng bắt đầu quan tâm đến câu hỏi làm thế nào để lọc các gói mạng một cách chính xác trên phần cứng hiện đại vào cuối những năm tám mươi của thế kỷ trước. Ý tưởng cơ bản của việc lọc, ban đầu được triển khai trong công nghệ CSPF (CMU/Stanford Packet Filter), là lọc các gói không cần thiết càng sớm càng tốt, tức là. trong không gian kernel, vì điều này tránh sao chép dữ liệu không cần thiết vào không gian người dùng. Để cung cấp bảo mật thời gian chạy cho việc chạy mã người dùng trong không gian kernel, một máy ảo có hộp cát đã được sử dụng.

Tuy nhiên, các máy ảo dành cho các bộ lọc hiện có được thiết kế để chạy trên các máy dựa trên ngăn xếp và không chạy hiệu quả trên các máy RISC mới hơn. Kết quả là, nhờ nỗ lực của các kỹ sư từ Berkeley Labs, một công nghệ BPF (Berkeley Packet Filters) mới đã được phát triển, kiến ​​trúc máy ảo được thiết kế dựa trên bộ xử lý Motorola 6502 - đặc trưng của các sản phẩm nổi tiếng như Táo II hoặc NES. Máy ảo mới tăng hiệu suất lọc lên hàng chục lần so với giải pháp hiện có.

Kiến trúc máy BPF

Chúng ta sẽ làm quen với kiến ​​trúc theo cách làm việc, phân tích các ví dụ. Tuy nhiên, để bắt đầu, giả sử rằng máy có hai thanh ghi 32 bit mà người dùng có thể truy cập được, một bộ tích lũy A và đăng ký chỉ số X, 64 byte bộ nhớ (16 từ), có sẵn để ghi và đọc tiếp theo, cùng một hệ thống lệnh nhỏ để làm việc với các đối tượng này. Hướng dẫn nhảy để thực hiện các biểu thức điều kiện cũng có sẵn trong các chương trình, nhưng để đảm bảo chương trình hoàn thành kịp thời, các bước nhảy chỉ có thể được thực hiện về phía trước, tức là đặc biệt, không được phép tạo vòng lặp.

Sơ đồ chung để khởi động máy như sau. Người dùng tạo một chương trình cho kiến ​​trúc BPF và sử dụng một số cơ chế hạt nhân (chẳng hạn như lệnh gọi hệ thống), tải và kết nối chương trình với đến một số tới trình tạo sự kiện trong kernel (ví dụ: một sự kiện là sự xuất hiện của gói tiếp theo trên card mạng). Khi một sự kiện xảy ra, kernel sẽ chạy chương trình (ví dụ: trong trình thông dịch) và bộ nhớ máy sẽ tương ứng với đến một số vùng bộ nhớ kernel (ví dụ: dữ liệu của gói đến).

Những điều trên sẽ đủ để chúng ta bắt đầu xem xét các ví dụ: chúng ta sẽ làm quen với hệ thống và định dạng lệnh nếu cần thiết. Nếu bạn muốn nghiên cứu ngay hệ thống lệnh của máy ảo và tìm hiểu về tất cả các khả năng của nó, thì bạn có thể đọc bài viết gốc Bộ lọc gói BSD và/hoặc nửa đầu của tập tin Tài liệu/mạng/filter.txt từ tài liệu hạt nhân. Ngoài ra, bạn có thể nghiên cứu cách trình bày libpcap: Phương pháp kiến ​​trúc và tối ưu hóa để chụp gói, trong đó McCanne, một trong những tác giả của BPF, nói về lịch sử sáng tạo libpcap.

Chúng ta chuyển sang xem xét tất cả các ví dụ quan trọng về việc sử dụng BPF cổ điển trên Linux: tcpdump (libpcap), seccomp, xt_bpf, cls_bpf.

tcpdump

Sự phát triển của BPF được thực hiện song song với sự phát triển của frontend để lọc gói - một tiện ích nổi tiếng tcpdump. Và vì đây là ví dụ lâu đời nhất và nổi tiếng nhất về việc sử dụng BPF cổ điển, có sẵn trên nhiều hệ điều hành nên chúng ta sẽ bắt đầu nghiên cứu về công nghệ với nó.

(Tôi đã chạy tất cả các ví dụ trong bài viết này trên Linux 5.6.0-rc6. Đầu ra của một số lệnh đã được chỉnh sửa để dễ đọc hơn.)

Ví dụ: quan sát các gói IPv6

Hãy tưởng tượng rằng chúng ta muốn xem xét tất cả các gói IPv6 trên một giao diện eth0. Để làm điều này chúng ta có thể chạy chương trình tcpdump với bộ lọc đơn giản ip6:

$ sudo tcpdump -i eth0 ip6

Trong trường hợp này, tcpdump biên dịch bộ lọc ip6 vào bytecode kiến ​​trúc BPF và gửi nó đến kernel (xem chi tiết trong phần Tcpdump: đang tải). Bộ lọc đã tải sẽ được chạy cho mọi gói đi qua giao diện eth0. Nếu bộ lọc trả về giá trị khác XNUMX n, sau đó lên đến n byte của gói sẽ được sao chép vào không gian người dùng và chúng ta sẽ thấy nó ở đầu ra tcpdump.

BPF dành cho trẻ nhỏ, phần XNUMX: BPF cổ điển

Hóa ra chúng ta có thể dễ dàng tìm ra mã byte nào đã được gửi tới kernel tcpdump với sự giúp đỡ của tcpdump, nếu chúng ta chạy nó với tùy chọn -d:

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

Trên dòng XNUMX, chúng tôi chạy lệnh ldh [12], viết tắt của “nạp vào sổ đăng ký A nửa từ (16 bit) nằm ở địa chỉ 12” và câu hỏi duy nhất là chúng ta đang xử lý loại bộ nhớ nào? Câu trả lời là tại x bắt đầu (x+1)byte thứ của gói mạng được phân tích. Chúng tôi đọc các gói từ giao diện Ethernet eth0và điều này có nghĩa làgói trông như thế này (để đơn giản, chúng tôi giả sử rằng không có thẻ Vlan trong gói):

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

Vì vậy sau khi thực hiện lệnh ldh [12] trong sổ đăng ký A sẽ có một cánh đồng Ether Type - loại gói được truyền trong khung Ethernet này. Trên dòng 1, chúng tôi so sánh nội dung của thanh ghi A (loại gói) c 0x86ddvà điều này và có Loại chúng tôi quan tâm là IPv6. Ở dòng 1, ngoài lệnh so sánh, còn có hai cột nữa - jt 2 и jf 3 — điểm mà bạn cần hướng tới nếu so sánh thành công (A == 0x86dd) và không thành công. Vì vậy, trong trường hợp thành công (IPv6), chúng ta chuyển sang dòng 2 và trong trường hợp không thành công - đến dòng 3. Trên dòng 3, chương trình kết thúc với mã 0 (không sao chép gói), ở dòng 2, chương trình kết thúc bằng mã 262144 (sao chép cho tôi gói tối đa 256 kilobyte).

Một ví dụ phức tạp hơn: chúng tôi xem xét các gói TCP theo cổng đích

Hãy xem bộ lọc sao chép tất cả các gói TCP có cổng đích 666 trông như thế nào. Chúng ta sẽ xem xét trường hợp IPv4, vì trường hợp IPv6 đơn giản hơn. Sau khi nghiên cứu ví dụ này, bạn có thể tự mình khám phá bộ lọc IPv6 như một bài tập (ip6 and tcp dst port 666) và bộ lọc cho trường hợp chung (tcp dst port 666). Vì vậy, bộ lọc mà chúng tôi quan tâm trông như thế này:

$ 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

Chúng ta đã biết dòng 0 và 1 làm gì. Ở dòng 2, chúng tôi đã kiểm tra rằng đây là gói IPv4 (Loại Ether = 0x800) và nạp nó vào thanh ghi A Byte thứ 24 của gói. Gói của chúng tôi trông giống như

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

có nghĩa là chúng tôi tải vào sổ đăng ký A trường Giao thức của tiêu đề IP, điều này hợp lý vì chúng tôi chỉ muốn sao chép các gói TCP. Chúng tôi so sánh Giao thức với 0x6 (IPPROTO_TCP) trên dòng 3.

Ở dòng 4 và 5, chúng ta tải các nửa từ nằm ở địa chỉ 20 và sử dụng lệnh jset kiểm tra xem một trong ba đã được đặt chưa cờ - đeo khẩu trang được cấp jset ba bit quan trọng nhất sẽ bị xóa. Hai trong số ba bit cho chúng ta biết liệu gói có phải là một phần của gói IP bị phân mảnh hay không và nếu có thì đó có phải là phân đoạn cuối cùng hay không. Bit thứ ba được dành riêng và phải bằng XNUMX. Chúng tôi không muốn kiểm tra các gói không đầy đủ hoặc bị hỏng, vì vậy chúng tôi kiểm tra cả ba bit.

Dòng 6 là thú vị nhất trong danh sách này. Sự biểu lộ ldxb 4*([14]&0xf) có nghĩa là chúng tôi tải vào sổ đăng ký X bốn bit có trọng số nhỏ nhất của byte thứ mười lăm của gói nhân với 4. Bốn bit có trọng số nhỏ nhất của byte thứ mười lăm là trường Độ dài tiêu đề Internet Tiêu đề IPv4, lưu trữ độ dài của tiêu đề bằng từ, do đó bạn cần nhân với 4. Điều thú vị là biểu thức 4*([14]&0xf) là ký hiệu cho một sơ đồ địa chỉ đặc biệt chỉ có thể được sử dụng ở dạng này và chỉ dành cho một thanh ghi X, I E. chúng ta cũng không thể nói ldb 4*([14]&0xf) cũng không ldxb 5*([14]&0xf) (ví dụ: chúng tôi chỉ có thể chỉ định một mức chênh lệch khác ldxb 4*([16]&0xf)). Rõ ràng là sơ đồ địa chỉ này đã được thêm vào BPF một cách chính xác để nhận X (thanh ghi chỉ mục) Độ dài tiêu đề IPv4.

Vì vậy, trên dòng 7, chúng tôi cố gắng tải nửa từ tại (X+16). Hãy nhớ rằng 14 byte được chiếm bởi tiêu đề Ethernet và X chứa độ dài của tiêu đề IPv4, chúng tôi hiểu rằng trong A Cổng đích TCP được tải:

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

Cuối cùng, trên dòng 8, chúng tôi so sánh cổng đích với giá trị mong muốn và trên dòng 9 hoặc 10, chúng tôi trả về kết quả - có sao chép gói hay không.

Tcpdump: đang tải

Trong các ví dụ trước, chúng tôi đặc biệt không đi sâu vào chi tiết về cách chúng tôi tải mã byte BPF vào kernel để lọc gói một cách chính xác như thế nào. Nói chung, tcpdump được chuyển tới nhiều hệ thống và để làm việc với các bộ lọc tcpdump sử dụng thư viện libpcap. Tóm lại, để đặt bộ lọc trên giao diện bằng cách sử dụng libpcap, bạn cần phải làm như sau:

Để xem chức năng thế nào pcap_setfilter được triển khai trong Linux, chúng tôi sử dụng strace (một số dòng đã bị xóa):

$ 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
...

Trên hai dòng đầu ra đầu tiên, chúng tôi tạo ổ cắm thô để đọc tất cả các khung Ethernet và liên kết nó với giao diện eth0. Của ví dụ đầu tiên của chúng tôi chúng tôi biết rằng bộ lọc ip sẽ bao gồm bốn hướng dẫn BPF và ở dòng thứ ba, chúng ta thấy cách sử dụng tùy chọn SO_ATTACH_FILTER cuộc gọi hệ thống setsockopt chúng tôi tải và kết nối bộ lọc có độ dài 4. Đây là bộ lọc của chúng tôi.

Điều đáng lưu ý là trong BPF cổ điển, việc tải và kết nối bộ lọc luôn diễn ra dưới dạng hoạt động nguyên tử và trong phiên bản mới của BPF, việc tải chương trình và liên kết nó với trình tạo sự kiện được phân tách theo thời gian.

Sự thật ẩn giấu

Một phiên bản đầu ra hoàn chỉnh hơn một chút trông như thế này:

$ 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
...

Như đã đề cập ở trên, chúng tôi tải và kết nối bộ lọc của mình với ổ cắm trên dòng 5, nhưng điều gì xảy ra trên dòng 3 và 4? Hoá ra là cái này libpcap chăm sóc chúng tôi - để đầu ra của bộ lọc của chúng tôi không bao gồm các gói không thỏa mãn nó, thư viện kết nối bộ lọc giả ret #0 (bỏ tất cả các gói), chuyển ổ cắm sang chế độ không chặn và cố gắng loại bỏ tất cả các gói có thể còn lại khỏi các bộ lọc trước đó.

Tổng cộng, để lọc các gói trên Linux bằng BPF cổ điển, bạn cần có một bộ lọc có dạng cấu trúc như struct sock_fprog và một ổ cắm mở, sau đó bộ lọc có thể được gắn vào ổ cắm bằng lệnh gọi hệ thống setsockopt.

Điều thú vị là bộ lọc có thể được gắn vào bất kỳ ổ cắm nào, không chỉ ổ cắm thô. Đây Ví dụ một chương trình cắt bỏ tất cả trừ hai byte đầu tiên khỏi tất cả các gói dữ liệu UDP đến. (Tôi đã thêm nhận xét vào mã để không làm lộn xộn bài viết.)

Thêm chi tiết về việc sử dụng setsockopt để kết nối các bộ lọc, xem ổ cắm (7), nhưng về việc viết các bộ lọc của riêng bạn như struct sock_fprog không có sự trợ giúp tcpdump chúng ta sẽ nói chuyện trong phần Lập trình BPF bằng chính đôi tay của chúng ta.

BPF cổ điển và thế kỷ XNUMX

BPF được đưa vào Linux vào năm 1997 và vẫn là công cụ hỗ trợ trong một thời gian dài libpcap không có bất kỳ thay đổi đặc biệt nào (tất nhiên là những thay đổi dành riêng cho Linux, , nhưng chúng không làm thay đổi bức tranh toàn cầu). Dấu hiệu nghiêm trọng đầu tiên cho thấy BPF sẽ phát triển xuất hiện vào năm 2011, khi Eric Dumazet đề xuất , bổ sung Just In Time Compiler vào kernel - một trình dịch để chuyển đổi mã byte BPF sang mã gốc x86_64 mã.

Trình biên dịch JIT là trình biên dịch đầu tiên trong chuỗi thay đổi: năm 2012 xuất hiện khả năng viết bộ lọc cho bí mật, sử dụng BPF, vào tháng 2013 năm XNUMX đã có thêm mô-đun xt_bpf, cho phép bạn viết các quy tắc cho iptables với sự giúp đỡ của BPF, và vào tháng 2013 năm XNUMX thêm cũng là một mô-đun cls_bpf, cho phép bạn viết các bộ phân loại lưu lượng truy cập bằng BPF.

Chúng ta sẽ sớm xem xét tất cả các ví dụ này một cách chi tiết hơn, nhưng trước tiên, sẽ rất hữu ích nếu chúng ta tìm hiểu cách viết và biên dịch các chương trình tùy ý cho BPF, vì các khả năng được thư viện cung cấp libpcap hạn chế (ví dụ đơn giản: bộ lọc được tạo libpcap chỉ có thể trả về hai giá trị - 0 hoặc 0x40000) hoặc nói chung, như trong trường hợp của seccomp, không được áp dụng.

Lập trình BPF bằng chính đôi tay của chúng ta

Chúng ta hãy làm quen với định dạng nhị phân của hướng dẫn BPF, nó rất đơn giản:

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

Mỗi lệnh chiếm 64 bit, trong đó 16 bit đầu tiên là mã lệnh, sau đó có hai thụt lề XNUMX bit, jt и jfvà 32 bit cho đối số K, mục đích của nó thay đổi tùy theo lệnh. Ví dụ, lệnh ret, kết thúc chương trình có mã 6và giá trị trả về được lấy từ hằng số K. Trong C, một lệnh BPF duy nhất được biểu diễn dưới dạng cấu trúc

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

và toàn bộ chương trình ở dạng cấu trúc

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

Vì vậy, chúng ta đã có thể viết chương trình (ví dụ: chúng ta biết mã lệnh từ [1]). Bộ lọc sẽ trông như thế này ip6 của ví dụ đầu tiên của chúng tôi:

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,
};

chương trình prog chúng ta có thể sử dụng hợp pháp trong cuộc gọi

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

Viết chương trình dưới dạng mã máy không thuận tiện lắm nhưng đôi khi nó cần thiết (ví dụ để gỡ lỗi, tạo bài kiểm tra đơn vị, viết bài trên Habré, v.v.). Để thuận tiện, trong tập tin <linux/filter.h> macro trợ giúp được xác định - ví dụ tương tự như trên có thể được viết lại thành

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),
}

Tuy nhiên, tùy chọn này không thuận tiện lắm. Đây là điều mà các lập trình viên nhân Linux đã lý giải, và do đó trong thư mục tools/bpf hạt nhân, bạn có thể tìm thấy trình biên dịch mã và trình gỡ lỗi để làm việc với BPF cổ điển.

Ngôn ngữ hội rất giống với đầu ra gỡ lỗi tcpdump, nhưng ngoài ra chúng ta có thể chỉ định các nhãn tượng trưng. Ví dụ: đây là chương trình loại bỏ tất cả các gói ngoại trừ TCP/IPv4:

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

Theo mặc định, trình biên dịch mã tạo mã theo định dạng <количество инструкций>,<code1> <jt1> <jf1> <k1>,..., ví dụ của chúng tôi với TCP nó sẽ là

$ 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,

Để thuận tiện cho người lập trình C, có thể sử dụng định dạng đầu ra khá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 },

Văn bản này có thể được sao chép vào định nghĩa cấu trúc kiểu struct sock_filter, như chúng tôi đã làm ở phần đầu của phần này.

Phần mở rộng Linux và netsniff-ng

Ngoài BPF tiêu chuẩn, Linux và tools/bpf/bpf_asm hô trợ và bộ không chuẩn. Về cơ bản, các lệnh được sử dụng để truy cập vào các trường của cấu trúc struct sk_buff, mô tả gói mạng trong kernel. Tuy nhiên, cũng có những loại hướng dẫn trợ giúp khác, ví dụ ldw cpu sẽ tải vào sổ đăng ký A kết quả của việc chạy một hàm kernel raw_smp_processor_id(). (Trong phiên bản mới của BPF, các tiện ích mở rộng không chuẩn này đã được mở rộng để cung cấp cho các chương trình một bộ trợ giúp kernel để truy cập bộ nhớ, cấu trúc và tạo sự kiện.) Đây là một ví dụ thú vị về bộ lọc trong đó chúng tôi chỉ sao chép tiêu đề gói vào không gian người dùng bằng cách sử dụng tiện ích mở rộng poff, bù tải trọng:

ld poff
ret a

Không thể sử dụng phần mở rộng BPF trong tcpdump, nhưng đây là lý do chính đáng để làm quen với gói tiện ích netsniff-ng, trong số những thứ khác, có chứa một chương trình nâng cao netsniff-ng, ngoài việc lọc bằng BPF, còn chứa một trình tạo lưu lượng truy cập hiệu quả và cao cấp hơn tools/bpf/bpf_asm, một trình biên dịch BPF có tên là bpfc. Gói chứa tài liệu khá chi tiết, xem thêm các liên kết ở cuối bài viết.

bí mật

Vì vậy, chúng ta đã biết cách viết các chương trình BPF với độ phức tạp tùy ý và sẵn sàng xem xét các ví dụ mới, ví dụ đầu tiên là công nghệ seccomp, cho phép sử dụng bộ lọc BPF để quản lý tập hợp và tập hợp các đối số cuộc gọi hệ thống có sẵn cho một tiến trình nhất định và các tiến trình con cháu của nó.

Phiên bản đầu tiên của seccomp đã được thêm vào kernel vào năm 2005 và không phổ biến lắm vì nó chỉ cung cấp một tùy chọn duy nhất - để giới hạn tập hợp các lệnh gọi hệ thống có sẵn cho một quy trình như sau: read, write, exit и sigreturnvà quá trình vi phạm quy tắc đã bị hủy bằng cách sử dụng SIGKILL. Tuy nhiên, vào năm 2012, seccomp đã thêm khả năng sử dụng bộ lọc BPF, cho phép bạn xác định một tập hợp các lệnh gọi hệ thống được phép và thậm chí thực hiện kiểm tra các đối số của chúng. (Thật thú vị, Chrome là một trong những người dùng đầu tiên sử dụng chức năng này và người dùng Chrome hiện đang phát triển cơ chế KRSI dựa trên phiên bản mới của BPF và cho phép tùy chỉnh các Mô-đun bảo mật Linux.) Bạn có thể tìm thấy các liên kết tới tài liệu bổ sung ở cuối của bài viết.

Lưu ý rằng đã có các bài viết trên hub về cách sử dụng seccomp, có thể ai đó sẽ muốn đọc chúng trước (hoặc thay vì) đọc các phần phụ sau. Trong bài viết Container và bảo mật: seccomp cung cấp các ví dụ về cách sử dụng seccomp, cả phiên bản 2007 và phiên bản sử dụng BPF (các bộ lọc được tạo bằng libseccomp), nói về kết nối của seccomp với Docker và cũng cung cấp nhiều liên kết hữu ích. Trong bài viết Cô lập daemon bằng systemd hoặc "bạn không cần Docker cho việc này!" Đặc biệt, nó đề cập đến cách thêm danh sách đen và danh sách trắng cuộc gọi hệ thống cho các trình nền chạy systemd.

Tiếp theo chúng ta sẽ xem cách viết và tải các bộ lọc cho seccomp trong C trần và sử dụng thư viện libseccomp và ưu nhược điểm của từng tùy chọn là gì, cuối cùng hãy xem chương trình sử dụng seccomp như thế nào strace.

Viết và tải bộ lọc cho seccomp

Chúng ta đã biết cách viết chương trình BPF, vì vậy trước tiên chúng ta hãy xem giao diện lập trình seccomp. Bạn có thể đặt bộ lọc ở cấp quy trình và tất cả các quy trình con sẽ kế thừa các hạn chế. Việc này được thực hiện bằng cách sử dụng lệnh gọi hệ thống seccomp(2):

seccomp(SECCOMP_SET_MODE_FILTER, flags, &filter)

đâu &filter - đây là một con trỏ tới một cấu trúc đã quen thuộc với chúng ta struct sock_fprog, I E. chương trình BPF.

Các chương trình dành cho seccomp khác với các chương trình dành cho socket như thế nào? Bối cảnh được truyền đi. Trong trường hợp ổ cắm, chúng tôi được cung cấp một vùng bộ nhớ chứa gói và trong trường hợp seccomp, chúng tôi được cấp một cấu trúc như

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

Здесь nr là số lượng cuộc gọi hệ thống sẽ được khởi chạy, arch - kiến ​​trúc hiện tại (xem thêm về điều này bên dưới), args - tối đa sáu đối số cuộc gọi hệ thống và instruction_pointer là một con trỏ tới hướng dẫn không gian người dùng đã thực hiện cuộc gọi hệ thống. Vì vậy, ví dụ, để tải số cuộc gọi hệ thống vào sổ đăng ký A chúng ta phải nói

ldw [0]

Có các tính năng khác dành cho chương trình seccomp, ví dụ: ngữ cảnh chỉ có thể được truy cập bằng cách căn chỉnh 32 bit và bạn không thể tải nửa từ hoặc một byte - khi cố tải bộ lọc ldh [0] cuộc gọi hệ thống seccomp sẽ trở lại EINVAL. Chức năng kiểm tra các bộ lọc đã tải seccomp_check_filter() hạt nhân. (Điều buồn cười là trong cam kết ban đầu có thêm chức năng seccomp, họ lại quên thêm quyền sử dụng hướng dẫn cho chức năng này mod (phần dư của phép chia) và hiện không có sẵn cho các chương trình BPF seccomp, kể từ khi nó được bổ sung sẽ phá vỡ ABI.)

Về cơ bản, chúng ta đã biết mọi thứ để viết và đọc các chương trình seccomp. Thông thường logic chương trình được sắp xếp dưới dạng danh sách trắng hoặc đen các lệnh gọi hệ thống, ví dụ chương trình

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

kiểm tra danh sách đen gồm bốn cuộc gọi hệ thống được đánh số 304, 176, 239, 279. Những cuộc gọi hệ thống này là gì? Chúng tôi không thể nói chắc chắn vì chúng tôi không biết chương trình được viết cho kiến ​​trúc nào. Vì vậy, các tác giả của seccomp phục vụ bắt đầu tất cả các chương trình bằng kiểm tra kiến ​​trúc (kiến trúc hiện tại được biểu thị trong ngữ cảnh dưới dạng một trường arch cấu trúc struct seccomp_data). Với kiến ​​trúc đã được kiểm tra, phần đầu của ví dụ sẽ như sau:

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

và sau đó số cuộc gọi hệ thống của chúng tôi sẽ nhận được các giá trị nhất định.

Chúng tôi viết và tải các bộ lọc cho seccomp bằng cách sử dụng libseccomp

Việc viết các bộ lọc bằng mã gốc hoặc trong tập hợp BPF cho phép bạn có toàn quyền kiểm soát kết quả, nhưng đồng thời, đôi khi nên có mã di động và/hoặc mã có thể đọc được. Thư viện sẽ giúp chúng ta việc này libseccomp, cung cấp giao diện chuẩn để viết các bộ lọc đen hoặc trắng.

Ví dụ: hãy viết chương trình chạy tệp nhị phân do người dùng chọn, trước đó đã cài đặt danh sách đen các cuộc gọi hệ thống từ bài viết trên (chương trình đã được đơn giản hóa để dễ đọc hơn, bạn có thể tìm thấy phiên bản đầy đủ đây):

#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]);
}

Đầu tiên chúng ta định nghĩa một mảng sys_numbers trong số hơn 40 số cuộc gọi hệ thống để chặn. Sau đó, khởi tạo bối cảnh ctx và nói với thư viện những gì chúng tôi muốn cho phép (SCMP_ACT_ALLOW) tất cả các lệnh gọi hệ thống theo mặc định (việc xây dựng danh sách đen sẽ dễ dàng hơn). Sau đó, lần lượt, chúng tôi thêm tất cả các lệnh gọi hệ thống từ danh sách đen. Để đáp lại cuộc gọi hệ thống từ danh sách, chúng tôi yêu cầu SCMP_ACT_TRAP, trong trường hợp này seccomp sẽ gửi tín hiệu đến tiến trình SIGSYS kèm theo mô tả về cuộc gọi hệ thống nào đã vi phạm quy tắc. Cuối cùng, chúng ta tải chương trình vào kernel bằng cách sử dụng seccomp_load, nó sẽ biên dịch chương trình và đính kèm nó vào tiến trình bằng lệnh gọi hệ thống seccomp(2).

Để biên dịch thành công, chương trình phải được liên kết với thư viện libseccomp, ví dụ:

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

Ví dụ về khởi chạy thành công:

$ ./seccomp_lib echo ok
ok

Ví dụ về cuộc gọi hệ thống bị chặn:

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

Chúng tôi sử dụng straceđể biết chi tiết:

$ 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

làm thế nào chúng tôi có thể biết rằng chương trình đã bị chấm dứt do sử dụng lệnh gọi hệ thống bất hợp pháp mount(2).

Vì vậy, chúng tôi đã viết một bộ lọc bằng thư viện libseccomp, ghép mã không tầm thường vào bốn dòng. Trong ví dụ trên, nếu có nhiều lệnh gọi hệ thống, thời gian thực hiện có thể giảm đáng kể vì việc kiểm tra chỉ là một danh sách so sánh. Để tối ưu hóa, libseccomp gần đây đã có bao gồm bản vá, bổ sung hỗ trợ cho thuộc tính bộ lọc SCMP_FLTATR_CTL_OPTIMIZE. Đặt thuộc tính này thành 2 sẽ chuyển bộ lọc thành chương trình tìm kiếm nhị phân.

Nếu bạn muốn xem các bộ lọc tìm kiếm nhị phân hoạt động như thế nào, hãy xem kịch bản đơn giản, tạo ra các chương trình như vậy trong trình biên dịch BPF bằng cách quay số cuộc gọi hệ thống, ví dụ:

$ 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

Không thể viết bất cứ điều gì nhanh hơn đáng kể, vì các chương trình BPF không thể thực hiện bước nhảy thụt lề (ví dụ: chúng tôi không thể thực hiện jmp A hoặc jmp [label+X]) và do đó tất cả các chuyển đổi đều là tĩnh.

seccomp và strace

Công dụng ai cũng biết strace là một công cụ không thể thiếu để nghiên cứu hành vi của các tiến trình trên Linux. Tuy nhiên, nhiều người cũng đã nghe nói về vấn đề hiệu năng khi sử dụng tiện ích này. Sự thật là strace thực hiện bằng cách sử dụng ptrace(2)và trong cơ chế này, chúng tôi không thể chỉ định tập hợp lệnh gọi hệ thống nào chúng tôi cần để dừng quá trình, ví dụ: các lệnh

$ 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

được xử lý trong khoảng thời gian gần như nhau, mặc dù trong trường hợp thứ hai, chúng tôi chỉ muốn theo dõi một cuộc gọi hệ thống.

Tùy chọn mới --seccomp-bpfthêm vào strace phiên bản 5.3, cho phép bạn tăng tốc quá trình lên nhiều lần và thời gian khởi động theo dấu vết của một cuộc gọi hệ thống đã tương đương với thời gian khởi động thông thường:

$ 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

(Tất nhiên, ở đây có một sự lừa dối nhỏ ở chỗ chúng ta không theo dõi lệnh gọi hệ thống chính của lệnh này. Ví dụ: nếu chúng ta đang theo dõi, newfsstatsau đó strace sẽ phanh mạnh như không có --seccomp-bpf.)

Tùy chọn này hoạt động như thế nào? Không có cô ấy strace kết nối với quá trình và bắt đầu nó bằng cách sử dụng PTRACE_SYSCALL. Khi một quy trình được quản lý phát ra (bất kỳ) lệnh gọi hệ thống nào, quyền kiểm soát sẽ được chuyển sang strace, xem xét các đối số của lệnh gọi hệ thống và chạy nó bằng cách sử dụng PTRACE_SYSCALL. Sau một thời gian, quá trình hoàn thành cuộc gọi hệ thống và khi thoát khỏi nó, quyền điều khiển sẽ được chuyển lại strace, xem xét các giá trị trả về và bắt đầu quá trình bằng cách sử dụng PTRACE_SYSCALL, và như thế.

BPF dành cho trẻ nhỏ, phần XNUMX: BPF cổ điển

Tuy nhiên, với seccomp, quá trình này có thể được tối ưu hóa chính xác như chúng ta mong muốn. Cụ thể, nếu chúng ta chỉ muốn nhìn vào cuộc gọi hệ thống X, thì chúng ta có thể viết một bộ lọc BPF cho X trả về một giá trị SECCOMP_RET_TRACEvà đối với các cuộc gọi mà chúng tôi không quan tâm - SECCOMP_RET_ALLOW:

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

Trong trường hợp này, strace ban đầu bắt đầu quá trình như PTRACE_CONT, bộ lọc của chúng tôi được xử lý cho mỗi cuộc gọi hệ thống, nếu cuộc gọi hệ thống không được thực hiện X, thì tiến trình vẫn tiếp tục chạy, nhưng nếu điều này X, khi đó seccomp sẽ chuyển quyền điều khiển stracesẽ xem xét các đối số và bắt đầu quá trình như PTRACE_SYSCALL (vì seccomp không có khả năng chạy chương trình khi thoát khỏi cuộc gọi hệ thống). Khi cuộc gọi hệ thống trở lại, strace sẽ khởi động lại quá trình bằng cách sử dụng PTRACE_CONT và sẽ đợi tin nhắn mới từ seccomp.

BPF dành cho trẻ nhỏ, phần XNUMX: BPF cổ điển

Khi sử dụng tùy chọn --seccomp-bpf có hai hạn chế. Thứ nhất, sẽ không thể tham gia vào một quy trình đã tồn tại (tùy chọn -p chương trình strace), vì điều này không được seccomp hỗ trợ. Thứ hai, không có khả năng không hãy xem xét các tiến trình con, vì các bộ lọc seccomp được kế thừa bởi tất cả các tiến trình con mà không có khả năng vô hiệu hóa điều này.

Chi tiết hơn một chút về cách chính xác strace làm việc với seccomp có thể được tìm thấy từ báo cáo gần đây. Đối với chúng tôi, điều thú vị nhất là BPF cổ điển được đại diện bởi seccomp vẫn được sử dụng cho đến ngày nay.

xt_bpf

Bây giờ chúng ta hãy quay trở lại thế giới của mạng.

Bối cảnh: cách đây rất lâu, vào năm 2007, cốt lõi là thêm mô-đun xt_u32 cho bộ lọc mạng. Nó được viết bằng cách tương tự với một bộ phân loại lưu lượng truy cập thậm chí còn cổ xưa hơn cls_u32 và cho phép bạn viết các quy tắc nhị phân tùy ý cho iptables bằng các thao tác đơn giản sau: tải 32 bit từ một gói và thực hiện một tập hợp các phép toán số học trên chúng. Ví dụ,

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

Tải 32 bit của tiêu đề IP, bắt đầu từ phần đệm 6 và áp dụng mặt nạ cho chúng 0xFF (lấy byte thấp). Lĩnh vực này protocol Tiêu đề IP và chúng tôi so sánh nó với 1 (ICMP). Bạn có thể kết hợp nhiều kiểm tra trong một quy tắc và bạn cũng có thể thực thi toán tử @ - di chuyển X byte sang phải. Ví dụ, quy tắc

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

kiểm tra xem Số thứ tự TCP có bằng nhau không 0x29. Tôi sẽ không đi sâu vào chi tiết vì rõ ràng là việc viết những quy tắc như vậy bằng tay không thuận tiện lắm. Trong bài viết BPF - mã byte bị lãng quên, có một số liên kết với các ví dụ về cách sử dụng và tạo quy tắc cho xt_u32. Xem thêm các liên kết ở cuối bài viết này.

Kể từ mô-đun năm 2013 thay vì mô-đun xt_u32 bạn có thể sử dụng mô-đun dựa trên BPF xt_bpf. Bất kỳ ai đã đọc đến đây đều đã hiểu rõ về nguyên tắc hoạt động của nó: chạy mã byte BPF dưới dạng quy tắc iptables. Bạn có thể tạo quy tắc mới, ví dụ như thế này:

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

đây <байткод> - đây là mã ở định dạng đầu ra của trình biên dịch mã bpf_asm theo mặc định, ví dụ:

$ 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

Trong ví dụ này, chúng tôi đang lọc tất cả các gói UDP. Bối cảnh cho chương trình BPF trong một mô-đun xt_bpftất nhiên, trỏ đến dữ liệu gói, trong trường hợp iptables, đến phần đầu của tiêu đề IPv4. Giá trị trả về từ chương trình BPF booleanĐâu false có nghĩa là gói không khớp.

Rõ ràng là mô-đun xt_bpf hỗ trợ các bộ lọc phức tạp hơn ví dụ trên. Hãy xem các ví dụ thực tế từ Cloudfare. Cho đến gần đây họ đã sử dụng mô-đun xt_bpf để bảo vệ chống lại các cuộc tấn công DDoS. Trong bài viết Giới thiệu Công cụ BPF họ giải thích cách thức (và lý do) họ tạo các bộ lọc BPF và xuất bản các liên kết tới một tập hợp các tiện ích để tạo các bộ lọc như vậy. Ví dụ: sử dụng tiện ích bpfgen bạn có thể tạo chương trình BPF khớp với truy vấn DNS cho tên 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

Trong chương trình đầu tiên chúng ta nạp vào sổ đăng ký X địa chỉ đầu dòng x04habrx03comx00 bên trong datagram UDP và sau đó kiểm tra yêu cầu: 0x04686162 <-> "x04hab" vv

Một lát sau, Cloudfare xuất bản mã trình biên dịch p0f -> BPF. Trong bài viết Giới thiệu trình biên dịch p0f BPF họ nói về p0f là gì và cách chuyển đổi chữ ký p0f thành 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,
...

Hiện tại không còn sử dụng Cloudfare nữa xt_bpf, vì họ đã chuyển sang XDP - một trong các tùy chọn để sử dụng phiên bản BPF mới, hãy xem. L4Drop: Giảm nhẹ DDoS XDP.

cls_bpf

Ví dụ cuối cùng về việc sử dụng BPF cổ điển trong kernel là trình phân loại cls_bpf cho hệ thống con kiểm soát giao thông trong Linux, được thêm vào Linux vào cuối năm 2013 và thay thế về mặt khái niệm cho hệ thống con cổ xưa cls_u32.

Tuy nhiên, bây giờ chúng tôi sẽ không mô tả công việc cls_bpf, vì theo quan điểm kiến ​​​​thức về BPF cổ điển, điều này sẽ không mang lại cho chúng ta bất cứ điều gì - chúng ta đã làm quen với tất cả các chức năng. Ngoài ra, trong các bài viết tiếp theo nói về BPF mở rộng, chúng ta sẽ gặp bộ phân loại này nhiều lần.

Một lý do khác không nên nói về việc sử dụng BPF cổ điển c cls_bpf Vấn đề là, so với BPF mở rộng, phạm vi áp dụng trong trường hợp này bị thu hẹp hoàn toàn: các chương trình cổ điển không thể thay đổi nội dung của gói và không thể lưu trạng thái giữa các cuộc gọi.

Vì vậy, đã đến lúc nói lời tạm biệt với BPF cổ điển và hướng tới tương lai.

Chia tay BPF cổ điển

Chúng tôi đã xem xét cách công nghệ BPF, được phát triển vào đầu những năm 32, đã tồn tại thành công trong một phần tư thế kỷ và cho đến cuối cùng đã tìm ra những ứng dụng mới. Tuy nhiên, tương tự như quá trình chuyển đổi từ máy xếp chồng sang RISC, vốn đóng vai trò là động lực cho sự phát triển của BPF cổ điển, vào những năm 64 đã có sự chuyển đổi từ máy XNUMX bit sang XNUMX bit và BPF cổ điển bắt đầu trở nên lỗi thời. Ngoài ra, khả năng của BPF cổ điển rất hạn chế và ngoài kiến ​​trúc lỗi thời - chúng tôi không có khả năng lưu trạng thái giữa các lệnh gọi đến chương trình BPF, không có khả năng tương tác trực tiếp với người dùng, không có khả năng tương tác với kernel, ngoại trừ việc đọc một số trường cấu trúc hạn chế sk_buff và khởi chạy các chức năng trợ giúp đơn giản nhất, bạn không thể thay đổi nội dung của các gói và chuyển hướng chúng.

Trên thực tế, hiện tại tất cả những gì còn lại của BPF cổ điển trong Linux là giao diện API và bên trong kernel, tất cả các chương trình cổ điển, có thể là bộ lọc socket hoặc bộ lọc seccomp, đều được tự động dịch sang định dạng mới, BPF mở rộng. (Chúng ta sẽ nói chính xác điều này xảy ra như thế nào trong bài viết tiếp theo.)

Quá trình chuyển đổi sang kiến ​​trúc mới bắt đầu vào năm 2013, khi Alexey Starovoitov đề xuất kế hoạch cập nhật BPF. Năm 2014 các bản vá tương ứng bắt đầu xuất hiện trong cốt lõi. Theo tôi hiểu, kế hoạch ban đầu chỉ là tối ưu hóa kiến ​​trúc và trình biên dịch JIT để chạy hiệu quả hơn trên máy 64-bit, nhưng thay vào đó, những tối ưu hóa này đã đánh dấu sự khởi đầu của một chương mới trong quá trình phát triển Linux.

Các bài viết tiếp theo trong loạt bài này sẽ đề cập đến kiến ​​trúc và ứng dụng của công nghệ mới, ban đầu được gọi là BPF nội bộ, sau đó là BPF mở rộng và bây giờ đơn giản là BPF.

tài liệu tham khảo

  1. Steven McCanne và Van Jacobson, "Bộ lọc gói BSD: Kiến trúc mới để thu thập gói ở cấp độ người dùng", https://www.tcpdump.org/papers/bpf-usenix93.pdf
  2. Steven McCanne, "libpcap: Phương pháp kiến ​​trúc và tối ưu hóa để chụp gói", https://sharkfestus.wireshark.org/sharkfest.11/presentations/McCanne-Sharkfest'11_Keynote_Address.pdf
  3. tcpdump, libpcap: https://www.tcpdump.org/
  4. Hướng dẫn trận đấu IPtable U32.
  5. BPF - mã byte bị lãng quên: https://blog.cloudflare.com/bpf-the-forgotten-bytecode/
  6. Giới thiệu Công cụ BPF: https://blog.cloudflare.com/introducing-the-bpf-tools/
  7. bpf_cls: http://man7.org/linux/man-pages/man8/tc-bpf.8.html
  8. Tổng quan về seccomp: https://lwn.net/Articles/656307/
  9. https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/seccomp_filter.rst
  10. habr: Container và bảo mật: seccomp
  11. habr: Cô lập daemon với systemd hoặc "bạn không cần Docker cho việc này!"
  12. Paul Chaignon, "strace --seccomp-bpf: cái nhìn sâu hơn", https://fosdem.org/2020/schedule/event/debugging_strace_bpf/
  13. netsniff-ng: http://netsniff-ng.org/

Nguồn: www.habr.com

Thêm một lời nhận xét