# 透過 Netfilter 自動過濾廣告 儘管我們可在網頁瀏覽器中透過像是 [AdBlock](https://getadblock.com/zh_TW/) 這類的 extension 來過濾廣告,但需要額外的設定和佔用更多系統資源,倘若我們能透過 [netfilter](https://www.netfilter.org/),直接在核心層級過濾網路廣告,那所有應用程式都有機會受益。 * 參考資訊: * [How to drop 10 million packets per second](https://blog.cloudflare.com/how-to-drop-10-million-packets/) * [Use the iptables firewall to block ads on your Linux machine](https://www.securitronlinux.com/debian-testing/use-the-iptables-firewall-to-block-ads-on-your-linux-machine/) * [nBlock](https://github.com/notracking/nBlock) * [2020 年開發紀錄](https://hackmd.io/@ZhuMon/2020q1_final_project) * 執行人: ItisCaleb → [開發紀錄](https://hackmd.io/@sysprog/BJb0NRYH3) ## TODO * 重現去年 Netfilter 實驗,記錄下來 >TLS ; https ; layer 4 firewall ; OSI 七層 TCP 3-way handshake SYN, sliding window * 閱讀 [深入理解 iptables 與 netfilter 架構](https://arthurchiao.art/blog/deep-dive-into-iptables-and-netfilter-arch-zh/) & [基於 iptables 做 NAT](https://arthurchiao.art/blog/nat-zh/) ## 實作 ### 手動更改 iptable 以過濾廣告 實驗前清除所有規則 ``` $ 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`加入到 iptable 的 reject 名單 ``` $ sudo iptables -A OUTPUT -d 142.250.0.0/15 -j REJECT ``` 使用以下指令查看 iptable 內容 ```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 aa860630@aa860630-ASUS-TUF-Dash-F15-FX516PM-FX516PM:~$ 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 ``` ### 使用核心模組以過濾廣告 根據 [Use the iptables firewall to block ads on your Linux machine](https://www.securitronlinux.com/debian-testing/use-the-iptables-firewall-to-block-ads-on-your-linux-machine/) 可以使用上述方式手動更改 iptable,但如果使用核心模組來過濾廣告不僅更靈活,同時也可以提高瀏覽器性能,主要差別如下: * 使用核心模組過濾可以根據域名進行過濾,而不需要依賴固定的 IP 地址 * 核心模組過濾可以在解析階段就阻止廣告域名,從而防止任何針對這些域名的連接嘗試,而 iptables 規則只能在連接建立後起作用 * 在 DNS 層過濾,可以更早地阻止廣告內容的加載,可能提高瀏覽器性能。 Netfilter 是一個框架,嵌入在 Linux 核心中,用於執行各種封包的操作任務,包括網路地址轉換 (NAT) 和封包過濾。它通過幾個預定義的鉤子 (hook) 在封包傳輸過程中運行。以下是五個主要鉤子及其用途: 1. `NF_IP_PRE_ROUTING` 在做任何路由決策之前觸發,當作檢查和修改封包的初始點,適用於路由前的目標 NAT 和封包處理 1. `NF_IP_LOCAL_IN` 針對發往本地系統的封包, 允許檢查和修改那些要交給本機的入站封包 1. `NF_IP_FORWARD` 用於在系統中從一個網路介面轉發到另一個網路介面的封包,對應用轉發規則和過濾不屬於本地系統的封包非常重要 1. `NF_IP_POST_ROUTING` 在所有路由決策做出之後但在封包實際傳送之前觸發,通常用於源 NAT 和在封包傳送前的附加封包處理 1. `NF_IP_LOCAL_OUT` 針對本地系統生成的封包,在它們實際發出之前,提供修改出站封包、設置源 NAT 或應用其他出站過濾規則的機會 ![image](https://hackmd.io/_uploads/BkSrcrBIR.png) 圖片來源 [Netfilter -- Linux netfilter Hacking HOWTO: Netfilter Architecture](https://netfilter.org/documentation/HOWTO/netfilter-hacking-HOWTO-3.html) 參照 [adriver](https://github.com/Jongy/adriver),使用 Linux 核心模組,即可在核心裡頭註冊不同的 hook 來處理封包,為了在使用者瀏覽網站時屏蔽廣告,因此 hook 必須作用在進入本機之前,也就是 Local In 或 Pre Routing ```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`,即丟棄該封包 **hook function 的返回值共五個:** * `NF_DROP`丟棄封包 * `NF_ACCEPT`允許封包通過 * `NF_STOLEN`表示封包已被當前 hook 函式"偷走"。封包的所有權已經轉移到當前 hook 函式中,不在繼續沿Netfilter 處理路徑傳輸 * `NF_QUEUE`將封包送往 nfqueue * `NF_REPEAT`重新呼叫這個 hook function ```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 無法被解析的原因 ### 透過掛載 module 實作 ```$ sudo insmod adblock.ko``` ```$ lsmod | grep adblock``` 瀏覽網頁會發現許多廣告被屏蔽了 ![image](https://hackmd.io/_uploads/rkIQJTY8C.png =90%x) 使用`ifconfig`查詢你的系統中有哪些網卡,並確定你要監聽的網卡名稱,須先下載套件`net-tools` ```shell ~~aa860630@aa860630-ASUS-TUF-Dash-F15-FX516PM-FX516PM:~/adriver$ ifconfig eno2: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500 ether 04:42:1a:d1:d5:20 txqueuelen 1000 (Ethernet) RX packets 0 bytes 0 (0.0 B) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 0 bytes 0 (0.0 B) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536 inet 127.0.0.1 netmask 255.0.0.0 inet6 ::1 prefixlen 128 scopeid 0x10<host> loop txqueuelen 1000 (Local Loopback) RX packets 5280 bytes 495925 (495.9 KB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 5280 bytes 495925 (495.9 KB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 wlo1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 192.168.100.110 netmask 255.255.255.0 broadcast 192.168.100.255 inet6 fe80::9ff8:2596:4921:4e8f prefixlen 64 scopeid 0x20<link> ether 20:1e:88:e9:bb:1e txqueuelen 1000 (Ethernet) RX packets 140233 bytes 201059706 (201.0 MB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 56952 bytes 6138159 (6.1 MB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0~~ ``` `lo`顯示 running 使用此網路介面 ```shell sudo tcpdump -ni lo -c 10 -t udp and dst host 127.0.0.1 ```