Try   HackMD

任務簡介

透過 netfilter,直接在核心層級過濾網路廣告。

執行人:steven523
專題解說錄影
Github

更新並確保專案在 Linux v6.8 上運作

下載指定的核心版本並解壓縮

$ wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.8.tar.xz
$ tar -xvf linux-6.8.tar.xz

查看目前版本

$ uname -r
6.5.0-35-generic

進入 linux-6.8 目錄,並複製核心檔案到當下目錄的 config 檔

$ cd linux-6.8
$ cp /boot/config-6.5.0-35-generic .config

配置 linux kernel,進入選單後將 config 檔載入

$ make menuconfig

點擊 load .config -> OK
點擊 save .config -> OK
離開選單

打開 .config 並將下列兩行內容刪除

CONFIG_SYSTEM_TRUSTED_KEYS=" "
CONFIG_SYSTEM_REVOCATION_KEYS=" "

編譯核心模組

$ make -j 8

編譯的過程中可能會遇到以下錯誤:

scripts/sign-file.c:25:10: fatal error: openssl/opensslv.h: No such file or directory
   25 | #include <openssl/opensslv.h>
      |          ^~~~~~~~~~~~~~~~~~~~
compilation terminated.
make[2]: *** [scripts/Makefile.host:116: scripts/sign-file] Error 1

這個錯誤表示編譯過程中缺少 OpenSSL 的標頭檔 opensslv.h,導致無法編譯 sign-file 工具

  • 安裝 OpenSSL 開發包並重新編譯
    ​​​​sudo apt-get install libssl-dev
    

安裝核心模組,安裝新核心,然後重新啟動系統

$ sudo make modules_install
$ sudo make install
$ sudo reboot

確認是否安裝成功

$ uname -mrs
Linux 6.8.0 x86_64

開發環境

$ gcc --version
gcc (Ubuntu 12.3.0-1ubuntu1~22.04) 12.3.0

$ uname -mrs
Linux 6.8.0 x86_64

$ lscpu
Architecture:            x86_64
  CPU op-mode(s):        32-bit, 64-bit
  Address sizes:         39 bits physical, 48 bits virtual
  Byte Order:            Little Endian
CPU(s):                  12
  On-line CPU(s) list:   0-11
Vendor ID:               GenuineIntel
  Model name:            Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz
    CPU family:          6
    Model:               165
    Thread(s) per core:  2
    Core(s) per socket:  6
    Socket(s):           1
    Stepping:            2
    CPU max MHz:         5000.0000
    CPU min MHz:         800.0000
    BogoMIPS:            5199.98
Virtualization features: 
  Virtualization:        VT-x
Caches (sum of all):     
  L1d:                   192 KiB (6 instances)
  L1i:                   192 KiB (6 instances)
  L2:                    1.5 MiB (6 instances)
  L3:                    12 MiB (1 instance)
NUMA:                    
  NUMA node(s):          1
  NUMA node0 CPU(s):     0-11

TODO: 重現去年 Netfilter 實驗,記錄下來

參照 ItisCaleb 於去年的實驗,可知一開始他是透過以 Linux 核心模組來阻擋已知的廣告網址,這些已知的網址以資料庫的方式儲存,如果封包經解析過後其域名符合儲存列表(hosts) 的內容,則拋棄。後來結合 eBPF 在 userspace 的應用程式透過額外的過濾程式來告訴作業系統它希望收到哪些網路封包。

不過在本實驗只執行使用 netfilter 操作核心模組來阻擋已知的廣告網址。

Netfilter 及相關工具介紹

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 →

以下是 5 個 chain 的標識,以及它們對應的 hook 點:

  • PREROUTING ( NF_IP_PRE_ROUTING ): 封包進入路由表之前
  • INPUT ( NF_IP_LOCAL_IN ): 通過路由表後,目的地為本機
  • FORWARD ( NF_IP_FORWARD ): 通過路由表後,目的地不為本機
  • OUTPUT ( NF_IP_LOCAL_OUT ): 由本機產生,向外轉發
  • POSTROUTING( NF_IP_POST_ROUTING ): 發送到網卡接口之前

假設伺服器知道如何路由封包,防火牆允許封包傳輸,以下就是不同場景下封包的傳輸流程:

  • 收到、目的地是本機的封包: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

BPF(Berkeley Packet Filter)

BPF, eBPF, XDP
最初於 20 世紀 90 年代初開發,作為網路封包擷取和分析的封包過濾機制。它的設計目標是高效、輕量級和安全,只允許根據過濾規則捕獲特定的資料包。 BPF 實現了更快的資料包處理,並減少了將不必要的資料包從核心複製到 user space 的開銷。

核心概念是讓 user space 的應用程式可以透過額外的過濾程式來告訴作業系統它希望收到哪些網路封包,這麼做的好處顯而易見:系統可以在封包一進入到 Kernel Space 時就過濾掉沒有作用的封包,避免這些封包一路經過作業系統的 Networking Stack(網路堆疊)一路傳到 User Space 上面的應用程式。

eBPF

jserv 教材
比起 BPF,它還可以用於非網路相關的功能,使開發者可以輕易地做到作業系統層級的動態追蹤,或是針對系統的某一個部分進行最佳化。

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 →

上圖顯示,當我們將 eBPF program 附加到 XDP Hook 上,NIC(網卡)接收到來自其他主機的封包,它會判斷應該對該封包做出何種行為:

  • 丟棄封包(XDP_DROP):假設我們撰寫了一個 DDoS 偵測器,eBPF program 判定該封包為惡意封包時便可以在 XDP Hook 階段將封包丟棄。
  • 接受封包(XDP_PASS):如果 eBPF program 認為封包沒有問題,也不需要對該封包進行任何修改(改變封包的內容,可能是修改 source IP、destination IP),eBPF program 可以回傳 XDP_PASS 讓封包流入作業系統的 Networking Stack。經過 kernel space 一系列的處理後,位於 user space 的 backend application 就能收到該封包了。
  • 傳送封包(XDP_TX):如果我們使用 XDP 實作一個 Proxy 或是 Load balancer,在修改封包內容後,eBPF program 應該回傳 XDP_TX,讓封包直接離開主機。
  • 轉送封包(XDP_REDIRECT):行為與 XDP_TX 雷同,不過網路封包會導向至其他網卡處理。
  • 異常封包處理(XDP_ABORTED):行為與 XDP_DROP 雷同,但是 eBPF program 會在 tracepoint 上提供錯誤訊息的 log。

XDP(eXpress Data Path)

一種基於 eBPF 的高效能資料路徑,目的是儘早處理網路封包,並繞過 Linux 的大部分網路堆疊,以高速率發送和接收網路封包。這允許在封包進入網卡驅動程式的最早階段就進行處理,從而顯著降低延遲和提高效能,透過它我們可以在網路封包進入到 Networking Stack 之前完成封包的處理

XDP 和 netfilter 可以互補使用,XDP 提供低層次的高效封包過濾,而 netfilter 提供高層次的靈活封包處理

  • XDP 可以快速過濾和丟棄大量不需要的流量,減少核心的處理負載
  • 對於通過 XDP 初步過濾的封包,netfilter 可以應用更複雜的防火牆規則和網絡策略。這包括狀態跟踪、NAT、應用層過濾等

核心模組功能

本 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 內容匹配,便可以將封包直接拋棄。

# Makefile

kernel:
    ./generate_hash.sh .
    $(MAKE) -C $(KDIR) M=$(PWD) modules
    $(MAKE) load

Makefile 中,我一開始透過以上方法在終端機執行 $ make kernel,直接做編譯 generate_hash.sh 、編譯核心模組和掛載模組的動作,不過執行時會發生以下兩種錯誤:

error: implicit declaration of function ‘in_word_set’ [-Werror=implicit-function-declaration]
  127 |         flag = in_word_set(host, strlen(host));
      |                ^~~~~~~~~~~
/home/steven/linux2024/final/Netfilter-Adblock/kadblock.c:127:14: warning: assignment to 
‘const char *’ from ‘int’ makes pointer from integer without a cast [-Wint-conversion]
  127 |         flag = in_word_set(host, strlen(host));
      |              ^
cc1: some warnings being treated as errors

這段報錯意思是,編譯器找不到 in_word_set 函式的聲明或定義,通常是因為編譯器在編譯過程中沒有找到相應的標頭檔,導致無法辨識 in_word_set 函式。

CC [M]  /home/steven/linux2024/final/Netfilter-Adblock/kadblock.o
In file included from /home/steven/linux2024/final/Netfilter-Adblock/kadblock.c:11:
hosts.gperf:88:1: warning: no previous prototype for ‘in_word_set’ [-Wmissing-prototypes]

而這個警告表示,編譯器在編譯 hosts.gperf 生成的程式碼時,沒有找到 in_word_set 函式的先前聲明,通常是因為在 hosts.gperf 生成的程式碼中,沒有提供 in_word_set 函式的函式原型。

所以我將 Makefile 的編譯內容改為:

kernel:
-   ./generate_hash.sh .
    $(MAKE) -C $(KDIR) M=$(PWD) modules
    $(MAKE) load

日後在編譯模組前,先單獨編譯 generate_hash.sh,並在生成的 host_table.h 中新增以下函式原型:

 # host_table.h

+ const char *in_word_set(const char *str, size_t len);

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",
      ...
    }
}

如此便可成功編譯模組。

執行成果

查看網路設備的狀態,並確認本機 IP 和網卡名稱

$ ifconfig
enp7s0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        ether 08:97:98:e0:51:7a  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 130405  bytes 12779024 (12.7 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 130405  bytes 12779024 (12.7 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

wlp0s20f3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.100.170  netmask 255.255.255.0  broadcast 192.168.100.255
        inet6 fe80::39eb:78ba:58ef:c490  prefixlen 64  scopeid 0x20<link>
        ether 84:1b:77:00:f8:e8  txqueuelen 1000  (Ethernet)
        RX packets 2704249  bytes 2343983910 (2.3 GB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 989360  bytes 305554448 (305.5 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

透過 tcpdump 觀察封包的來源 IP 和埠、目標 IP 和埠,以及封包的長度。這段指令用於捕捉從指定網卡 lo 進入的,來源或目的地為 127.0.0.1 的封包,並在捕捉到 10 個封包後停止。

$ sudo tcpdump -ni lo -c 10 -t host 127.0.0.1
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on lo, link-type EN10MB (Ethernet), snapshot length 262144 bytes
00:46:35.144993 IP 127.0.0.1.37354 > 127.0.0.53.53: 17123+ A? google.com. (28)
00:46:35.145080 IP 127.0.0.1.32121 > 127.0.0.53.53: 65502+ Type65? google.com. (28)
00:46:35.145430 IP 127.0.0.53.53 > 127.0.0.1.32121: 65502 1/0/0 Type65 (53)
00:46:35.153299 IP 127.0.0.53.53 > 127.0.0.1.37354: 17123 1/0/0 A 172.217.163.46 (44)
00:46:40.240913 IP 127.0.0.1.7845 > 127.0.0.53.53: 55449+ A? mail.google.com. (33)
00:46:40.240945 IP 127.0.0.1.55228 > 127.0.0.53.53: 2123+ Type65? mail.google.com. (33)
00:46:40.251998 IP 127.0.0.53.53 > 127.0.0.1.7845: 55449 1/0/0 A 172.217.160.69 (49)
00:46:40.252763 IP 127.0.0.53.53 > 127.0.0.1.55228: 2123 0/1/0 (83)
00:46:49.263768 IP 127.0.0.1.32516 > 127.0.0.53.53: 26712+ A? ogs.google.com. (32)
00:46:49.263892 IP 127.0.0.1.23883 > 127.0.0.53.53: 29786+ Type65? ogs.google.com. (32)
10 packets captured
26 packets received by filter
0 packets dropped by kernel

tcpdump 是一個擷取網路封包的工具,用於捕捉和分析網路流量。

從這段指令可以看到 UDP 協定的封包

$ sudo tcpdump -ni lo -c 10 -t udp and host 127.0.0.1
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on lo, link-type EN10MB (Ethernet), snapshot length 262144 bytes
IP 127.0.0.1.37578 > 127.0.0.53.53: 12977+ [1au] A? tpc.googlesyndication.com. (54)
IP 127.0.0.1.34392 > 127.0.0.53.53: 23572+ [1au] A? www.googleadservices.com. (53)
IP 127.0.0.1.52780 > 127.0.0.53.53: 25730+ [1au] A? www.googleadservices.com. (53)
IP 127.0.0.1.53841 > 127.0.0.53.53: 39705+ [1au] A? pagead2.googlesyndication.com. (58)
IP 127.0.0.1.48540 > 127.0.0.53.53: 51286+ [1au] A? www.googleadservices.com. (53)
IP 127.0.0.1.40488 > 127.0.0.53.53: 64589+ [1au] A? www.googleadservices.com. (53)
IP 127.0.0.1.49773 > 127.0.0.53.53: 5303+ [1au] A? www.googleadservices.com. (53)
IP 127.0.0.1.41083 > 127.0.0.53.53: 63712+ [1au] A? www.googleadservices.com. (53)
IP 127.0.0.1.42716 > 127.0.0.53.53: 22789+ [1au] A? www.googleadservices.com. (53)
IP 127.0.0.1.1363 > 127.0.0.53.53: 22673+ A? www.googleadservices.com. (42)
10 packets captured
22 packets received by filter
0 packets dropped by kernel

先在原本的阻擋列表 hosts 新增上述的幾個域名

google.com                     // https
mail.google.com                // https
www.googleadservices.com       // udp
pagead2.googlesyndication.com  // udp

透過 dmesg 查看核心環境中有關 HTTP GET、TLS、UDP 封包以及域名的相關訊息,可看到編譯並掛載核心模組後,UDP 封包顯示已被丟棄,不過屬於 https 的封包 google.com 卻沒顯示被丟棄。

$ sudo dmesg
[32735.615743] TLS handshake len: 4284
[32735.615805] TLS handshake len: 4284
[32735.618173] TLS handshake len: 4284

[10211.770548] Host: connectivity-check.ubuntu.com
[10212.352794] HTTP GET request detected.

[10447.714883] Host: detectportal.firefox.com
[10447.778043] HTTP GET request detected.

[10448.461515] Host: ipv4only.arpa
[10448.461738] HTTP GET request detected.

[13412.789060] Host: x.trvdp.com
[13412.789125] Get UDP
[13412.789126] Host: x.trvdp.com
[13418.142087] Get UDP
[13418.142091] Host: google.com
[13418.142162] Get UDP
[13418.142165] Host: google.com
[13419.865934] Get UDP
[13419.865938] Host: www.googleadservices.com
[13419.865939] Dropping Packet
[13419.865968] Get UDP
[13419.865969] Host: www.googleadservices.com
[13419.865970] Dropping Packet
[13419.927362] Get UDP
[13419.927366] Host: www.googleadservices.com
[13419.927367] Dropping Packet
[13419.927401] Get UDP
[13419.927402] Host: www.googleadservices.com
[13419.927403] Dropping Packet
[13419.963672] Get UDP
[13419.963675] Host: www.googleadservices.com
[13419.963677] Dropping Packet
[13419.963721] Get UDP
[13419.963722] Host: www.googleadservices.com
[13419.963722] Dropping Packet

以下圖片是 opgg 網站,這個網站內有時包含 https 廣告有時則是 udp 廣告,經測試後可看見比對結果,原本分佈在五處的廣告在掛載模組後看不見了。左下的影片是該網站原本就插入的,不算廣告。

image

image

問題紀錄

目前無法處理 https 封包,遇到時即便該域名儲存在 hosts 列表,但無法阻擋它。

因為通過網路傳輸 https 封包時會在 TCP 上添加一個加密層,通常是 TLS 或 SSL,所以當我們在 kernel space 收到時即使能提取到 TCP 資料部份,也無法讀取或修改其中的內容,若要查看解密後的內容,則需要訪問解密金鑰並進行解密,這通常是在應用層(例如瀏覽器或伺服器)資料傳輸後完成的。

若要阻擋 https 封包,可嘗試兩種方法:

  • 基於 IP 和 port 過濾所有 https 封包(通常是 port 443),但這種方法太過粗糙,會影響所有使用 https 的網站
  • 利用 eBPF 在 kernel space 執行自定義的程式碼來過濾和處理封包,可以實現在 user space 阻擋指定的域名封包,但這通常需要配合其他工具和技術來達成完整的功能。例如可以使用 eBPF 與特定的軟體或框架( 如 tc(traffic control)、XDP(eXpress Data Path))結合,進而達到阻擋 https 封包的效果。