Chúng tôi viết bảo vệ chống lại các cuộc tấn công DDoS trên XDP. phần hạt nhân

Công nghệ Đường dẫn dữ liệu eXpress (XDP) cho phép xử lý lưu lượng ngẫu nhiên được thực hiện trên giao diện Linux trước khi các gói đi vào ngăn xếp mạng hạt nhân. Ứng dụng XDP - bảo vệ chống lại các cuộc tấn công DDoS (CloudFlare), bộ lọc phức tạp, thu thập số liệu thống kê (Netflix). Các chương trình XDP được thực thi bởi máy ảo eBPF, do đó chúng có các hạn chế về cả mã và các hàm kernel khả dụng tùy thuộc vào loại bộ lọc.

Bài viết nhằm bổ sung những thiếu sót của nhiều tài liệu về XDP. Đầu tiên, họ cung cấp mã làm sẵn ngay lập tức bỏ qua các tính năng của XDP: nó được chuẩn bị để xác minh hoặc quá đơn giản để gây ra sự cố. Sau đó, khi bạn cố gắng viết mã từ đầu, bạn sẽ không biết phải làm gì với những lỗi điển hình. Thứ hai, các cách kiểm tra XDP cục bộ mà không cần VM và phần cứng không được đề cập, mặc dù thực tế là chúng có những cạm bẫy riêng. Văn bản này dành cho các lập trình viên quen thuộc với mạng và Linux, những người quan tâm đến XDP và eBPF.

Trong phần này, chúng ta sẽ hiểu chi tiết cách lắp ráp bộ lọc XDP và cách kiểm tra nó, sau đó chúng ta sẽ viết một phiên bản đơn giản của cơ chế cookie SYN nổi tiếng ở cấp độ xử lý gói. Chúng tôi sẽ chưa tạo “danh sách trắng”
khách hàng đã được xác minh, giữ bộ đếm và quản lý bộ lọc - đủ nhật ký.

Chúng tôi sẽ viết bằng C - nó không thời trang nhưng thực tế. Tất cả mã đều có sẵn trên GitHub thông qua liên kết ở cuối và được chia thành các cam kết theo các giai đoạn được mô tả trong bài viết.

Khước từ. Trong suốt bài viết này, tôi sẽ phát triển một giải pháp nhỏ để chống lại các cuộc tấn công DDoS, vì đây là một nhiệm vụ thực tế đối với XDP và lĩnh vực chuyên môn của tôi. Tuy nhiên, mục tiêu chính là hiểu công nghệ; đây không phải là hướng dẫn để tạo ra sự bảo vệ sẵn có. Mã hướng dẫn không được tối ưu hóa và bỏ sót một số sắc thái.

Tổng quan ngắn gọn về XDP

Tôi sẽ chỉ phác thảo những điểm chính để không trùng lặp tài liệu và các bài viết hiện có.

Vì vậy, mã bộ lọc được tải vào kernel. Bộ lọc được thông qua các gói đến. Kết quả là bộ lọc phải đưa ra quyết định: chuyển gói tin đến kernel (XDP_PASS), thả gói (XDP_DROP) hoặc gửi lại (XDP_TX). Bộ lọc có thể thay đổi gói, điều này đặc biệt đúng đối với XDP_TX. Bạn cũng có thể làm hỏng chương trình (XDP_ABORTED) và bỏ gói, nhưng điều này cũng tương tự assert(0) - để gỡ lỗi.

Máy ảo eBPF (Bộ lọc gói Berkley mở rộng) được cố ý làm đơn giản để nhân có thể kiểm tra xem mã có lặp lại và không làm hỏng bộ nhớ của người khác hay không. Hạn chế và kiểm tra tích lũy:

  • Vòng lặp (nhảy lại) bị cấm.
  • Có một ngăn xếp dữ liệu, nhưng không có chức năng (tất cả các chức năng C phải được nội tuyến).
  • Truy cập vào bộ nhớ bên ngoài ngăn xếp và bộ đệm gói bị cấm.
  • Kích thước của mã bị hạn chế, nhưng trong thực tế, điều này không đáng kể lắm.
  • Chỉ các chức năng hạt nhân đặc biệt (trình trợ giúp eBPF) mới được phép.

Phát triển và cài đặt bộ lọc trông như thế này:

  1. mã nguồn (ví dụ. kernel.c) biên dịch thành đối tượng (kernel.o) cho kiến ​​trúc máy ảo eBPF. Kể từ tháng 2019 năm 10.1, việc biên dịch thành eBPF được Clang hỗ trợ và được hứa hẹn trong GCC XNUMX.
  2. Nếu trong mã đối tượng này có các cuộc gọi đến cấu trúc hạt nhân (ví dụ: bảng và bộ đếm), thay vì ID của chúng có số XNUMX, nghĩa là mã đó không thể được thực thi. Trước khi tải vào kernel, các số XNUMX này phải được thay thế bằng ID của các đối tượng cụ thể được tạo thông qua các lệnh gọi kernel (liên kết mã). Bạn có thể làm điều này với các tiện ích bên ngoài hoặc bạn có thể viết một chương trình sẽ liên kết và tải một bộ lọc cụ thể.
  3. Hạt nhân xác minh chương trình đang được tải. Nó kiểm tra sự vắng mặt của các chu trình và không thoát khỏi ranh giới gói và ngăn xếp. Nếu người xác minh không thể chứng minh rằng mã là chính xác, thì chương trình sẽ bị từ chối - người ta phải có khả năng làm hài lòng anh ta.
  4. Sau khi xác minh thành công, nhân sẽ biên dịch mã đối tượng kiến ​​trúc eBPF thành mã máy kiến ​​trúc hệ thống (đúng lúc).
  5. Chương trình được gắn vào giao diện và bắt đầu xử lý các gói tin.

Vì XDP chạy trong nhân, nên việc gỡ lỗi dựa trên nhật ký theo dõi và trên thực tế, trên các gói mà chương trình lọc hoặc tạo. Tuy nhiên, eBPF giữ mã được tải xuống an toàn cho hệ thống, vì vậy bạn có thể thử nghiệm với XDP ngay trên Linux cục bộ của mình.

Chuẩn bị Môi trường

Lắp ráp

Clang không thể trực tiếp phát hành mã đối tượng cho kiến ​​trúc eBPF, vì vậy quy trình bao gồm hai bước:

  1. Biên dịch mã C thành mã byte LLVM (clang -emit-llvm).
  2. Chuyển đổi mã byte thành mã đối tượng eBPF (llc -march=bpf -filetype=obj).

Khi viết bộ lọc, một vài tệp có chức năng phụ trợ và macro sẽ rất hữu ích từ các bài kiểm tra hạt nhân. Điều quan trọng là chúng khớp với phiên bản kernel (KVER). Tải chúng xuống helpers/:

export KVER=v5.3.7
export BASE=https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/plain/tools/testing/selftests/bpf
wget -P helpers --content-disposition "${BASE}/bpf_helpers.h?h=${KVER}" "${BASE}/bpf_endian.h?h=${KVER}"
unset KVER BASE

Makefile cho Arch Linux (nhân 5.3.7):

CLANG ?= clang
LLC ?= llc

KDIR ?= /lib/modules/$(shell uname -r)/build
ARCH ?= $(subst x86_64,x86,$(shell uname -m))

CFLAGS = 
    -Ihelpers 
    
    -I$(KDIR)/include 
    -I$(KDIR)/include/uapi 
    -I$(KDIR)/include/generated/uapi 
    -I$(KDIR)/arch/$(ARCH)/include 
    -I$(KDIR)/arch/$(ARCH)/include/generated 
    -I$(KDIR)/arch/$(ARCH)/include/uapi 
    -I$(KDIR)/arch/$(ARCH)/include/generated/uapi 
    -D__KERNEL__ 
    
    -fno-stack-protector -O2 -g

xdp_%.o: xdp_%.c Makefile
    $(CLANG) -c -emit-llvm $(CFLAGS) $< -o - | 
    $(LLC) -march=bpf -filetype=obj -o $@

.PHONY: all clean

all: xdp_filter.o

clean:
    rm -f ./*.o

KDIR chứa đường dẫn đến tiêu đề hạt nhân, ARCH - Kiến Trúc Hệ Thống. Đường dẫn và công cụ có thể hơi khác nhau giữa các bản phân phối.

Ví dụ về sự khác biệt cho Debian 10 (kernel 4.19.67)

# другая команда
CLANG ?= clang
LLC ?= llc-7

# другой каталог
KDIR ?= /usr/src/linux-headers-$(shell uname -r)
ARCH ?= $(subst x86_64,x86,$(shell uname -m))

# два дополнительных каталога -I
CFLAGS = 
    -Ihelpers 
    
    -I/usr/src/linux-headers-4.19.0-6-common/include 
    -I/usr/src/linux-headers-4.19.0-6-common/arch/$(ARCH)/include 
    # далее без изменений

CFLAGS bao gồm một thư mục có tiêu đề phụ và một số thư mục có tiêu đề hạt nhân. Biểu tượng __KERNEL__ có nghĩa là các tiêu đề UAPI (API không gian người dùng) được xác định cho mã nhân, do bộ lọc được thực thi trong nhân.

Bảo vệ ngăn xếp có thể bị vô hiệu hóa (-fno-stack-protector) bởi vì trình xác minh mã eBPF vẫn kiểm tra các ranh giới không nằm ngoài ngăn xếp. Bạn nên kích hoạt tối ưu hóa ngay lập tức vì kích thước của mã byte eBPF bị giới hạn.

Hãy bắt đầu với một bộ lọc vượt qua tất cả các gói và không làm gì cả:

#include <uapi/linux/bpf.h>

#include <bpf_helpers.h>

SEC("prog")
int xdp_main(struct xdp_md* ctx) {
    return XDP_PASS;
}

char _license[] SEC("license") = "GPL";

Đội make sưu tầm xdp_filter.o. Bạn có thể kiểm tra nó ở đâu bây giờ?

Kiểm tra đứng

Giá đỡ phải bao gồm hai giao diện: trên đó sẽ có bộ lọc và từ đó các gói sẽ được gửi. Đây phải là các thiết bị Linux hoàn chỉnh có IP riêng để kiểm tra cách các ứng dụng thông thường hoạt động với bộ lọc của chúng tôi.

Các thiết bị như veth (Ethernet ảo) phù hợp với chúng tôi: chúng là một cặp giao diện mạng ảo được “kết nối” trực tiếp với nhau. Bạn có thể tạo chúng như thế này (trong phần này, tất cả các lệnh ip thực hiện từ root):

ip link add xdp-remote type veth peer name xdp-local

Здесь xdp-remote и xdp-local - tên thiết bị. TRÊN xdp-local (192.0.2.1/24) một bộ lọc sẽ được đính kèm, với xdp-remote (192.0.2.2/24) lưu lượng đến sẽ được gửi đi. Tuy nhiên, có một vấn đề: các giao diện nằm trên cùng một máy và Linux sẽ không gửi lưu lượng đến máy này thông qua máy kia. Bạn có thể giải quyết nó với các quy tắc phức tạp iptables, nhưng họ sẽ phải thay đổi các gói, điều này gây bất tiện khi gỡ lỗi. Tốt hơn là sử dụng các không gian tên mạng (không gian tên mạng, hơn nữa là netns).

Không gian tên mạng chứa một tập hợp các giao diện, bảng định tuyến và quy tắc NetFilter được tách biệt khỏi các đối tượng tương tự trong các mạng khác. Mỗi quy trình chạy trong một số không gian tên và chỉ các đối tượng của mạng này mới có sẵn cho nó. Theo mặc định, hệ thống có một không gian tên mạng duy nhất cho tất cả các đối tượng, vì vậy bạn có thể làm việc trên Linux mà không cần biết về mạng.

Hãy tạo một không gian tên mới xdp-test và di chuyển đến đó xdp-remote.

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

Sau đó, quá trình chạy trong xdp-test, sẽ không thấy" xdp-local (mặc định nó vẫn nằm trong netns) và khi gửi một gói đến 192.0.2.1 thì nó sẽ chuyển qua xdp-remote, vì đó là giao diện duy nhất tại 192.0.2.0/24 khả dụng cho quá trình này. Điều này cũng hoạt động ngược lại.

Khi di chuyển giữa các mạng, giao diện bị hỏng và mất địa chỉ. Để thiết lập giao diện trong netns, bạn cần chạy ip ... trong không gian tên lệnh này ip netns exec:

ip netns exec xdp-test 
    ip address add 192.0.2.2/24 dev xdp-remote
ip netns exec xdp-test 
    ip link set xdp-remote up

Như bạn có thể thấy, điều này không khác gì cài đặt xdp-local trong không gian tên mặc định:

    ip address add 192.0.2.1/24 dev xdp-local
    ip link set xdp-local up

Nếu chạy tcpdump -tnevi xdp-local, bạn có thể thấy rằng các gói được gửi từ xdp-test, được gửi đến giao diện này:

ip netns exec xdp-test   ping 192.0.2.1

Thật tiện lợi khi chạy Shell trong xdp-test. Kho lưu trữ có một tập lệnh tự động hóa làm việc với chân đế, ví dụ, bạn có thể thiết lập chân đế bằng lệnh sudo ./stand up và loại bỏ nó sudo ./stand down.

truy tìm

Bộ lọc được gắn vào thiết bị như thế này:

ip -force link set dev xdp-local xdp object xdp_filter.o verbose

Ключ -force cần thiết để liên kết một chương trình mới nếu một chương trình khác đã được liên kết. "Không có tin tức là tin tốt" không phải là về lệnh này, dù sao thì đầu ra cũng rất lớn. biểu thị verbose tùy chọn, nhưng cùng với nó, một báo cáo về công việc của trình xác minh mã với danh sách trình biên dịch mã sẽ xuất hiện:

Verifier analysis:

0: (b7) r0 = 2
1: (95) exit

Tách chương trình ra khỏi giao diện:

ip link set dev xdp-local xdp off

Trong kịch bản, đây là các lệnh sudo ./stand attach и sudo ./stand detach.

Bằng cách ràng buộc bộ lọc, bạn có thể đảm bảo rằng ping tiếp tục hoạt động, nhưng chương trình có hoạt động không? Hãy thêm logo. Chức năng bpf_trace_printk() tương tự như printf(), nhưng chỉ hỗ trợ tối đa ba đối số ngoài mẫu và một danh sách giới hạn các chỉ định. vĩ mô bpf_printk() đơn giản hóa cuộc gọi.

   SEC("prog")
   int xdp_main(struct xdp_md* ctx) {
+      bpf_printk("got packet: %pn", ctx);
       return XDP_PASS;
   }

Đầu ra chuyển đến kênh theo dõi kernel, kênh này cần được bật:

echo -n 1 | sudo tee /sys/kernel/debug/tracing/options/trace_printk

Xem lưu lượng tin nhắn:

cat /sys/kernel/debug/tracing/trace_pipe

Cả hai đội này đều call sudo ./stand log.

Ping bây giờ sẽ tạo ra các thông báo như thế này trong đó:

<...>-110930 [004] ..s1 78803.244967: 0: got packet: 00000000ac510377

Nếu bạn nhìn kỹ vào đầu ra của trình xác minh, bạn có thể nhận thấy các phép tính lạ:

0: (bf) r3 = r1
1: (18) r1 = 0xa7025203a7465
3: (7b) *(u64 *)(r10 -8) = r1
4: (18) r1 = 0x6b63617020746f67
6: (7b) *(u64 *)(r10 -16) = r1
7: (bf) r1 = r10
8: (07) r1 += -16
9: (b7) r2 = 16
10: (85) call bpf_trace_printk#6
<...>

Thực tế là các chương trình eBPF không có phần dữ liệu, vì vậy cách duy nhất để mã hóa chuỗi định dạng là các đối số trực tiếp của các lệnh VM:

$ python -c "import binascii; print(bytes(reversed(binascii.unhexlify('0a7025203a74656b63617020746f67'))))"
b'got packet: %pn'

Vì lý do này, đầu ra gỡ lỗi làm phình to mã kết quả.

Gửi gói XDP

Hãy thay đổi bộ lọc: để nó gửi lại tất cả các gói đến. Điều này là không chính xác từ quan điểm mạng, vì cần phải thay đổi địa chỉ trong các tiêu đề, nhưng bây giờ công việc về nguyên tắc là quan trọng.

       bpf_printk("got packet: %pn", ctx);
-      return XDP_PASS;
+      return XDP_TX;
   }

Phóng tcpdump trên xdp-remote. Nó sẽ hiển thị Yêu cầu tiếng vang ICMP đi và đến giống hệt nhau và ngừng hiển thị Trả lời tiếng vang ICMP. Nhưng nó không hiển thị. Hóa ra là làm việc XDP_TX trong chương trình cho xdp-local cần thiếtđể ghép nối giao diện xdp-remote một chương trình cũng được chỉ định, ngay cả khi nó trống và nó đã được nâng lên.

Làm sao tôi biết được?

Theo dõi đường dẫn của một gói trong kernel Nhân tiện, cơ chế sự kiện perf cho phép sử dụng cùng một máy ảo, nghĩa là eBPF được sử dụng để tháo gỡ với eBPF.

Bạn phải làm điều tốt từ điều xấu, bởi vì không có gì khác để làm điều đó.

$ sudo perf trace --call-graph dwarf -e 'xdp:*'
   0.000 ping/123455 xdp:xdp_bulk_tx:ifindex=19 action=TX sent=0 drops=1 err=-6
                                     veth_xdp_flush_bq ([veth])
                                     veth_xdp_flush_bq ([veth])
                                     veth_poll ([veth])
                                     <...>

Mã 6 là gì?

$ errno 6
ENXIO 6 No such device or address

Chức năng veth_xdp_flush_bq() nhận mã lỗi từ veth_xdp_xmit(), nơi tìm kiếm theo ENXIO và tìm nhận xét.

Khôi phục bộ lọc tối thiểu (XDP_PASS) trong tập tin xdp_dummy.c, thêm nó vào Makefile, liên kết với xdp-remote:

ip netns exec remote 
    ip link set dev int xdp object dummy.o

Bây giờ tcpdump cho thấy những gì được mong đợi:

62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), length 84)
    192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, length 64
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), length 84)
    192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, length 64

Nếu chỉ ARP được hiển thị thay vào đó, bạn cần xóa các bộ lọc (điều này làm cho sudo ./stand detach), cho phép ping, sau đó cài đặt bộ lọc và thử lại. Vấn đề là bộ lọc XDP_TX cũng ảnh hưởng đến ARP và nếu ngăn xếp
không gian tên xdp-test quản lý để "quên" địa chỉ MAC 192.0.2.1, anh ta sẽ không thể phân giải IP này.

Báo cáo sự cố

Hãy chuyển sang nhiệm vụ đã nêu: viết cơ chế cookie SYN trên XDP.

Cho đến nay, lũ SYN vẫn là một cuộc tấn công DDoS phổ biến, bản chất của nó như sau. Khi kết nối được thiết lập (bắt tay TCP), máy chủ sẽ nhận được SYN, phân bổ tài nguyên cho kết nối trong tương lai, phản hồi bằng gói SYNACK và đợi ACK. Kẻ tấn công chỉ cần gửi các gói SYN từ các địa chỉ giả với số lượng hàng nghìn mỗi giây từ mỗi máy chủ trong một mạng botnet có nhiều nghìn. Máy chủ buộc phải phân bổ tài nguyên ngay khi gói đến, nhưng sẽ giải phóng nó sau một thời gian chờ lâu, do đó, bộ nhớ hoặc giới hạn đã hết, kết nối mới không được chấp nhận, dịch vụ không khả dụng.

Nếu bạn không phân bổ tài nguyên trên gói SYN mà chỉ phản hồi bằng gói SYNACK thì làm sao máy chủ hiểu được gói ACK đến sau thuộc gói SYN chưa được lưu? Xét cho cùng, kẻ tấn công cũng có thể tạo ACK giả. Bản chất của cookie SYN là mã hóa seqnum tham số kết nối dưới dạng hàm băm của địa chỉ, cổng và thay đổi muối. Nếu ACK được quản lý để đến trước khi thay đổi muối, bạn có thể tính toán lại hàm băm và so sánh với acknum. giả mạo acknum kẻ tấn công không thể, vì muối bao gồm bí mật và sẽ không có thời gian để sắp xếp nó do kênh hạn chế.

Cookie SYN đã được triển khai trong nhân Linux từ lâu và thậm chí có thể được bật tự động nếu SYN đến quá nhanh và quá nhiều.

Chương trình giáo dục về bắt tay TCP

TCP cung cấp khả năng truyền dữ liệu dưới dạng luồng byte, ví dụ: các yêu cầu HTTP được truyền qua TCP. Luồng được truyền từng mảnh trong các gói. Tất cả các gói TCP đều có cờ logic và số thứ tự 32 bit:

  • Sự kết hợp của các cờ xác định vai trò của một gói cụ thể. Cờ SYN có nghĩa đây là gói đầu tiên của người gửi trên kết nối. Cờ ACK có nghĩa là người gửi đã nhận được tất cả dữ liệu kết nối lên đến một byte. acknum. Một gói có thể có một số cờ và được đặt tên theo sự kết hợp của chúng, ví dụ: gói SYNACK.

  • Số thứ tự (seqnum) chỉ định phần bù trong luồng dữ liệu cho byte đầu tiên được gửi trong gói này. Ví dụ: nếu trong gói đầu tiên có X byte dữ liệu, con số này là N, thì trong gói tiếp theo có dữ liệu mới, con số này sẽ là N+X. Khi bắt đầu kết nối, mỗi bên chọn số này một cách ngẫu nhiên.

  • Số xác nhận (acknum) - phần bù tương tự như số thứ tự, nhưng nó không xác định số lượng byte được truyền mà là số lượng byte đầu tiên từ người nhận mà người gửi không nhìn thấy.

Khi bắt đầu kết nối, các bên phải thỏa thuận seqnum и acknum. Máy khách gửi một gói SYN với seqnum = X. Máy chủ phản hồi bằng một gói SYNACK, nơi nó tự viết seqnum = Y và phơi bày acknum = X + 1. Máy khách phản hồi SYNACK bằng gói ACK, trong đó seqnum = X + 1, acknum = Y + 1. Sau đó, quá trình truyền dữ liệu thực sự bắt đầu.

Nếu người đối thoại không xác nhận đã nhận gói, TCP sẽ gửi lại gói đó khi hết thời gian chờ.

Tại sao cookie SYN không phải lúc nào cũng được sử dụng?

Đầu tiên, nếu SYNACK hoặc ACK bị mất, bạn sẽ phải đợi gửi lại - quá trình thiết lập kết nối bị chậm lại. Thứ hai, trong gói SYN - và chỉ trong gói đó! - một số tùy chọn được truyền ảnh hưởng đến hoạt động tiếp theo của kết nối. Không nhớ các gói SYN đến, máy chủ do đó bỏ qua các tùy chọn này, trong các gói sau, máy khách sẽ không gửi chúng nữa. TCP có thể hoạt động trong trường hợp này, nhưng ít nhất ở giai đoạn đầu, chất lượng kết nối sẽ giảm.

Về các gói, một chương trình XDP nên thực hiện như sau:

  • phản hồi SYN bằng SYNACK với cookie;
  • trả lời ACK bằng RST (ngắt kết nối);
  • bỏ các gói tin khác.

Mã giả của thuật toán cùng với phân tích gói tin:

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

Một (*) các điểm mà bạn cần quản lý trạng thái của hệ thống được đánh dấu - ở giai đoạn đầu tiên, bạn có thể thực hiện mà không cần chúng bằng cách thực hiện bắt tay TCP với việc tạo cookie SYN dưới dạng số thứ tự.

Trên công trường (**), trong khi chúng tôi không có bảng, chúng tôi sẽ bỏ qua gói tin.

Thực hiện bắt tay TCP

Phân tích cú pháp gói và xác minh mã

Chúng tôi cần cấu trúc tiêu đề mạng: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) và TCP (uapi/linux/tcp.h). Cái cuối cùng tôi không thể kết nối do lỗi liên quan đến atomic64_t, tôi đã phải sao chép các định nghĩa cần thiết vào mã.

Tất cả các chức năng được phân biệt trong C để có thể đọc được phải được nội tuyến tại vị trí cuộc gọi, vì trình xác minh eBPF trong hạt nhân cấm các bước nhảy ngược, trên thực tế, các vòng lặp và lệnh gọi hàm.

#define INTERNAL static __attribute__((always_inline))

Macro LOG() vô hiệu hóa in trong bản dựng phát hành.

Chương trình là một đường dẫn của các chức năng. Mỗi người nhận được một gói trong đó tiêu đề của cấp độ tương ứng được tô sáng, ví dụ: process_ether() chờ đợi để được lấp đầy ether. Dựa trên kết quả phân tích trường, chức năng có thể chuyển gói tin lên cấp độ cao hơn. Kết quả của chức năng là một hành động XDP. Trong khi trình xử lý SYN và ACK cho phép tất cả các gói đi qua.

struct Packet {
    struct xdp_md* ctx;

    struct ethhdr* ether;
    struct iphdr* ip;
    struct tcphdr* tcp;
};

INTERNAL int process_tcp_syn(struct Packet* packet) { return XDP_PASS; }
INTERNAL int process_tcp_ack(struct Packet* packet) { return XDP_PASS; }
INTERNAL int process_tcp(struct Packet* packet) { ... }
INTERNAL int process_ip(struct Packet* packet) { ... }

INTERNAL int
process_ether(struct Packet* packet) {
    struct ethhdr* ether = packet->ether;

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

    if (ether->h_proto != bpf_ntohs(ETH_P_IP)) {
        return XDP_PASS;
    }

    // B
    struct iphdr* ip = (struct iphdr*)(ether + 1);
    if ((void*)(ip + 1) > (void*)packet->ctx->data_end) {
        return XDP_DROP; /* malformed packet */
    }

    packet->ip = ip;
    return process_ip(packet);
}

SEC("prog")
int xdp_main(struct xdp_md* ctx) {
    struct Packet packet;
    packet.ctx = ctx;

    // A
    struct ethhdr* ether = (struct ethhdr*)(void*)ctx->data;
    if ((void*)(ether + 1) > (void*)ctx->data_end) {
        return XDP_PASS;
    }

    packet.ether = ether;
    return process_ether(&packet);
}

Mình chú ý check A và B. Nếu comment ra A thì chương trình sẽ build, nhưng sẽ báo lỗi verify khi load:

Verifier analysis:

<...>
11: (7b) *(u64 *)(r10 -48) = r1
12: (71) r3 = *(u8 *)(r7 +13)
invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0)
R7 offset is outside of the packet
processed 11 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0

Error fetching program/map!

Chuỗi chìa khóa invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): có các đường dẫn thực thi khi byte thứ mười ba kể từ khi bắt đầu bộ đệm nằm ngoài gói. Thật khó để biết chúng ta đang nói về dòng nào từ danh sách, nhưng có một số hướng dẫn (12) và một trình phân tách mã hiển thị các dòng mã nguồn:

llvm-objdump -S xdp_filter.o | less

Trong trường hợp này, nó trỏ đến dòng

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

điều đó làm cho nó rõ ràng rằng vấn đề là ether. Nó sẽ luôn như vậy.

Trả lời SYN

Mục tiêu ở giai đoạn này là tạo một gói SYNACK chính xác với một seqnum, sẽ được thay thế bằng cookie SYN trong tương lai. Tất cả các thay đổi diễn ra trong process_tcp_syn() và môi trường xung quanh.

Kiểm tra gói

Thật kỳ lạ, đây là dòng đáng chú ý nhất, hay đúng hơn là một nhận xét về nó:

/* Required to verify checksum calculation */
const void* data_end = (const void*)ctx->data_end;

Khi viết phiên bản mã đầu tiên, kernel 5.1 đã được sử dụng, đối với trình xác minh có sự khác biệt giữa data_end и (const void*)ctx->data_end. Tại thời điểm viết bài này, kernel 5.3.1 không gặp vấn đề này. Có lẽ trình biên dịch đã truy cập một biến cục bộ khác với một trường. Đạo đức - trên một tổ hợp lớn, đơn giản hóa mã có thể hữu ích.

Các kiểm tra định kỳ khác về độ dài cho vinh quang của người xác minh; Ô MAX_CSUM_BYTES bên dưới

const u32 ip_len = ip->ihl * 4;
if ((void*)ip + ip_len > data_end) {
    return XDP_DROP; /* malformed packet */
}
if (ip_len > MAX_CSUM_BYTES) {
    return XDP_ABORTED; /* implementation limitation */
}

const u32 tcp_len = tcp->doff * 4;
if ((void*)tcp + tcp_len > (void*)ctx->data_end) {
    return XDP_DROP; /* malformed packet */
}
if (tcp_len > MAX_CSUM_BYTES) {
    return XDP_ABORTED; /* implementation limitation */
}

trải gói

Chúng tôi điền seqnum и acknum, đặt ACK (đã đặt SYN):

const u32 cookie = 42;
tcp->ack_seq = bpf_htonl(bpf_ntohl(tcp->seq) + 1);
tcp->seq = bpf_htonl(cookie);
tcp->ack = 1;

Hoán đổi cổng TCP, địa chỉ IP và MAC. Thư viện chuẩn không có sẵn trong chương trình XDP, vì vậy memcpy() — một macro ẩn nội tại Clang.

const u16 temp_port = tcp->source;
tcp->source = tcp->dest;
tcp->dest = temp_port;

const u32 temp_ip = ip->saddr;
ip->saddr = ip->daddr;
ip->daddr = temp_ip;

struct ethhdr temp_ether = *ether;
memcpy(ether->h_dest, temp_ether.h_source, ETH_ALEN);
memcpy(ether->h_source, temp_ether.h_dest, ETH_ALEN);

tính toán lại tổng kiểm tra

Tổng kiểm tra IPv4 và TCP yêu cầu bổ sung tất cả các từ 16 bit trong tiêu đề và kích thước của tiêu đề được ghi trong chúng, tức là không xác định được tại thời điểm biên dịch. Đây là một vấn đề vì trình xác minh sẽ không bỏ qua vòng lặp bình thường cho đến biến biên. Nhưng kích thước của các tiêu đề bị giới hạn: tối đa 64 byte mỗi tiêu đề. Bạn có thể thực hiện một vòng lặp với số lần lặp cố định, có thể kết thúc sớm.

Tôi lưu ý rằng có RFC 1624 về cách tính toán lại một phần tổng kiểm tra nếu chỉ các từ cố định của gói bị thay đổi. Tuy nhiên, phương pháp này không phổ biến và việc triển khai sẽ khó duy trì hơn.

Hàm tính tổng kiểm tra:

#define MAX_CSUM_WORDS 32
#define MAX_CSUM_BYTES (MAX_CSUM_WORDS * 2)

INTERNAL u32
sum16(const void* data, u32 size, const void* data_end) {
    u32 s = 0;
#pragma unroll
    for (u32 i = 0; i < MAX_CSUM_WORDS; i++) {
        if (2*i >= size) {
            return s; /* normal exit */
        }
        if (data + 2*i + 1 + 1 > data_end) {
            return 0; /* should be unreachable */
        }
        s += ((const u16*)data)[i];
    }
    return s;
}

Mặc dù size được kiểm tra bằng mã gọi, điều kiện thoát thứ hai là cần thiết để người xác minh có thể chứng minh kết thúc vòng lặp.

Đối với các từ 32 bit, một phiên bản đơn giản hơn được triển khai:

INTERNAL u32
sum16_32(u32 v) {
    return (v >> 16) + (v & 0xffff);
}

Trên thực tế tính toán lại tổng kiểm tra và gửi lại gói tin:

ip->check = 0;
ip->check = carry(sum16(ip, ip_len, data_end));

u32 tcp_csum = 0;
tcp_csum += sum16_32(ip->saddr);
tcp_csum += sum16_32(ip->daddr);
tcp_csum += 0x0600;
tcp_csum += tcp_len << 8;
tcp->check = 0;
tcp_csum += sum16(tcp, tcp_len, data_end);
tcp->check = carry(tcp_csum);

return XDP_TX;

Chức năng carry() tạo một tổng kiểm tra trong tổng số 32 bit của các từ 16 bit, theo RFC 791.

Kiểm tra bắt tay TCP

Bộ lọc thiết lập chính xác kết nối với netcat, bỏ qua ACK cuối cùng mà Linux đã phản hồi bằng một gói RST, vì ngăn xếp mạng không nhận được SYN - nó đã được chuyển đổi thành SYNACK và được gửi lại - và theo quan điểm của HĐH, một gói đã đến mà không phải là liên quan đến các kết nối mở.

$ sudo ip netns exec xdp-test   nc -nv 192.0.2.1 6666
192.0.2.1 6666: Connection reset by peer

Điều quan trọng là phải kiểm tra với các ứng dụng chính thức và quan sát tcpdump trên xdp-remote bởi vì, ví dụ, hping3 không trả lời tổng kiểm tra không chính xác.

Theo quan điểm của XDP, bản thân việc kiểm tra là tầm thường. Thuật toán tính toán là nguyên thủy và có thể dễ bị tấn công tinh vi. Ví dụ, nhân Linux sử dụng mã hóa SipHash, nhưng việc triển khai nó cho XDP rõ ràng nằm ngoài phạm vi của bài viết này.

Đã xuất hiện cho TODO mới liên quan đến tương tác bên ngoài:

  • Chương trình XDP không thể lưu trữ cookie_seed (phần bí mật của muối) trong một biến toàn cục, bạn cần một kho lưu trữ hạt nhân có giá trị sẽ được cập nhật định kỳ từ một trình tạo đáng tin cậy.

  • Nếu cookie SYN trong gói ACK khớp, bạn không cần in thông báo nhưng hãy nhớ IP của ứng dụng khách đã xác minh để tiếp tục bỏ qua các gói từ nó.

Xác thực bởi một khách hàng hợp pháp:

$ sudoip netns exec xdp-test   nc -nv 192.0.2.1 6666
192.0.2.1 6666: Connection reset by peer

Nhật ký ghi lại quá trình kiểm tra (flags=0x2 là SYN, flags=0x10 là ACK):

Ether(proto=0x800)
  IP(src=0x20e6e11a dst=0x20e6e11e proto=6)
    TCP(sport=50836 dport=6666 flags=0x2)
Ether(proto=0x800)
  IP(src=0xfe2cb11a dst=0xfe2cb11e proto=6)
    TCP(sport=50836 dport=6666 flags=0x10)
      cookie matches for client 20200c0

Miễn là không có danh sách các IP được kiểm tra, sẽ không có sự bảo vệ nào chống lại lũ SYN, nhưng đây là phản ứng đối với lũ ACK do lệnh này đưa ra:

sudo ip netns exec xdp-test   hping3 --flood -A -s 1111 -p 2222 192.0.2.1

Các mục nhật ký:

Ether(proto=0x800)
  IP(src=0x15bd11a dst=0x15bd11e proto=6)
    TCP(sport=3236 dport=2222 flags=0x10)
      cookie mismatch

Kết luận

Đôi khi eBPF nói chung và XDP nói riêng được trình bày như một công cụ quản trị nâng cao hơn là một nền tảng phát triển. Thật vậy, XDP là một công cụ can thiệp vào quá trình xử lý gói nhân chứ không phải là giải pháp thay thế cho ngăn xếp nhân, như DPDK và các tùy chọn bỏ qua nhân khác. Mặt khác, XDP cho phép bạn triển khai logic khá phức tạp, hơn nữa, dễ dàng cập nhật mà không bị tạm dừng trong quá trình xử lý lưu lượng. Trình xác minh không tạo ra vấn đề lớn, cá nhân tôi sẽ không từ chối như vậy đối với các phần của mã không gian người dùng.

Trong phần thứ hai, nếu chủ đề thú vị, chúng tôi sẽ hoàn thành bảng các máy khách đã xác minh và ngắt kết nối, triển khai bộ đếm và viết tiện ích không gian người dùng để quản lý bộ lọc.

Links:

Nguồn: www.habr.com

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