# 透過 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
```