# 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 相關專案開發的學員,在其開發紀錄提出你的疑惑和建議。 > 在此彙整你的認知和對比你的產出。