# Linux 核心專題: 透過 Netfilter 自動過濾廣告
> 執行人: aa860630, steven523
> [專題解說影片(aa860630)](https://youtu.be/1cDO52NlVTw?si=qy-F8ls-h57DJqGL)
> [專題解說影片(steven523)](https://youtu.be/4-ZzcvyabF4?si=-Nx4ETdB0Q_-JhzE)
### Reviewed by `ollieni`
為何只擷取udp 通訊協定與tcp通訊協定的封包就可以屏蔽廣告?
> [name=steven523]
> 對於屏蔽廣告,主要涉及到的協定的確是 UDP 和 TCP 通訊協定的封包,不過這並不是絕對的,因為還有其他層次和協議可能會參與到廣告的傳輸和顯示中,但通常在常規廣告過濾中不太常見:
> * **QUIC (Quick UDP Internet Connections) 協定**,一種基於 UDP 的傳輸層協定,旨在提高 HTTP/3 的效能,它透過 UDP 來取代 TCP 以在 TLS 上串流各種協定。
> 攔截和分析 UDP port 53 上的 DNS Query,過濾包含廣告域名的查詢。
>
> * **WebSocket 協定**,一種在單一 TCP 連接上進行全雙工通訊的協定,有時也用於傳輸廣告內容。
> 攔截和分析 TCP 上的 WebSocket 流量,過濾包含廣告內容的請求。
### Reviewed by `gawei1206`
對於有些網址即使我把他加入到了規則中,但卻無法阻止與他的連線,像是文中提到 Youtube 那段,原因就只有沒有把所有 IP 地址加入規則這一種可能嗎,還是有其他可能的原因?
>[name=aa860630]
>內容傳遞網路 (CDN) 是一組地理上分散的伺服器,用於在靠近終端使用者的位置快取內容,以達到更好的閱讀質量
>
>即使將某些網址的 IP 地址加入 iptables 的規則中,仍無法阻止與該網址的連線,原因可能是因為你使用代理伺服器或 VPN,上述方法可以繞過本地的 iptables 規則,通過其他 IP 地址訪問目標網站,導致封鎖無效
### Reviewed by `jujuegg`
> * `NF_DROP`:將封包丟棄
> * `NF_STOLEN`:由 hook function 處理該封包,不再繼續傳送
Netfilter 可以藉由在 hook function 中回傳不同的操作來告訴防火牆我們要執行的動作,因為 `NF_DROP` 和 `NF_STOLEN` 都不會將封包繼續往下傳,請問這兩個操作還有特別不一樣的地方嗎?
>[name=steven523]
>有的,`NF_DROP` 是指不再處理也不會繼續傳送封包,並將 `sk_buff` 所佔的資源丟棄,大多情況都是透過此操作來過濾不安全或不需要的封包。
>
>而 `NF_STOLEN` 的部分雖然也不會將封包繼續傳送給後續的 Netfilter hook,但是會將此封包給 hook function 進一步接管,這同時也意味著需要自己管理封包佔用的資源,因為核心不會釋放掉 `sk_buff` 資源,使用完 `sk_buff` 後需要再呼叫 `kfree_skb()`將其釋放。
>使用到 `NF_STOLEN` 的情況比較少,像是對封包進行分析或修改,然後再重新傳入 network stack,或是將封包交給其他核心子系統或使用者空間處理。
>
>[參考資料](https://stackoverflow.com/questions/19342950/what-is-the-difference-between-nf-drop-and-nf-stolen-in-netfilter-hooks)
## 任務簡介
在 Linux v6.8 重現[透過 Netfilter 自動過濾廣告](https://hackmd.io/@sysprog/BJb0NRYH3)實驗,並修正對應的程式碼。
## TODO: 探討 netfilter 原理並闡述過濾廣告的策略
> 可重用去年報告的素材,但要更新到 Linux v6.8+
[Netfilter](https://www.netfilter.org/index.html) 是 Linux 2.4 引入的一個子系統,它提供了一個用於實現高級網路功能的框架,例如封包過濾、網路位址轉換(NAT) 和連接跟蹤。它利用核心網路程式碼中的 Hook 來實現,核心程式碼可以為特定的網路事件去註冊呼叫函式的位置,例如當收到封包時,就會觸發事件的處理程式並執行模組指定的操作。
[Iptables](https://www.netfilter.org/projects/iptables/index.html) 允許系統管理員配置 Linux 核心防火牆的 IP 封包過濾規則。可透過 Iptables 工具使我們方便在 **userspace** 對 Netfilter 進行操作。
此外 Netfilter 框架提供了一種強大的機制,用於阻擋和操作 Linux 核心中的網路封包。該框架有 2 種元件 - **Netfilter hooks** 和 **Conntrack**。
[Conntrack](https://www.netfilter.org/projects/conntrack-tools/) 所做的事情就是**發現並追蹤這些連線的狀態**,具體包括:
1. 從資料包中提取元組(tuple) 訊息,辨別資料流(flow) 和對應的連接(connection)
2. 為所有連線維護一個狀態資料庫(conntrack table),例如連線的建立時間、傳送包數、傳送位元組數等等
3. 回收過期的連接(GC)
4. 為更上層的功能(例如NAT) 提供服務
:::info
基本上 conntrack 可以用來清理掉封包的殘留連接狀態。例如在 TCP 協定中,如果有一個封包通過 iptables 的 `DROP` 目標被丟棄,但這個封包是 TCP 在 [three way handshake](https://reurl.cc/OM3a33) 過程中的一部分(如 SYN 封包),則 Conntrack 可能會記錄這個封包的狀態為 `SYN_RECV`,即已接收到 SYN 封包但尚未進行確認。在這種情況下,即使這個 SYN 封包被丟棄了,Conntrack 也可能會記錄相關的連接狀態。
==不過在我們的專案中,這樣的情況在阻擋網路廣告時並不常見,所以基本上不太會用到這個指令。==
:::
[Netfilter hooks](https://www.netfilter.org/documentation/HOWTO/netfilter-hacking-HOWTO-3.html) 是在核心中註冊的函式,要在 [network stack](https://en.wikipedia.org/wiki/Protocol_stack) 中的特定點呼叫。這些 hooks 可以看作是在 stack 不同層中的檢查點。
每個 hook 點對應於封包處理的不同階段,如下圖所示:
![image](https://hackmd.io/_uploads/rJ21x5GwA.png)
以下是五個主要的 hook 點,以及它們對應 chain 的標識:
* `NF_IP_PRE_ROUTING` ( `PREROUTING` ): 在做任何路由決策之前觸發,當作檢查和修改封包的初始點,適用於路由前的目標 NAT 和封包處理
* `NF_IP_LOCAL_IN` ( `INPUT` ): 針對發往本地系統的封包, 允許檢查和修改那些要交給本機的入站封包
* `NF_IP_FORWARD` ( `FORWARD` ): 用於在系統中從一個網路介面轉發到另一個網路介面的封包,對應用轉發規則和過濾不屬於本地系統的封包非常重要
* `NF_IP_LOCAL_OUT` ( `OUTPUT` ): 在所有路由決策做出之後但在封包實際傳送之前觸發,通常用於源 NAT 和在封包傳送前的附加封包處理
* `NF_IP_POST_ROUTING` ( `POSTROUTING` ): 針對本地系統生成的封包,在它們實際發出之前,提供修改出站封包、設置源 NAT 或應用其他出站過濾規則的機會
假設伺服器知道如何路由封包,防火牆允許封包傳輸,以下就是不同場景下封包的傳輸流程:
* 收到、目的地是本機的封包:`PREROUTING` -> `INPUT`
* 收到、目的地是其他主機要再轉發出去的封包:`PREROUTING` -> `FORWARD` -> `POSTROUTING`
* 本地產生的封包:`OUTPUT` -> `POSTROUTING`
當封包到達或離開網路介面時,它會按順序通過每個 hook 點。而在每個 hook 點,核心都會為該 hook 點呼叫所有已註冊的 netfilter hook 函式。每個 netfilter hook 函式都可以檢查封包並決定如何處理它,可能的操作包括:
* `NF_ACCEPT`:繼續正常的封包處理
* `NF_DROP`:將封包丟棄
* `NF_STOLEN`:由 hook function 處理該封包,不再繼續傳送
* `NF_QUEUE`: 將封包送入 NFQUEUE,通常交由 userspace 處理(如 iptables 或 nftables)
* `NF_REPEAT`:再次呼叫該 hook funciton
網路封包過濾是一種用於控制網路流量的方法,它根據指定的規則來允許或拒絕封包進入或離開網路。這些規則通常基於封包的 source IP/Port、destination IP/Port 和 協定類型。
常見的廣告一般分成 HTTPS 和 UDP 兩種封包,以下是透過掛載核心模組來過濾廣告的策略:
* HTTPS 流量使用 TLS 加密,因此需要解密才能查看其內容
* 透過 [Man-in-the-middle](https://en.wikipedia.org/wiki/Man-in-the-middle_attack) 冒充實際的 client server 與網路通訊兩端分別建立連接,並以 local proxy 服務攔截流量
* 或是利用 eBPF 搭配 XDP 工具執行自定義的程式碼來過濾和處理封包,可以實現在 user space 獲取加密前的封包
* 當獲得 HTTPS 封包內容時提取域名,並與過濾名單比對
* 如果匹配,則丟棄該封包,阻止該廣告的載入
</br>
* 通過監控和攔截 DNS Query,可以辨識目標域名並加以過濾
* 攔截 DNS Query ( UDP 封包,port 53 )
* 提取 DNS Query 中的域名,並與過濾名單比對
* 如果匹配,則丟棄該封包,阻止該廣告的載入
除了掛載核心模組之外,我們還能透過實作以下手動更改 iptables 的方式來過濾廣告:
實驗前清除所有規則
```
$ sudo iptables -F
```
使用命令查找目標網頁的資訊
```shell
$ nslookup youtube.com
Server: 127.0.0.53
Address: 127.0.0.53#53
Non-authoritative answer:
Name: youtube.com
Address: 142.251.42.238
Name: youtube.com
Address: 2404:6800:4012:2::200e
```
將 `youtube.com` 加入到 iptables 的 reject 名單
```
$ sudo iptables -A OUTPUT -d 142.250.0.0/15 -j REJECT
```
使用以下指令查看 iptables 內容
```shell
$ sudo iptables -L
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain FORWARD (policy ACCEPT)
target prot opt source destination
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
REJECT all -- anywhere cg-in-f0.1e100.net/15 reject-with icmp-port-unreachable
```
REJECT 中指定了目標是 `cg-in-f0.1e100.net/15`,這可能不是 YouTube 所有的 IP 地址,因此瀏覽器還可以順利打開
拒絕所有可能是 YouTube 的 IP 地址
```shell
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
REJECT all -- anywhere cg-in-f0.1e100.net/15 reject-with icmp-port-unreachable
REJECT all -- anywhere ord38s04-in-f0.1e100.net/16 reject-with icmp-port-unreachable
REJECT all -- anywhere 216-58-0-0.cpe.distributel.net/16 reject-with icmp-port-unreachable
```
測試連接 : 連接至 `youtube.com` 失敗
```shell
$ curl -v http://youtube.com
* Trying 172.217.160.78:80...
* connect to 172.217.160.78 port 80 failed: Connection refused
* Trying 2404:6800:4012::200e:80...
* Immediate connect fail for 2404:6800:4012::200e: Network is unreachable
* Failed to connect to youtube.com port 80 after 12 ms: Connection refused
* Closing connection 0
curl: (7) Failed to connect to youtube.com port 80 after 12 ms: Connection refused
```
不過使用核心模組比起上述的手動更改 iptables,不僅更靈活,同時也可以提高瀏覽器性能,主要差別如下:
* 使用核心模組過濾可以根據域名進行過濾,而不需要依賴固定的 IP 地址
* 核心模組過濾可以在解析階段就阻止廣告域名,從而防止任何針對這些域名的連接嘗試,而 iptables 規則只能在連接建立後起作用
* 在 DNS 層過濾,可以更早地阻止廣告內容的加載,可能提高瀏覽器性能。
## TODO: 嘗試藉由 netfilter 阻擋廣告
> 在 Linux v6.8 重現[透過 Netfilter 自動過濾廣告](https://hackmd.io/@sysprog/BJb0NRYH3)實驗,並修正對應的程式碼。
這個部份我們各自參照了兩個不同的專案來實作
### 參照 [adriver](https://github.com/Jongy/adriver)
使用 Linux 核心模組,即可在核心裡頭註冊不同的 hook 來處理封包,為了在使用者瀏覽網站時屏蔽廣告,因此 hook 必須作用在進入本機之前,也就是 Local In 或 PreRouting。
```c
static struct nf_hook_ops my_ops = {
.hook = my_hook,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_PRE_ROUTING,
.priority = NF_IP_PRI_FIRST,
};
static int mod_init(void)
{
return nf_register_net_hook(&net, &my_ops);
}
static void mod_exit(void)
{
nf_unregister_net_hook(&net, &my_ops);
}
```
**而 hook 函式需接受三個參數**
* `priv`: 為一個私有指針,通常用於傳遞 hook 函式自身需要的私有資料
* `skb`: 是一個指向 Linux 中網路封包(socket buffer)的指針。在 netfilter 中,封包通過系統中的不同階段(如 INPUT、OUTPUT 等)時,可以被 hook 函式捕獲和處理。skb 對象包含了網路封包的所有信息,包括協定頭、有效資料等。
* `state`: 是一個指向 `nf_hook_state` 構體的指針,這個結構體提供了有關 hook 點和系統狀態的詳細信息
```c
static unsigned int my_hook(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state)
{
...
```
`my_hook` 函式主要由 `should_run_get_sfilter()` 與 `should_run_dns_sfilter()` 組成,兩個 funtion 的功用在於檢查並提取傳入的封包是否為 DNS 封包或 HTTP封包,並將其資料存儲到 `buf` 結構體中
`run_get_sfilters(&buf)` 與 `run_dns_sfilters(&buf)` 則將 `buf` 與預先設置好的 DNS 過濾規則或 HTTP 過濾規則做比對,若比對成功說明其封包包含與廣告相關的內容,因此在函式中將 `ret` 設置為 `NF_DROP`,即丟棄該封包
```c
should_run_dns_sfilter(skb, &buf);
if (buf.data)
{
if (run_dns_sfilters(&buf))
{
printk(KERN_INFO "dns filter matched, dropping");
// just drop here, don't REJECT
ret = NF_DROP;
goto done;
}
ret = NF_ACCEPT;
goto done;
}
// buf.data is NULL, no need to free
should_run_get_sfilter(skb, &buf);
if (buf.data)
{
if (run_get_sfilters(&buf))
{
printk(KERN_INFO "get filter matched, dropping");
ret = NF_DROP;
goto done;
}
}
done:
```
**從 UDP 通訊協定中獲取 DNS 資訊**
檢查傳入的網路封包 (`skb`) 是否為 DNS 封包需經過一連串條件審核,步驟如下:
1. `skb` 是否不為空
1. `skb` 是否包含 `ip_header`
1. `ip_header` 裡的協定是否為 UDP 通訊協定
1. `udp_header` 是否不為空
1. 確認埠號為 53
```c
static void should_run_dns_sfilter(struct sk_buff *skb, struct buf *buf)
{
struct iphdr *ip_header = NULL;
struct udphdr *udp_header = NULL;
if(!skb || !(ip_header = ip_hdr(skb)))
{
printk(KERN_INFO "Not IP");
return;
}
if(IPPROTO_UDP != ip_header->protocol)
{
printk(KERN_INFO "Not UDP");
return;
}
udp_header = udp_hdr(skb);
if(!udp_header)
{
printk(KERN_INFO "Failed to get UDP header");
return;
}
if(ntohs(udp_header->dest) == 53)
{
dns_data = (unsigned char *)((unsigned char *)udp_header + sizeof(struct udphdr));
dns_data_len = ntohs(udp_header->len) - sizeof(struct udphdr);
}
...
}
```
埠號 53 是用於 DNS(域名系統)服務的預設埠,且許多廣告和跟蹤服務使用 DNS 請求來進行域名解析,過濾埠號 53 上的數據包可以有效地阻止這些廣告和跟蹤請求
在確認某網路封包就是目標封包後,提取其 DNS 資訊,及下圖中的 Data。
![image](https://hackmd.io/_uploads/HJBJC5IUR.png)
`udp_header` 指標位置 + `udphdr` 長度,即為 Data 位置
```c
dns_data = (unsigned char *)((unsigned char *)udp_header + sizeof(struct udphdr));
dns_data_len = ntohs(udp_header->len) - sizeof(struct udphdr);
```
`dns_data` ,即 DNS Message Packet,用 16 進位法表示如下:
`de b2 01 20 00 01 00 00 00 00 00 01 09 67 6f 6f 67 6c 65 61 64 73 01 67 0b 64 6f 75 62 6c 65 63 6c 69 63 6b 03 6e 65 74 0b 64 6c 69 6e 6b 72 6f 75 74 65 72 00 00 1c 00 01 00 00 29 04 b0 00 00`
前 12 個 bytes 為 DNS Header,分別代表:
* ID(2bytes): 識別碼
* Flag(2bytes): 用來描述 DNS 訊息封包的功能
* QR: 0 表示此訊息是查詢,1 為回應訊息封包
* OP-code: 表示封包的類型。0 是標準查詢,1 是反向查詢,2 是伺服器狀態查詢,3-15 保留未用
* AA: 1 表示回應中的資料是授權資料,來自管轄的名稱伺服器
* TC: 1 表示資料過長被截短,因為超過了 UDP 封包的限制(512 Bytes)
* RD: 1 表示遞迴查詢,0 表示反覆查詢。通常由查詢封包設定,回應封包會反映同樣的設定
* RA: 由回應的伺服器設定,1 表示支援遞迴
* R-code: 在回應封包中表示查詢結果。0 沒有錯誤,1 封包格式錯誤,2 伺服器錯誤,3 名稱錯誤,4 不支援此查詢類型,5 拒絕查詢
* Question Count(2bytes): 表示後面緊接著問題區段的數量
* Answer RR Count(2bytes): 表示後面緊接著答案區段中資源紀錄(Resource Record, RR)的數量
* Authority RR Count(2bytes): 權威區段的紀錄數量
* Addition RR Count(2bytes):此欄位為增加權威區段中紀錄的數量
![image](https://hackmd.io/_uploads/rkC9aNOLC.png =60%x)
Question Name 欄位存放所欲查詢的 FQDN 名稱,每一名稱長度不定,因此,此欄位的長度也不定。網域名稱的存放是以 ASCII 字元表示,最長限制在 64 個字元之內,名稱中的『.』(dot),並不表示出來,而以字元計數取代。譬如:
`09 67 6f 6f 67 6c 65 61 64 73 01 67 0b 64 6f 75 62 6c 65 63 6c 69 63 6b 03 6e 65 74 0b 64 6c 69 6e 6b 72 6f 75 74 65 72`
* Length: 9, Label: googleads
* Length: 1, Label: g
* Length: 11, Label: doubleclick
* Length: 3, Label: net
* Length: 11, Label: dlinkrouter
* Null terminator: 00
Fully Qualified Domain Name: `googleads.g.doubleclick.net.dlinkrouter`
參考資料: [DNS 訊息格式](https://www.tsnien.idv.tw/Internet_WebBook/chap13/13-6%20DNS%20%E8%A8%8A%E6%81%AF%E6%A0%BC%E5%BC%8F.html)
封包在執行完 `should_run_dns_sfilter` 之後,若包含 DNS 資訊的話會將其存入 `buf->data` 裡,用來與 `dns_sfilters[]` 裡的字串做比對,若比對成功則丟棄封包,`dns_sfilters[]` 包含許多過濾規則,內容如下:
```c
const struct sfilter dns_sfilters[] = {
{3, {"media","admob","com" } },
{3, {"tpc","googlesyndication","com" } },
{3, {"pagead2","googlesyndication","com" } },
{3, {"xads","zedo","com" } },
{3, {"xads","zedo","com" } },
{3, {"ad","doubleclick","net" } },
{4, {"cm","g","doubleclick","net" } }
}
```
使用 `sudo dmesg` 查看核心訊息
```shell
[ 702.524716] buf data: googleadsgdoubleclicknet
[ 702.524718] DNS filter matched
[ 703.106169] buf data: lexicon33acrosscom
[ 703.106172] DNS filter matched
[ 702.973604] buf data: securepubadsgdoubleclicknet
[ 702.973606] DNS filter matched
```
**從 TCP 通訊協定中獲取 HTTP 資訊**
檢查傳入的網路封包(`skb`)是否為 DNS 封包需經過一連串條件審核,步驟如下:
1. `skb`是否不為空
1. `skb`是否包含 `ip_header`
1. `ip_header` 裡的協定是否為 TCP 通訊協定
1. `tcp_header` 是否不為空
```c
static void should_run_get_sfilter(struct sk_buff *skb, struct buf *buf)
{
struct iphdr *iph = (struct iphdr *)skb_network_header(skb);
struct tcphdr *tcp_header;
if(!skb){
return;
}
if (!iph || iph->protocol != IPPROTO_TCP) {
printk(KERN_INFO "Not a TCP packet\n");
return;
}
tcp_header = (struct tcphdr *) skb_transport_header(skb);
if (!tcp_header) {
printk(KERN_INFO "Failed to get TCP header\n");
return;
}
...
}
```
我在解析 TCP 通訊協定的資料時,獲取 payload 的方式與 UDP 一樣,但印出來的都是以 `16 03 03` 或 `17 03 03` 為開頭且無法被閱讀的資料。我嘗試理解格式訊息發現資料使用 TLS 進行加密,因此無法被解析
> 16 03 03
* `16` TLS Handshake protocol
* `03 03` SSL 3.3 (TLS 1.2)
* `00 7a` Length of handshake message (122 bytes)
> 17 03 03
* `17` TLS Application Data
* `03 03` version 1.2
* `00 45` length of encrypted data (69 bytes)
得到 `tcp_header` 後,嘗試讀取其 Source port 及 Destination port
```shell
[18156.615918] Source port: 443
[18156.615922] Destination port: 43550
[18156.615926] payload = 17 03 03 00 6a 85 d5 73 14 a2 23 72 ae be 80 82 ef 62 ae e9 27 f8 8b 03 38 b3 0e a5 a8 2b 59 e9 1c 74 6b d8 c7 a0 a8 0b 75 f2 6c 9b b3 fe 75 5f 57 ef 87 9a 19 cb 46 19 4d 73 ae 77 e8 75 8c 0d
```
埠號為 443 說明這個 TCP 連接是用於 HTTPS 通訊,是 HTTP 協定的安全版本,通過 TLS/SSL 進行加密,確保資料在傳輸過程中受到保護,這也解釋上述 payload 無法被解析的原因
---
### 參照 [ItisCaleb](https://github.com/ItisCaleb) 於去年的實驗
本 Linux 核心模組,用於阻擋和過濾特定網絡流量。主要功能包括:
1. 從 sk_buff 結構中提取 TCP 和 UDP 封包
2. 解析封包,提取域名
3. 檢查域名是否在儲存列表(hosts) 中,如果在則丟棄此封包
`hosts` 列表內容包含我瀏覽各網頁時蒐集的廣告域名,以及此[網站](https://pgl.yoyo.org/as/iplist.php?ipformat=iptables&showintro=1&mimetype=plaintext)提供用於 iptables 的命令列表,域名位於每列命令的尾端。
#### `struct sk_buff`
[`sk_buff`](https://docs.kernel.org/networking/skbuff.html)(socket buffer) 結構在 Linux 核心中用於管理網路封包,它本身是一個 metadata structure,不包含任何封包資料,所以真正的封包資料儲存在它所指向的緩衝區內。
下圖的各指標是 `sturct sk_buff` 在封包緩衝區內的不同位置和緩衝區的佈局:
![image](https://hackmd.io/_uploads/HyR_wh9I0.png)
* `head`:指向已分配記憶體的開始位置
* `data`:指向實際封包資料的開始位置
* `tail`:指向封包資料的結束位置
* `end`:指向已分配記憶體的結束位置
* `headroom`:資料開始之前的空間
* `data`:實際的封包資料
* `tailroom`:資料結束之後的空間
* `skb_shared_info`:儲存關於緩衝區的共享信息,比如分頁碎片(page frags) 和分段列表(frag_list)
#### 於核心模組初始化時註冊 hook
```c
static struct nf_hook_ops filter_ops = {
.hook = filter_hook,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_PRE_ROUTING
};
static int mod_init(void) {
return nf_register_net_hook(&init_net, &filter_ops);
}
static void mod_exit(void) {
nf_unregister_net_hook(&init_net, &filter_ops);
}
module_init(mod_init);
module_exit(mod_exit);
```
註冊 hook 時,通過 `struct nf_hook_ops` 的 `.hook` function point 來指定實際的處理函式 `filter_hook`,該函式在被呼叫時返回 `NF_DROP` 或 `NF_ACCEPT` 來決定是否阻擋該封包。
`struct nf_hook_ops` 的參數:
* `hook` : 指向 netfilter hook function 的指標
* `pf` : 要阻擋的封包協定類別
* `hooknum` : 指定要呼叫函式的 hook point
>`NFPROTO_IPV4` 和 `NF_INET_PRE_ROUTING` 皆定義在 [linux/netfilter.h](https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/netfilter.h#L61)
#### 提取 TCP 資料
```c
static int extract_tcp_data(struct sk_buff *skb, char **data)
{
struct iphdr *ip = NULL;
struct tcphdr *tcp = NULL;
size_t data_off, data_len;
if (!skb || !(ip = ip_hdr(skb)) || IPPROTO_TCP != ip->protocol)
return -1; // not ip - tcp
if (!(tcp = tcp_hdr(skb)))
return -1; // bad tcp
/* data length = total length - ip header length - tcp header length */
data_off = ip->ihl * 4 + tcp->doff * 4;
data_len = skb->len - data_off;
if (data_len == 0){
return -1;
}
if (skb_linearize(skb))
return -1;
*data = skb->data + data_off;
return data_len;
}
```
這段函式是從 `sk_buff` 結構中提取 TCP 資料,會先透過宣告的 `*ip`和`tcp` 分別指向 `ip_hdr` 和 `tcp_hdr` 提取的 IP header 和 TCP header,接著檢查他們是否有被正確的提取。
計算封包資料的偏移量和長度過後利用 [`skb_linearize`](https://blog.csdn.net/lxm13613538669/article/details/133896301#:~:text=skb_linearize-,skb_linearize,-%E6%98%AFLinux%E5%86%85) 將 paged skb 轉換成線性的,確保資料在記憶體區塊中是連續分佈,接著把資料指標指向實際資料位置後,回傳資料的長度以便 hook function 操作。
> `skb_linearize` 主要作用是將非線性資料轉換為線性資料,以便於核心的處理和傳遞。執行成功時返回 0,失敗實則返回非 0 值。
#### 提取 UDP 資料
```c
static int extract_udp_data(struct sk_buff *skb, char **data)
{
struct iphdr *ip = NULL;
struct udphdr *udp = NULL;
size_t data_off, data_len;
if (!skb || !(ip = ip_hdr(skb)) || IPPROTO_UDP != ip->protocol)
return -1; // not ip - udp
if (!(udp = udp_hdr(skb)))
return -1; // bad udp
/* data length = total length - ip header length - udp header length */
data_off = ip->ihl * 4 + sizeof(struct udphdr);
data_len = skb->len - data_off;
if (skb_linearize(skb))
return -1;
*data = skb->data + data_off;
return data_len;
}
```
這段函式從 `sk_buff` 結構中提取 UDP 資料,基本上作法跟 `extract_tcp_data` 類似,但是在這段函式中將 `tcp->doff * 4` 修改為 `sizeof(struct udphdr)`,這是因為 UDP header 是固定長度的,而 TCP header 可能包含額外的欄位。
#### 透過 hook funtion 判斷是否阻擋該網絡封包
```c
static unsigned int filter_hook(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state)
```
在 `filter_hook` 函式中包含三個參數:
* `*priv` : 指向註冊 hook function 時傳遞私有資料的指標
* `*skb` : 指向作為 `sk_buff` 結構的網路封包指標
* `*state` : 指向 `nf_hook_state` 結構的指標,該結構包含有關 hook point 的資訊,例如網路協定、網路介面和路由資訊
#### 確認 TCP 資料是 TLS handshake 還是 HTTP GET 請求
```c
int len = extract_tcp_data(skb, &data);
if (len != -1) {
tcpflag = 1;
printk("TCP info: %s", data);
if (data[0] == 0x16) {
printk("TLS handshake len: %d", len);
proto = tls_protocol;
} else if (strncmp(data, "GET ", sizeof("GET ") - 1) == 0) {
proto = http_protocol;
printk("HTTP GET request detected.");
}
}
```
透過 `extract_tcp_data` 函式從 `sk_buff` 中提取到 TCP 資料後,檢查資料的類型,並將對應的協定設置到 `proto` 結構中,在後續的處理過程根據 `proto` 結構的內容來處理不同類型的協定資料(TLS 或 HTTP)。
在 **TLS** 協定資料中,第一個 byte 通常等於 `0x16`,表示這是一個 TLS handshake 封包 ; 而 **HTTP GET** 請求的資料通常以 "GET " 開頭。
>HTTP over TLS : HTTPS 經由 HTTP 進行通訊,但利用 **SSL/TLS** 來加密封包。HTTPS 開發的主要目的,是提供對網站伺服器的身分認證,保護交換資料的隱私與完整性。
>TLS handshake 是啟動使用 TLS 之通訊工作階段的過程。過程中通訊雙方交換訊息以相互確認,彼此驗證,確立它們將使用的加密演算法,並生成一致的工作階段金鑰。
#### 檢查 UDP 資料是否是 DNS 請求
```c
len = extract_udp_data(skb, &data);
if (len != -1) {
if (ntohs(udp_hdr(skb)->dest) == 53) {
proto = dns_protocol;
printk("Get UDP %d", len);
}
}
```
透過 [ntohs](https://linux.die.net/man/3/ntohs) 函式將 network byte 順序轉換為 host byte 順序。檢查提取到的 UDP 封包其 destination port 是否等於 53。[port 53](https://www.speedguide.net/port.php?port=53) 是 DNS 的標準 port,因此這個檢查是為了確定該封包是否是 DNS 請求。
#### 解析封包,提取域名並檢查其是否在 hosts 檔案
```c
struct Protocol {
const char *const name;
const uint16_t default_port;
int (*const parse_packet)(const char *, size_t, char **);
const char *const abort_message;
const size_t abort_message_len;
};
```
創建 `struct Protocol` 結構描述封包的網絡協定內容
* `name` : 協定的名稱
* `default_port` : 協定的默認 port,例如 HTTP 通常使用 port 80 、DNS 使用 port 53 、TLS 使用 port 443
* `parse_packet` : 用於解析封包的 function point,從中提取有用的資訊,例如從 HTTP 請求中提取域名
* `abort_message` : 當協定中止時使用的消息
* `abort_message_len` : 中止消息的長度
```c
const struct Protocol *proto = NULL;
...
if (proto) proto->parse_packet(data, len, &host);
if (host) {
printk("Host: %s", host);
flag = in_word_set(host, strlen(host));
if (flag) {
printk("Dropping Packet");
ret = NF_DROP;
if (tcpflag) {
send_server_ack(skb, state);
send_close(skb, proto, state);
send_tcp_reset(skb, state);
}
}
kfree(host);
}
return ret;
```
透過已設置的 `proto` 呼叫其結構內的 `parse_packet` 提取域名。透過 [`in_word_set`](https://www.gnu.org/software/gperf/manual/html_node/Output-Format.html) 函式檢查是否在 hosts 檔案中,若有的話則將 `ret` 設定成 `NF_DROP` 並阻擋該封包。
如果丟棄的是 TCP 封包,則逐一呼叫 `send_server_ack`、`send_close` 和 `send_tcp_reset` 函式來發送 TCP 控制包,以通知對方終止連接。
* `send_server_ack(skb, state)` : 發送一個 ACK 包,確認接收到資料
* `send_close(skb, proto, state)` : 發送一個 FIN/PSH 包,表示將關閉連接,並附加一個自定義的中止消息
* `send_tcp_reset(skb, state)` : 發送一個 RST 包,強制關閉連接
```bash
# generate_hash.sh
#/bin/sh
if ! [ -x "$(command -v gperf)" ];
then
echo "gperf could not be found" >&2
echo "Please download it first" >&2
exit 1
fi
if [ "$#" -ne 1 ] || ! [ -d "$1" ];
then
echo "Must provide output directory" >&2
echo "Usage: $0 <output directory>" >&2
exit 1
fi
echo '%{' > hosts.gperf
echo '%}' >> hosts.gperf
echo '%%' >> hosts.gperf
cat hosts >> hosts.gperf
echo '%%' >> hosts.gperf
gperf -D -L ANSI-C hosts.gperf > "$1/host_table.h"
rm hosts.gperf
```
使用 [GNU gperf](https://www.gnu.org/software/gperf/manual/gperf.html) 來為要阻擋的域名建立 hash table,gperf 是一個用於生成完美 hash function 和 hash table 的工具,適合用來快速搜尋和配對一組鍵值(如域名)。
```c
# host_table.h
const char *in_word_set (register const char *str, register size_t len)
{
static const char * wordlist[] =
{
"onestat.com",
"Adsatt.ABCNews.starwave.com",
"adsend.de",
"admeta.com",
"nedstatbasic.net",
"adtoma.com",
"demandbase.com",
"mmismm.com",
"content.ad",
"aistat.net",
...
}
}
```
在終端機輸入 `make kernel` 先執行上述 `generate_hash.sh` 後會透過 gperf 生成一份 `host_table.h` 檔案,並可以在裡面看到 `in_word_set` 函式存放多個從 hosts 生成的域名列,如果提取出的域名在列表中,則傳回指向該域名的指標,只要從封包提取出來的域名與 hosts 內容匹配,便可以將封包直接拋棄。
### 實驗結果
以下圖片是 [opgg](https://www.op.gg/modes/aram/jinx/build?region=global) 網站,這個網站內有時包含 https 廣告有時則是 udp 廣告,經測試後可看見比對結果,原本分佈在五處的廣告在掛載模組後看不見了。左下的影片是該網站原本就插入的,不算廣告。
![image](https://hackmd.io/_uploads/SJWQMnh80.png)
![image](https://hackmd.io/_uploads/By4172nLR.png)
## TODO: 檢視其他學員在 netfilter 的投入狀況,提出疑惑和建議
> 在[課程期末專題](https://hackmd.io/@sysprog/linux2024-projects)找出同樣從事 netfilter 相關專案開發的學員,在其開發紀錄提出你的疑惑和建議。
> 在此彙整你的認知和對比你的產出。