LSF(Linux socket filter)起源於BPF(Berkeley Packet Filter),基礎從架構一致,但使用更簡單。LSF內部的BPF最早是cBPF(classic),後來x86平台首先切換到eBPF(extended),但由於很多上層應用程序仍然使用cBPF(tcpdump、iptables),並且eBPF還沒有支持很多平台,所以內核提供了從cBPF向eBPF轉換的邏輯,並且eBPF在設計的時候也是沿用了很多cBPF的指令編碼。但是在指令集合寄存器,還有架構設計上有很大不同(例如eBPF已經可以調用C函數,並且可以跳轉到另外的eBPF程序)。
但是新的eBPF一出來就被玩壞了,人們很快發現了它在內核trace方面的意義,它可以保證絕對安全的獲取內核執行信息。是內核調試和開發者的不二選擇,所以針對這個方面,例如kprobe、ktap、perf eBPF等優秀的工作逐漸產生。反而包過濾部門關注的人不夠多。tc(traffic controll)是使用eBPF的一角優秀的用戶端程序,它允許不用重新編譯模塊就可以動態添加刪除新的流量控制算法。netfilter的xtable模塊,配合xt_bpf模塊,就可以實現將eBPF程序添加到hook點,來實現過濾。當然,內核中提供了從cBPF到eBPF編譯的函數,所以,任何情況下想要使用cBPF都可以,內核會自動檢測和編譯。
其核心原理是對用戶提供了兩種SOCKET選項:SO_ATTACH_FILTER和SO_ATTACH_BPF。允許用戶在某個sokcet上添加一個自定義的filter,只有滿足該filter指定條件的數據包才會上發到用戶空間。因為sokect有很多種,你可以在各個維度的socket添加這種filter,如果添加在raw socket,就可以實現基於全部IP數據包的過濾(tcpdump就是這個原理),如果你想做一個http分析工具,就可以在基於80端口(或其他http監聽端口)的socket添加filter。還有一種使用方式離線式的,使用libpcap抓包存儲在本地,然後可以使用bpf代碼對數據包進行離線分析,這對於實驗新的規則和測試bpf程序非常有幫:SO_ATTACH_FILTER插入的是cBPF代碼,SO_ATTACH_BPF插入的是eBPF代碼。eBPF是對cBPF的增強,目前用戶端的tcpdump等程序還是用的cBPF版本,其加載到內核中後會被內核自動的轉變為eBPF。
echo 2 > /proc/sys/net/core/bpf_jit_enable
通過像這個寫入0/1/2可以實現關閉、打開、調試日志等bpf模式。
在用戶空間使用,最簡單的辦法是使用libpcap的引擎,由於bpf是一種匯編類型的語言,自己寫難度比較高,所以libpcap提供了一些上層封裝可以直接調用。然而libpcap並不能提供所有需求,比如bpf模塊開發者的測試需求,還有高端的自定義bpf腳本的需求。這種情況下就需要自己編寫bpf代碼,然後使用內核tools/net/目錄下的工具進行編譯成bpf匯編代碼,再使用socket接口傳入這些代碼即可。bpf引擎在內核中實現,但是bpf程序的工作地點很多需要額外的模塊來支持,常用的有netfilter自帶的xtable、xt_bpf 可以實現在netfilter的hook點執行bpf程序、cls_bpf和act_bpf可以實現對流量進行分類和丟棄(qos).
內核對bpf的完整支持是從3.9開始的,作為iptables的一部分存在,默認使用的是xt_bpf,用戶端的庫是libxt_bpf。iptables一開始對規則的管理方式是順序的一條條的執行,這種執行方式難免在匹配數目多的時候帶來性能瓶頸,添加了bpf支持後,靈活性大大提升。以上所有提到的可以使用bpf的地方均指同時可使用eBPF和cBPF。因為內核在執行前會自動檢查是否需要轉換編碼。
前面說的bpf程序是用來做包過濾的,那麼bpf代碼只能用來做包過濾嗎?非也。內核的bpf支持是一種基礎架構,只是一種中間代碼的表達方式,是向用戶空間提供一個向內核注入可執行代碼的公共接口。只是目前的大部分應用是使用這個接口來做包過濾。其他的如seccomp BPF可以用來實現限制用戶進程可使用的系統調用,cls_bpf可以用來將流量分類,PTP dissector/classifier(干啥的還不知道)等都是使用內核的eBPF語言架構來實現各自的目的,並不一定是包過濾功能。
工具:tcpdump、tools/net、cloudfare、seccomp BPF、IO visitor、ktap
cBPF中每一條匯編指令都是如下格式:
struct sock_filter { /* Filter block */
__u16 code; /* Actual filter code */
__u8 jt; /* Jump true */
__u8 jf; /* Jump false */
__u32 k; /* Generic multiuse field */
};
一個列子:op:16, jt:8, jf:8, k:32
code是真實的匯編指令,jt是指令結果為true的跳轉,jf是為false的跳轉,k是指令的參數,根據指令不同不同。一個bpf程序編譯後就是一個sock_filter的數組,而可以使用類似匯編的語法進行編程,然後使用內核提供的bpf_asm程序進行編譯。
bpf在內核中實際上是一個虛擬機,有自己定義的虛擬寄存器組。和我們熟悉的java虛擬機的原理一致。這個虛擬機的設計是lsf的成功的所在。cBPF有3種寄存器:
A 32位,所有加載指令的目的地址和所有指令運算結果的存儲地址
X 32位,二元指令計算A中參數的輔助寄存器(例如移位的位數,除法的除數)
M[] 0-15共16個32位寄存器,可以自由使用
我們最常見的用法莫過於從數據包中取某個字的數據內來做判斷。按照bpf的規定,我們可以使用偏移來指定數據包的任何位置,而很多協議很常用並且固定,例如端口和ip地址等,bpf就為我們提供了一些預定義的變量,只要使用這個變量就可以直接取值到對應的數據包位置。例如:
len skb->len
proto skb->protocol
type skb->pkt_type
poff Payload start offset
ifidx skb->dev->ifindex
nla Netlink attribute of type X with offset A
nlan Nested Netlink attribute of type X with offset A
mark skb->mark
queue skb->queue_mapping
hatype skb->dev->type
rxhash skb->hash
cpu raw_smp_processor_id()
vlan_tci skb_vlan_tag_get(skb)
vlan_avail skb_vlan_tag_present(skb)
vlan_tpid skb->vlan_proto
rand prandom_u32()
更可貴的是這個列表還可以由用戶自己去擴展。各種bpf引擎的具體實現還會定義各自的擴展。
由於用戶可以提交cBPF的代碼,首先是將用戶提交來的結構體數組進行編譯成eBPF代碼(提交的是eBPF就不用了)。然後再將eBPF代碼轉變為可直接執行的二進制。cBPF這在很多平台還在使用,這個代碼就和用戶空間使用的那種匯編是一樣的,但是在X86架構,現在在內核態已經都切換到使用eBPF作為中間語言了。也就是說x86在用戶空間使用的匯編和在內核空間使用的並不一樣。但是內核在定義eBPF的時候已經盡量的復用cBPF的編碼,有的指令的編碼和意義,如BPF_LD都是完全一樣的。然而在還不支持eBPF的平台,cBPF則是唯一可以直接執行的代碼,不需要轉換為eBPF。
eBPF對每一個bpf語句的表達與cBPF稍有不同,如下定義:
struct bpf_insn {
__u8 code; /* opcode */
__u8 dst_reg:4; /* dest register */
__u8 src_reg:4; /* source register */
__s16 off; /* signed offset */
__s32 imm; /* signed immediate constant */
};
其寄存器也不同:
* R0 - return value from in-kernel function, and exit value for eBPF program
* R1 - R5 - arguments from eBPF program to in-kernel function
* R6 - R9 - callee saved registers that in-kernel function will preserve
* R10 - read-only frame pointer to access stack
為了配合更強大的功能,eBPF匯編架構使用的寄存器有所增加,上述的寄存器的存在,充分體現了函數調用的概念,而不再是加載處理的原始邏輯。有了函數調用的邏輯設置可以直接調用內核內部的函數(這是一個安全隱患,但是內部有規避機制)。不但如此,由於這種寄存器架構與x86等CPU的真實寄存器架構非常像,實際的實現正是實行了直接的寄存器映射,也就是說這些虛擬的寄存器實際上是使用的同功能的真實的寄存器,這無疑是對效率的極大提高。而且,在64位的計算機上這些計算機將會有64位的寬度,完美的發揮硬件能力。但是目前的64位支持還不太完善,但已經可用。
目前的內核實現,只可以在eBPF程序中調用預先定義好的內核函數,不可以調用其他的eBPF程序(但是可以通過map的支持跳轉到其他eBPF程序,然後再跳回來,後面有介紹)。這看起來無關緊要,但是卻是一個極大的能力,這就意味著你可以使用C語言來實現eBPF程序邏輯,eBPF只需要調用這個C函數就好了。
eBPF不但是程序,還可以訪問外部的數據,重要的是這個外部的數據可以在用戶空間管理。這個k-v格式的map數據體是通過在用戶空間調用bpf系統調用創建、添加、刪除等操作管理的。
用戶可以同時定義多個map,使用fd來訪問某個map。有一個特殊種類的map,叫program arry,這個map存儲的是其他eBPF程序的fd,通過這個map可以實現eBPF之間的跳轉,跳轉走了就不會跳轉回來,最大深度是32,這樣就防止了無限循環的產生(也就是可以使用這個機制實現有限循環)。更重要的是,這個map在運行時可以通過bpf系統調用動態的改變,這就提供了強大的動態編程能力。比如可以實現一個大型過程函數的中間某個過程的改變。實際上一共有3種map:
BPF_MAP_TYPE_HASH, //hash類型
BPF_MAP_TYPE_ARRAY, //數組類型
BPF_MAP_TYPE_PROG_ARRAY, //程序表類型
除了在用戶空間通過nettable和tcpdump來使用bpf,在內核中或者在其他通用的編程中可以直接使用C寫eBPF代碼,但是需要LLVM支持,例子。
在用戶空間通過使用bpf系統調用的BPF_PROG_LOAD方法,就可以發送eBPF的代碼進內核,如此發送的代碼不需要再做轉換,因為其本身就是eBPF格式的。如果要在內核空間模塊使用eBPF,可以直接使用對應的函數接口插入eBPF程序到sk_buff,提供強大的過濾能力。
Linux提供的系統調用bpf,用於操作eBPF相關的內核部分:
#include
int bpf(int cmd, union bpf_attr *attr, unsigned int size);
bpf man page
這個函數的第一個參數cmd就是內核支持的操作種類,包括BPF_MAP_CREATE、BPF_MAP_LOOKUP_ELEM、BPF_MAP_UPDATE_ELEM、BPF_MAP_DELETE_ELEM、BPF_MAP_GET_NEXT_KEY、BPF_PROG_LOAD 6種。然而,從名字上就可以發現,有5種是用來操作map的。這個map前面說過,是用戶程序和內核eBPF程序通信的唯一方式。這5個調用類型都是給用戶空間的程序使用的。最後一個BPF_PROG_LOAD 方法用來向內核中加載eBPF代碼體。
第二個參數attr則是cmd參數的具體參數了,根據cmd的不同而不同,如果load的話還包括了完整的eBPF程序。
值得注意的是,每一個map和eBPF都是一個文件,都有對應的fd,這個fd在用戶空間看來與其他fd無異,可以釋放可以通過unix domain socket在進程間傳遞。如果定義一個raw類型的socket,在其上附上eBPF程序過濾程序,其甚至就可以直接充當iptable的規則使用。
act_bpf
cls_bpf
IO visitor:這可能是基於eBPF相關的最大型的系統了。由多個廠商參與。
xtable、xt_bpf
我們知道eBPF有map數據結構,有程序執行能力。那麼這就是完美的跟蹤框架。比如通過kprobe將一個eBPF程序插入IO代碼,監控IO次數,然後通過map向用戶空間匯報具體的值。用戶端只需要每次使用bpf系統調用查看這個map就可以得到想要統計的內容了。那麼為何要用eBPF,而不是直接使用kprobe的c代碼本身呢?這就是eBPF的安全性,其機制設計使其永遠不會crash掉內核,不會與正常的內核邏輯發生交叉影響。可以說,通過工具選擇避免了可能發生的很多問題。更可貴的是eBPF是原生的支持tracepoint,這就為kprobe不穩定的情況提供了可用性。
Brendan Gregg’s Blog 描述了一個使用eBPF進行kprobe測試的例子。
ktap創造性的使用eBPF機制實現了內核模塊的腳本化,使用ktap,你可以直接使用腳本編程,無需要編譯內核模塊,就可以實現內核代碼的追蹤和插入。這背後就是eBPF和內核的tracing子系統。
bpf subcommand to perf:華為也在為bpf添加perf腳本的支持能力。
可以看出來,eBPF起源於包過濾,但是目前在trace市場得到越來越廣泛的應用。
也就是說目前使用傳統的bpf語法和寄存器在用戶空間寫bpf代碼,代碼在內核中會被編譯成eBPF代碼,然後編譯為二進制執行。傳統的bpf語法和寄存器簡單,更面向業務,類似於高層次的編程語言,而內核的eBPF語法和寄存器復雜,類似於真實的匯編代碼。
那麼為何內核要大費周章的實現如此一個引擎呢?因為輕量級、安全性和可移植性。由於是中間代碼,可移植性不必說,但是使用內核模塊調用內核的函數接口一般也是可移植的,所以這個並不是很重要的理由。eBPF代碼在執行的過程中被嚴格的限制了禁止循環和安全審查,使得eBPF被嚴格的定位於提供過程式的執行語句塊,甚至連函數都算不上,最大不超過4096個指令。所以這就是其定位:輕量級、安全、不循環。
上面說了幾個bpf的用途,但遠不至於此。
http://www.tcpdump.org/papers/bpf-usenix93.pdf
http://lwn.net/Articles/498231/
https://www.kernel.org/doc/Documentation/networking/filter.txt