我們在 XDP 上編寫了針對 DDoS 攻擊的保護措施。 核部分

eXpress 數據路徑 (XDP) 技術允許在數據包進入內核網絡堆棧之前對 Linux 接口上的流量進行任意處理。 XDP 的應用 - 防禦 DDoS 攻擊 (CloudFlare)、複雜過濾器、統計數據收集 (Netflix)。 XDP 程序由 eBPF 虛擬機執行,因此根據過濾器的類型,其代碼和可用的內核函數都受到限制。

這篇文章旨在彌補XDP上眾多資料的不足。 首先,他們提供現成的代碼,可以立即繞過 XDP 的功能:準備用於驗證或太簡單而不會導致問題。 當您稍後嘗試從頭開始編寫自己的代碼時,不知道如何處理典型錯誤。 其次,它沒有涵蓋在沒有虛擬機和硬件的情況下本地測試 XDP 的方法,儘管它們有自己的陷阱。 本文面向熟悉網絡和 Linux、對 XDP 和 eBPF 感興趣的程序員。

在這一部分中,我們將詳細了解 XDP 過濾器是如何組裝以及如何測試它,然後我們將在數據包處理級別編寫眾所周知的 SYN cookies 機制的簡單版本。 直到我們形成“白名單”
驗證客戶端,保留計數器並管理過濾器 - 足夠的日誌。

我們將用 C 語言編寫 - 這並不時尚,但很實用。 所有代碼都可以通過末尾的鏈接在 GitHub 上獲取,並根據文章中描述的步驟分為提交。

免責聲明。 在本文中,我們將開發一個用於抵禦 DDoS 攻擊的小型解決方案,因為這對於 XDP 和我的領域來說是一項現實任務。 然而,主要目標是了解技術,這並不是創建現成保護的指南。 教程代碼未優化並且省略了一些細微差別。

XDP 簡要概述

我將僅陳述要點,以免重複文檔和現有文章。

因此,過濾器代碼被加載到內核中。 過濾器傳遞傳入數據包。 因此,過濾器必須做出決定:將數據包傳遞給內核(XDP_PASS),丟棄數據包(XDP_DROP)或發回(XDP_TX)。 過濾器可以改變封裝,尤其是對於 XDP_TX。 您還可以使程序崩潰(XDP_ABORTED)並放下包裹,但這是類似的 assert(0) - 用於調試。

eBPF(擴展伯克利數據包過濾器)虛擬機故意變得簡單,以便內核可以檢查代碼不循環並且不會損壞其他人的內存。 累積限制和檢查:

  • 禁止循環(跳回)。
  • 有一個用於數據的堆棧,但沒有函數(所有 C 函數都必須內聯)。
  • 禁止訪問堆棧和數據包緩衝區之外的內存。
  • 代碼的大小是有限的,但實際上這並不是很重要。
  • 僅允許特殊的內核函數(eBPF 助手)。

開發和安裝過濾器如下所示:

  1. 源代碼(例如 kernel.c) 編譯為對象 (kernel.o)用於 eBPF 虛擬機架構。 截至 2019 年 10.1 月,Clang 支持編譯為 eBPF,並在 GCC XNUMX 中承諾。
  2. 如果在此目標代碼中存在對內核結構(例如表和計數器)的調用,則它們的 ID 為零,而不是零,也就是說,此類代碼無法執行。 在加載到內核之前,這些零必須替換為通過內核調用創建的特定對象的 ID(鏈接代碼)。 您可以使用外部實用程序來執行此操作,也可以編寫一個程序來鏈接和加載特定的過濾器。
  3. 內核驗證正在加載的程序。 它檢查是否存在循環以及包和堆棧邊界是否未退出。 如果驗證者不能證明代碼是正確的,程序就會被拒絕——必須能夠取悅他。
  4. 驗證成功後,內核將eBPF架構目標代碼編譯為系統架構機器代碼(即時)。
  5. 該程序連接到接口並開始處理數據包。

由於 XDP 在內核中運行,因此調試是通過跟踪日誌進行的,實際上是通過程序過濾或生成的數據包進行的。 但是,eBPF 可以保證下載的代碼對系統的安全,因此您可以在本地 Linux 上試驗 XDP。

準備環境

裝配

Clang 無法直接為 eBPF 架構發布目標代碼,因此該過程由兩個步驟組成:

  1. 將 C 代碼編譯為 LLVM 字節碼(clang -emit-llvm).
  2. 將字節碼轉換為 eBPF 目標代碼(llc -march=bpf -filetype=obj).

編寫過濾器時,幾個帶有輔助函數和宏的文件會派上用場 來自內核測試。 它們與內核版本匹配很重要(KVER)。 將它們下載到 helpers/:

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

Arch Linux(內核 5.3.7)的 Makefile:

CLANG ?= clang
LLC ?= llc

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

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

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

.PHONY: all clean

all: xdp_filter.o

clean:
    rm -f ./*.o

KDIR 包含內核頭的路徑, ARCH - 系統架構。 不同發行版的路徑和工具可能略有不同。

Debian 10(內核 4.19.67)的差異示例

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

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

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

CFLAGS 包括一個帶有輔助頭的目錄和幾個帶有內核頭的目錄。 象徵 __KERNEL__ 意味著 UAPI(用戶空間 API)標頭是為內核代碼定義的,因為過濾器是在內核中執行的。

可以禁用堆棧保護(-fno-stack-protector)因為 eBPF 代碼驗證器無論如何都會檢查非堆棧邊界。 您應該立即啟用優化,因為 eBPF 字節碼的大小是有限的。

讓我們從一個通過所有數據包但不執行任何操作的過濾器開始:

#include <uapi/linux/bpf.h>

#include <bpf_helpers.h>

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

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

團隊 make 收集 xdp_filter.o。 現在哪裡可以測試呢?

試驗台

該支架應包括兩個接口:其上將有一個過濾器,並且將從其發送數據包。 這些必須是具有自己的 IP 的完整 Linux 設備,以便檢查常規應用程序如何與我們的過濾器配合使用。

像veth(虛擬以太網)這樣的設備很適合我們:它們是一對直接相互“連接”的虛擬網絡接口。 您可以像這樣創建它們(在本節中,所有命令 ip 執行自 root):

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

這裡 xdp-remote и xdp-local — 設備名稱。 在 xdp-local (192.0.2.1/24) 將附加一個過濾器,其中 xdp-remote (192.0.2.2/24) 傳入流量將被發送。 然而,有一個問題:這些接口位於同一台機器上,Linux 不會通過其中一個接口向另一個接口發送流量。 你可以用棘手的規則來解決它 iptables,但是必須要換包,調試的時候很不方便。 最好使用網絡命名空間(網絡命名空間,進一步的netns)。

網絡命名空間包含一組接口、路由表和 NetFilter 規則,這些規則與其他 netns 中的類似對象隔離。 每個進程都在某個命名空間中運行,並且只有該網絡命名空間的對象可供其使用。 默認情況下,系統對所有對像都有一個網絡命名空間,因此您可以在 Linux 上工作而無需了解 netns。

讓我們創建一個新的命名空間 xdp-test 並搬到那裡 xdp-remote.

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

然後進程運行在 xdp-test,不會“看到” xdp-local (默認情況下它將保留在 netns 中)並且當發送數據包到 192.0.2.1 時將通過它 xdp-remote,因為這是該進程唯一可用的 192.0.2.0/24 接口。 這也適用於相反的情況。

在 netns 之間移動時,接口會關閉並丟失地址。 要在 netns 中設置接口,您需要運行 ip ... 在此命令命名空間中 ip netns exec:

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

正如你所看到的,這與設置沒有什麼不同 xdp-local 在默認命名空間中:

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

如果運行 tcpdump -tnevi xdp-local,您可以看到發送的數據包 xdp-test,被傳遞到這個接口:

ip netns exec xdp-test   ping 192.0.2.1

運行shell很方便 xdp-test。 存儲庫有一個腳本可以自動使用支架進行工作,例如,您可以使用以下命令設置支架 sudo ./stand up 並刪除它 sudo ./stand down.

追踪

過濾器像這樣連接到設備上:

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

關鍵 -force 如果已經鏈接了另一個程序,則需要鏈接新程序。 “沒有消息就是好消息”與此命令無關,無論如何輸出都是大量的。 表明 verbose 可選,但隨之而來的是有關代碼驗證器工作的報告以及彙編器列表:

Verifier analysis:

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

從界面中分離程序:

ip link set dev xdp-local xdp off

在腳本中,這些是命令 sudo ./stand attach и sudo ./stand detach.

通過綁定過濾器,您可以確保 ping 繼續工作,但是該程序是否有效? 讓我們添加徽標。 功能 bpf_trace_printk() 如同 printf(),但僅支持除模式之外的最多三個參數以及有限的說明符列表。 宏 bpf_printk() 簡化通話。

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

輸出進入內核跟踪通道,需要啟用該通道:

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

查看消息流:

cat /sys/kernel/debug/tracing/trace_pipe

這兩支球隊都打了電話 sudo ./stand log.

Ping 現在應該在其中生成如下消息:

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

如果仔細觀察驗證器的輸出,您會發現奇怪的計算:

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

事實上,eBPF 程序沒有數據部分,因此對格式字符串進行編碼的唯一方法是 VM 命令的直接參數:

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

因此,調試輸出會大大增加生成的代碼。

發送XDP數據包

讓我們更改過濾器:讓它將所有傳入數據包發回。 從網絡的角度來看,這是不正確的,因為有必要更改標頭中的地址,但現在原則上的工作很重要。

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

發射 tcpdump 上 xdp-remote。 它應該顯示相同的傳出和傳入 ICMP 回顯請求,並停止顯示 ICMP 回顯回复。 但它沒有顯示。 結果可以工作 XDP_TX 在計劃中 xdp-local 必要配對接口 xdp-remote 一個程序也被分配了,即使它是空的,並且它被提出了。

我怎麼知道的?

跟踪內核中包的路徑 順便說一句,perf events機制允許使用相同的虛擬機,即eBPF用於與eBPF進行反彙編。

你必須化惡為善,因為除此之外別無他法。

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

代碼6是什麼?

$ errno 6
ENXIO 6 No such device or address

功能 veth_xdp_flush_bq() 從中獲取錯誤代碼 veth_xdp_xmit(),其中搜索 ENXIO 並找到評論。

恢復最小過濾器(XDP_PASS)在文件中 xdp_dummy.c,將其添加到Makefile中,綁定到 xdp-remote:

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

現在 tcpdump 顯示預期內容:

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

如果只顯示 ARP,則需要刪除過濾器(這使得 sudo ./stand detach), 讓 ping,然後安裝過濾器並重試。 問題是過濾器 XDP_TX 也會影響 ARP,如果堆棧
命名空間 xdp-test 如果“忘記”了 MAC 地址 192.0.2.1,他將無法解析該 IP。

制定問題

讓我們繼續完成既定的任務:在 XDP 上編寫 SYN cookie 機制。

到目前為止,SYN Flood 仍然是一種流行的 DDoS 攻擊,其本質如下。 當建立連接(TCP 握手)時,服務器接收 SYN,為將來的連接分配資源,用 SYNACK 數據包進行響應,並等待 ACK。 攻擊者只需從數千個殭屍網絡中的每台主機每秒從假地址發送數千個 SYN 數據包。 服務器在數據包到達時被迫立即分配資源,但在長時間超時後釋放資源,導致內存或限制耗盡,不接受新連接,服務不可用。

如果不對SYN報文分配資源,而只響應SYNACK報文,那麼服務器怎麼能知道後面到來的ACK報文屬於沒有保存的SYN報文呢? 畢竟,攻擊者也可以生成假 ACK。 SYN cookie的本質是編碼 seqnum 連接參數為地址、端口和變化鹽的哈希值。 如果 ACK 在鹽變化之前成功到達,您可以再次計算哈希並與 acknum。 偽造的 acknum 攻擊者不能,因為鹽包含秘密,並且由於通道有限而沒有時間對其進行排序。

SYN cookie 已經在 Linux 內核中實現了很長一段時間,如果 SYN 到達太快且大量,甚至可以自動啟用。

TCP 握手教育計劃

TCP 以字節流的形式提供數據傳輸,例如,HTTP 請求通過 TCP 傳輸。 流以數據包的形式逐段傳輸。 所有 TCP 數據包都有邏輯標誌和 32 位序列號:

  • 標誌的組合定義了特定包的角色。 SYN 標誌意味著這是發送方在連接上的第一個數據包。 ACK 標誌意味著發送方已收到最多一個字節的所有連接數據。 acknum。 一個數據包可能有多個標誌,並根據它們的組合來命名,例如 SYNACK 數據包。

  • 序列號 (seqnum) 指定此數據包中發送的第一個字節在數據流中的偏移量。 例如,如果在第一個包含 X 字節數據的數據包中,該數字為 N,則在下一個包含新數據的數據包中,該數字將為 N+X。 在通話開始時,雙方隨機選擇該號碼。

  • 確認號(acknum) - 與 seqnum 相同的偏移量,但它並不確定傳輸字節的編號,而是確定來自接收方的第一個字節的編號,發送方沒有看到該編號。

連接開始時,雙方必須同意 seqnum и acknum。 客戶端發送一個 SYN 數據包,其中包含 seqnum = X。 服務器用 SYNACK 數據包進行響應,並在其中寫入自己的數據包 seqnum = Y 並暴露 acknum = X + 1。 客戶端用 ACK 數據包響應 SYNACK,其中 seqnum = X + 1, acknum = Y + 1。 之後,實際的數據傳輸開始。

如果對話者未確認收到數據包,TCP 將在超時後重新發送數據包。

為什麼不總是使用 SYN cookie?

首先,如果 SYNACK 或 ACK 丟失,您將必須等待重新發送 - 連接建立速度會減慢。 其次,在 SYN 數據包中 - 並且僅在其中! - 傳輸的許多選項會影響連接的進一步操作。 服務器不記得傳入的 SYN 數據包,因此會忽略這些選項,在接下來的數據包中,客戶端將不再發送它們。 TCP 在這種情況下可以工作,但至少在初始階段,連接質量會下降。

就軟件包而言,XDP 程序應該執行以下操作:

  • 使用帶有 cookie 的 SYNACK 響應 SYN;
  • 用 RST 應答 ACK(斷開連接);
  • 丟棄其他數據包。

算法的偽代碼以及數據包解析:

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

(*) 需要管理系統狀態的點已被標記 - 在第一階段,您可以通過簡單地實現 TCP 握手並生成 SYN cookie 作為序列號而無需它們。

現場 (**),雖然我們沒有桌子,但我們會跳過數據包。

TCP握手實現

包解析和代碼驗證

我們需要網絡頭結構:以太網(uapi/linux/if_ether.h)、IPv4(uapi/linux/ip.h) 和 TCP (uapi/linux/tcp.h)。 由於與以下相關的錯誤,我無法連接最後一個 atomic64_t,我必須將必要的定義復製到代碼中。

所有在 C 中為了可讀性而區分的函數都必須在調用位置內聯,因為內核中的 eBPF 驗證器禁止向後跳轉,即實際上禁止循環和函數調用。

#define INTERNAL static __attribute__((always_inline))

LOG() 在發布版本中禁用打印。

該程序是一個函數管道。 每個接收到一個數據包,其中相應級別的標頭被突出顯示,例如, process_ether() 等待被填滿 ether。 根據現場分析的結果,該功能可以將數據包傳輸到更高的級別。 該函數的結果是 XDP 操作。 而 SYN 和 ACK 處理程序則讓所有數據包通過。

struct Packet {
    struct xdp_md* ctx;

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

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

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

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

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

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

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

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

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

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

我注意標記A和B的檢查。如果註釋掉A,程序將構建,但加載時會出現驗證錯誤:

Verifier analysis:

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

Error fetching program/map!

關鍵字符串 invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0):當緩衝區開頭的第 12 個字節位於數據包外部時,存在執行路徑。 從列表中很難看出我們正在談論哪一行,但有一個指令編號 (XNUMX) 和一個顯示源代碼行的反彙編程序:

llvm-objdump -S xdp_filter.o | less

在這種情況下,它指向該行

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

這清楚地表明問題是 ether。 永遠都是這樣。

回复 SYN

此階段的目標是生成具有固定值的正確 SYNACK 數據包 seqnum,將來會被 SYN cookie 取代。 所有的改變都發生在 process_tcp_syn() 和周圍環境。

檢查包裹

奇怪的是,這是最引人注目的一行,或者更確切地說,是對其的評論:

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

在編寫第一個版本的代碼時,使用的是5.1內核,對於其驗證器來說,兩者之間存在差異 data_end и (const void*)ctx->data_end。 在撰寫本文時,5.3.1 內核沒有這個問題。 也許編譯器訪問局部變量的方式與訪問字段的方式不同。 寓意 - 對於大型嵌套,簡化代碼會有所幫助。

此外,為了驗證者的榮耀,對長度進行例行檢查; 氧 MAX_CSUM_BYTES 下方。

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

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

套餐傳播

我們填 seqnum и acknum,設置 ACK(SYN 已設置):

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

交換 TCP 端口、IP 和 MAC 地址。 標準庫無法從 XDP 程序中獲得,因此 memcpy() — 隱藏 Clang intrinsik 的宏。

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

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

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

校驗和重新計算

IPv4和TCP校驗和要求將報頭中的所有16位字相加,並且報頭的大小寫在其中,即在編譯時是未知的。 這是一個問題,因為驗證者不會跳過正常循環直到邊界變量。 但標頭的大小是有限的:每個標頭最多 64 字節。 您可以創建一個具有固定迭代次數的循環,該循環可以提前結束。

我注意到有 RFC 1624 如果僅更改數據包的固定字,如何部分重新計算校驗和。 然而,該方法並不通用,並且實施起來會更難以維護。

校驗和計算函數:

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

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

雖然 size 由調用代碼檢查,第二個退出條件是必要的,以便驗證者可以證明循環的結束。

對於 32 位字,實現了一個更簡單的版本:

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

實際上重新計算校驗和並將數據包發回:

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

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

return XDP_TX;

功能 carry() 根據 RFC 32,從 16 位字的 791 位和中得出校驗和。

TCP握手檢查

過濾器正確建立與 netcat,跳過最後的 ACK,Linux 使用 RST 數據包進行響應,因為網絡堆棧沒有收到 SYN - 它被轉換為 SYNACK 並發回 - 從操作系統的角度來看,到達的數據包不是與開放連接相關。

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

檢查成熟的應用程序並觀察非常重要 tcpdump 上 xdp-remote 因為,例如, hping3 不響應錯誤的校驗和。

從 XDP 的角度來看,檢查本身是微不足道的。 計算算法很原始,可能容易受到老練的攻擊者的攻擊。 例如,Linux 內核使用加密的 SipHash,但其 XDP 的實現顯然超出了本文的範圍。

出現與外部交互相關的新 TODO:

  • XDP程序無法存儲 cookie_seed (鹽的秘密部分)在全局變量中,您需要一個內核存儲,其值將從可靠的生成器定期更新。

  • 如果 ACK 數據包中的 SYN cookie 匹配,則無需打印消息,而是記住已驗證客戶端的 IP,以便進一步跳過其中的數據包。

由合法客戶驗證:

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

日誌記錄了檢查的通過(flags=0x2 是同步 flags=0x10 是 ACK):

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

只要沒有經過驗證的 IP 列表,就不會針對 SYN 洪水本身提供保護,但以下是對此命令發起的 ACK 洪水的反應:

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

日誌條目:

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

結論

有時,eBPF(尤其是 XDP)更多地被視為高級管理員工具,而不是開發平台。 事實上,XDP 是一種干擾內核數據包處理的工具,而不是像 DPDK 和其他內核旁路選項那樣替代內核堆棧。 另一方面,XDP 允許您實現相當複雜的邏輯,而且很容易更新,無需暫停流量處理。 驗證器不會產生大問題,就我個人而言,我不會拒絕部分用戶空間代碼。

在第二部分中,如果主題有趣,我們將完成已驗證客戶端的表並斷開連接,實現計數器並編寫用戶空間實用程序來管理過濾器。

引用:

來源: www.habr.com

添加評論