Try   HackMD

Linux 核心專題: 透過 Netfilter 自動過濾廣告

執行人: aa860630, steven523
專題解說影片(aa860630)
專題解說影片(steven523)

Reviewed by ollieni

為何只擷取udp 通訊協定與tcp通訊協定的封包就可以屏蔽廣告?

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 地址加入規則這一種可能嗎,還是有其他可能的原因?

aa860630
內容傳遞網路 (CDN) 是一組地理上分散的伺服器,用於在靠近終端使用者的位置快取內容,以達到更好的閱讀質量

即使將某些網址的 IP 地址加入 iptables 的規則中,仍無法阻止與該網址的連線,原因可能是因為你使用代理伺服器或 VPN,上述方法可以繞過本地的 iptables 規則,通過其他 IP 地址訪問目標網站,導致封鎖無效

Reviewed by jujuegg

  • NF_DROP:將封包丟棄
  • NF_STOLEN:由 hook function 處理該封包,不再繼續傳送

Netfilter 可以藉由在 hook function 中回傳不同的操作來告訴防火牆我們要執行的動作,因為 NF_DROPNF_STOLEN 都不會將封包繼續往下傳,請問這兩個操作還有特別不一樣的地方嗎?

steven523
有的,NF_DROP 是指不再處理也不會繼續傳送封包,並將 sk_buff 所佔的資源丟棄,大多情況都是透過此操作來過濾不安全或不需要的封包。

NF_STOLEN 的部分雖然也不會將封包繼續傳送給後續的 Netfilter hook,但是會將此封包給 hook function 進一步接管,這同時也意味著需要自己管理封包佔用的資源,因為核心不會釋放掉 sk_buff 資源,使用完 sk_buff 後需要再呼叫 kfree_skb()將其釋放。
使用到 NF_STOLEN 的情況比較少,像是對封包進行分析或修改,然後再重新傳入 network stack,或是將封包交給其他核心子系統或使用者空間處理。

參考資料

任務簡介

在 Linux v6.8 重現透過 Netfilter 自動過濾廣告實驗,並修正對應的程式碼。

TODO: 探討 netfilter 原理並闡述過濾廣告的策略

可重用去年報告的素材,但要更新到 Linux v6.8+

Netfilter 是 Linux 2.4 引入的一個子系統,它提供了一個用於實現高級網路功能的框架,例如封包過濾、網路位址轉換(NAT) 和連接跟蹤。它利用核心網路程式碼中的 Hook 來實現,核心程式碼可以為特定的網路事件去註冊呼叫函式的位置,例如當收到封包時,就會觸發事件的處理程式並執行模組指定的操作。

Iptables 允許系統管理員配置 Linux 核心防火牆的 IP 封包過濾規則。可透過 Iptables 工具使我們方便在 userspace 對 Netfilter 進行操作。

此外 Netfilter 框架提供了一種強大的機制,用於阻擋和操作 Linux 核心中的網路封包。該框架有 2 種元件 - Netfilter hooksConntrack

Conntrack 所做的事情就是發現並追蹤這些連線的狀態,具體包括:

  1. 從資料包中提取元組(tuple) 訊息,辨別資料流(flow) 和對應的連接(connection)
  2. 為所有連線維護一個狀態資料庫(conntrack table),例如連線的建立時間、傳送包數、傳送位元組數等等
  3. 回收過期的連接(GC)
  4. 為更上層的功能(例如NAT) 提供服務

基本上 conntrack 可以用來清理掉封包的殘留連接狀態。例如在 TCP 協定中,如果有一個封包通過 iptables 的 DROP 目標被丟棄,但這個封包是 TCP 在 three way handshake 過程中的一部分(如 SYN 封包),則 Conntrack 可能會記錄這個封包的狀態為 SYN_RECV,即已接收到 SYN 封包但尚未進行確認。在這種情況下,即使這個 SYN 封包被丟棄了,Conntrack 也可能會記錄相關的連接狀態。

不過在我們的專案中,這樣的情況在阻擋網路廣告時並不常見,所以基本上不太會用到這個指令。

Netfilter hooks 是在核心中註冊的函式,要在 network stack 中的特定點呼叫。這些 hooks 可以看作是在 stack 不同層中的檢查點。

每個 hook 點對應於封包處理的不同階段,如下圖所示:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

以下是五個主要的 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 冒充實際的 client server 與網路通訊兩端分別建立連接,並以 local proxy 服務攔截流量
    • 或是利用 eBPF 搭配 XDP 工具執行自定義的程式碼來過濾和處理封包,可以實現在 user space 獲取加密前的封包
    • 當獲得 HTTPS 封包內容時提取域名,並與過濾名單比對
    • 如果匹配,則丟棄該封包,阻止該廣告的載入

  • 通過監控和攔截 DNS Query,可以辨識目標域名並加以過濾
    • 攔截 DNS Query ( UDP 封包,port 53 )
    • 提取 DNS Query 中的域名,並與過濾名單比對
    • 如果匹配,則丟棄該封包,阻止該廣告的載入

除了掛載核心模組之外,我們還能透過實作以下手動更改 iptables 的方式來過濾廣告:

實驗前清除所有規則

$ sudo iptables -F

使用命令查找目標網頁的資訊

$ 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 內容

$ 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 地址

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 失敗

$ 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 自動過濾廣告實驗,並修正對應的程式碼。

這個部份我們各自參照了兩個不同的專案來實作

參照 adriver

使用 Linux 核心模組,即可在核心裡頭註冊不同的 hook 來處理封包,為了在使用者瀏覽網站時屏蔽廣告,因此 hook 必須作用在進入本機之前,也就是 Local In 或 PreRouting。

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 點和系統狀態的詳細信息
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,即丟棄該封包

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 是否不為空
  2. skb 是否包含 ip_header
  3. ip_header 裡的協定是否為 UDP 通訊協定
  4. udp_header 是否不為空
  5. 確認埠號為 53
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

udp_header 指標位置 + udphdr 長度,即為 Data 位置

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

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 訊息格式

封包在執行完 should_run_dns_sfilter 之後,若包含 DNS 資訊的話會將其存入 buf->data 裡,用來與 dns_sfilters[] 裡的字串做比對,若比對成功則丟棄封包,dns_sfilters[] 包含許多過濾規則,內容如下:

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 查看核心訊息

[  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是否不為空
  2. skb是否包含 ip_header
  3. ip_header 裡的協定是否為 TCP 通訊協定
  4. tcp_header 是否不為空
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 0317 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

[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 於去年的實驗

本 Linux 核心模組,用於阻擋和過濾特定網絡流量。主要功能包括:

  1. 從 sk_buff 結構中提取 TCP 和 UDP 封包
  2. 解析封包,提取域名
  3. 檢查域名是否在儲存列表(hosts) 中,如果在則丟棄此封包

hosts 列表內容包含我瀏覽各網頁時蒐集的廣告域名,以及此網站提供用於 iptables 的命令列表,域名位於每列命令的尾端。

struct sk_buff

sk_buff(socket buffer) 結構在 Linux 核心中用於管理網路封包,它本身是一個 metadata structure,不包含任何封包資料,所以真正的封包資料儲存在它所指向的緩衝區內。

下圖的各指標是 sturct sk_buff 在封包緩衝區內的不同位置和緩衝區的佈局:

image

  • head:指向已分配記憶體的開始位置
  • data:指向實際封包資料的開始位置
  • tail:指向封包資料的結束位置
  • end:指向已分配記憶體的結束位置
  • headroom:資料開始之前的空間
  • data:實際的封包資料
  • tailroom:資料結束之後的空間
  • skb_shared_info:儲存關於緩衝區的共享信息,比如分頁碎片(page frags) 和分段列表(frag_list)

於核心模組初始化時註冊 hook

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_DROPNF_ACCEPT 來決定是否阻擋該封包。

struct nf_hook_ops 的參數:

  • hook : 指向 netfilter hook function 的指標
  • pf : 要阻擋的封包協定類別
  • hooknum : 指定要呼叫函式的 hook point

NFPROTO_IPV4NF_INET_PRE_ROUTING 皆定義在 linux/netfilter.h

提取 TCP 資料

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 資料,會先透過宣告的 *iptcp 分別指向 ip_hdrtcp_hdr 提取的 IP header 和 TCP header,接著檢查他們是否有被正確的提取。

計算封包資料的偏移量和長度過後利用 skb_linearize 將 paged skb 轉換成線性的,確保資料在記憶體區塊中是連續分佈,接著把資料指標指向實際資料位置後,回傳資料的長度以便 hook function 操作。

skb_linearize 主要作用是將非線性資料轉換為線性資料,以便於核心的處理和傳遞。執行成功時返回 0,失敗實則返回非 0 值。

提取 UDP 資料

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 判斷是否阻擋該網絡封包

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 請求

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 請求

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 函式將 network byte 順序轉換為 host byte 順序。檢查提取到的 UDP 封包其 destination port 是否等於 53。port 53 是 DNS 的標準 port,因此這個檢查是為了確定該封包是否是 DNS 請求。

解析封包,提取域名並檢查其是否在 hosts 檔案

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 : 中止消息的長度
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 函式檢查是否在 hosts 檔案中,若有的話則將 ret 設定成 NF_DROP 並阻擋該封包。

如果丟棄的是 TCP 封包,則逐一呼叫 send_server_acksend_closesend_tcp_reset 函式來發送 TCP 控制包,以通知對方終止連接。

  • send_server_ack(skb, state) : 發送一個 ACK 包,確認接收到資料
  • send_close(skb, proto, state) : 發送一個 FIN/PSH 包,表示將關閉連接,並附加一個自定義的中止消息
  • send_tcp_reset(skb, state) : 發送一個 RST 包,強制關閉連接
# 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 來為要阻擋的域名建立 hash table,gperf 是一個用於生成完美 hash function 和 hash table 的工具,適合用來快速搜尋和配對一組鍵值(如域名)。

# 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 廣告有時則是 udp 廣告,經測試後可看見比對結果,原本分佈在五處的廣告在掛載模組後看不見了。左下的影片是該網站原本就插入的,不算廣告。

image

image

TODO: 檢視其他學員在 netfilter 的投入狀況,提出疑惑和建議

課程期末專題找出同樣從事 netfilter 相關專案開發的學員,在其開發紀錄提出你的疑惑和建議。
在此彙整你的認知和對比你的產出。