# Linux 核心專題: 網路防火牆設計和實作 > 執行人: jujuegg > [專題解說錄影](https://www.youtube.com/watch?v=d1XMPahLZAo) ### Reviewed by `popo8712` 在實現你的防火牆規則匹配函式 `matchOneRule` 時,你將來源端口和目標端口的範圍組合存儲在一個32位的 `unsigned int` 中。請問這種設計在處理非常大的端口範圍時是否會有問題?例如處理跨多個16位區間的端口範圍時,你如何保證正確性和效率? > 埠號的範圍是 0 至 65535,基本上不會有超過 16 位元區間的埠號範圍。 > [name=jujuegg] ### Reviewed by `jychen0611` * 什麼情況下會使用到 `nfqueue`,它的功能和運作原理是? >NF_QUEUE : 將封包送往 `nfqueue`。 > 一般來說我們使用 NetFilter 是在 kernel space 處理封包,如果我們想要在 user space 處理的話,就需要用送到 nfqueue 中,本質上也是用 Netlink 來實現 kernel space 與 user space 之間的通訊,我目前還沒實作出例子。 > [name=jujuegg] * [Fix the struct has no member problem](https://github.com/jujuegg/RJFireWall/commit/dceb56653450385ba8e22ffad0717d8901f18e66) 中可針對核心版本不同做出判別,使其相容先前的版本,也就是 ```c struct netlink_kernel_cfg nltest_cfg = { .groups = 0, .flags = 0, .input = nlRecv, .cb_mutex = NULL, .bind = NULL, .unbind = NULL, #if LINUX_VERSION_CODE >= KERNEL_VERSION(a, b, c) .release = NULL, #else .compare = NULL, #endif }; ``` 並嘗試提交 pull request 到 [RJFireWall ](https://github.com/RicheyJang/RJFireWall)。流程可參考 : [理解如何在Github使用Pull Request (PR) ](https://hackmd.io/@judyyutong/understandPR) >我希望自己在未來能夠從小地方開始嘗試提出 pull request 現在就有一個機會,不用等到以後 * [Fix the sprintf overflow problem](https://github.com/RicheyJang/RJFireWall/commit/a449f7b92a9b30bf9c11134e4fb3efa2ed9d318e) 中為何是修改為 10 而非其他數字? 若有考量因素則可使用 **git rebase -i** 將敘述新增至 commit message 中。 ```diff - char saddr[25],daddr[25],sport[13],dport[13],proto[6],action[8],log[5]; + char saddr[25],daddr[25],sport[13],dport[13],proto[6],action[10],log[5]; ``` > 只是單純因為要存入 `action` 的字串長度有可能會超過 8 個字元,而不會超過 10 個字元。 > [name=jujuegg] ### Reviewed by `nosba0957` 在 `matchOneRule` 函式中,`sport` 和 `dport` 分別代表什麼?為何一個是 `unsigned short` 另一個是 `unsigned int` 型別? > sport 代表 Source IP 的埠號,dport 代表 Destination IP 的埠號。 > 關於 `unsigned short` 與 `unsigned int` 型別,由於埠號的長度是 16 個位元,所以兩個型態都不會有問題,不過感謝指正,我應該要統一型別。 > [name=jujuegg] ## 任務簡介 藉由 Linux 核心的 netfilter 和 netlink 打造網路傳輸層級的防火牆,並確保得以在 Linux 6.8+ 運作。 ## TODO: 檢驗 [RJ FireWall](https://github.com/RicheyJang/RJFireWall) 程式碼並探究其原理 > 理解如何使用 Linux 核心的 netfilter 對網路連線進行過濾、改寫,和 NAT 等功能 > 理解如何使用 Linux 核心的 netlink 對核心模組和工具程式進行互動 > 理解如何有效處理大量封包過濾條件 > 理解如何處理 NAT,以及 Linux 核心有哪些關鍵機制 ### Netlink Netlink 提供了 kernel space 與 user space 之間的通訊,透過 socket 來完成,以下是一個簡例。 ```c fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC); /* format the request */ send(fd, &request, sizeof(request)); n = recv(fd, &response, RSP_BUFFER_SIZE); /* interpret the response */ ``` 在此專案中,我會頻繁的使用 [`exchangeMsgK`](https://github.com/jujuegg/RJFireWall/blob/master/common/exchange.c#L3) 這個函式來與 Linux 核心交換資訊。 #### User Space #### `exchangeMsgK` 首先我們要開一個類型是 AF_NETLINK 的 socket,在 [linux/socket.h](https://github.com/torvalds/linux/blob/master/include/linux/socket.h#L264) 有定義,AF_xxxx (Address Families) 其實和 PF_xxxx (Protocol Families) 是一個完全相同的數字。 ```c int skfd = socket(PF_NETLINK, SOCK_RAW, NETLINK_MYFW); ``` 然後為該 socket 綁定一個 [`sockaddr_nl`](https://github.com/torvalds/linux/blob/master/include/uapi/linux/netlink.h#L37) 的結構體,讓核心知道我們是哪一個 process: ```c struct sockaddr_nl local; memset(&local, 0, sizeof(local)); local.nl_family = AF_NETLINK; local.nl_pid = getpid(); local.nl_groups = 0; bind(skfd, (struct sockaddr *) &local, sizeof(local)) ``` 同時也要指定傳輸的目的地,也就是 kernel space: ```c struct sockaddr_nl kpeer; memset(&kpeer, 0, sizeof(kpeer)); kpeer.nl_family = AF_NETLINK; kpeer.nl_pid = 0; // kernel space kpeer.nl_groups = 0; ``` 再來要準備我們要傳給核心的訊息,要使用的結構是 [`nlmsghdr`](https://github.com/torvalds/linux/blob/master/include/uapi/linux/netlink.h#L52C8-L52C16),`smsg` 是訊息的 payload 部分。 在 [uapi/linux/netlink.h](https://github.com/torvalds/linux/blob/master/include/uapi/linux/netlink.h#L98) 中定義了許多巨集函式可以使用。 - `NLMSG_SPACE` 會回傳該訊息的 header + payload 的長度,且該長度是對齊過的。 - `NLMSG_DATA` 會回傳該訊息的所在記憶體位址。 ```c struct nlmsghdr *message=(struct nlmsghdr *)malloc(NLMSG_SPACE(slen)*sizeof(uint8_t)); memset(message, '\0', sizeof(struct nlmsghdr)); message->nlmsg_len = NLMSG_SPACE(slen); message->nlmsg_flags = 0; message->nlmsg_type = 0; message->nlmsg_seq = 0; message->nlmsg_pid = local.nl_pid; memcpy(NLMSG_DATA(message), smsg, slen); ``` 將該訊息傳輸給 linux 核心,`kpeer` 也是一個 `nlmsghdr` 的結構體,與 `local` 相同,告訴此函式我們要傳給核心的目的地。 ```c sendto(skfd, message, message->nlmsg_len, 0, (struct sockaddr *) &kpeer, sizeof(kpeer)) ``` 如果運作順利,就可以成功接收來自 linux 核心的回覆: ```c struct nlmsghdr *nlh = (struct nlmsghdr *) malloc(NLMSG_SPACE(MAX_PAYLOAD)*sizeof(uint8_t)); recvfrom(skfd, nlh, NLMSG_SPACE(MAX_PAYLOAD), 0, (struct sockaddr *) &kpeer, (socklen_t *)&kpeerlen) ``` #### Kernel Space 而在核心這邊,我們需要呼叫 [`netlink_kernel_create`](https://github.com/torvalds/linux/blob/master/include/linux/netlink.h#L56),把要執行在 kernel space 的程式掛載上去,也就是放在 [`netlink_kernel_cfg`](https://github.com/torvalds/linux/blob/master/include/linux/netlink.h#L46) 結構中的 `*input` 成員。 ```c struct netlink_kernel_cfg nltest_cfg = { .groups = 0, .flags = 0, .input = nlRecv, //Receive Function .cb_mutex = NULL, .bind = NULL, .unbind = NULL, .release = NULL, }; nlsk = netlink_kernel_create(&init_net, NETLINK_MYFW, &nltest_cfg); ``` ##### `nlRECV(struct sk_buff *skb)` 在這邊我們不需要自己開一個 socket 去接收來自 user space 的訊息,核心會自動將訊息存入該函式的參數 `sk_buff` 中。 要抓取該封包的 header 與其中的 data 部分 ```c struct nlmsghdr *nlh = NULL; nlh = nlmsg_hdr(skb); void *data; data = NLMSG_DATA(nlh); pid = nlh->nlmsg_pid; len = nlh->nlmsg_len - NLMSG_SPACE(0); ``` 到這邊我們就完成了 User Space 與 Kernel Space 之間的通訊工作。 ### NetFilter 先感謝之前的相關期末專題執行人 [ItisCaleb](https://github.com/ItisCaleb/Netfilter-Adblock) 整理出來的材料,讓我可以不用花太多時間找第一手的教材來閱讀。 在 [Linux 專題: 透過 Netfilter 自動過濾廣告](https://hackmd.io/@sysprog/BJb0NRYH3#%E8%AA%AA%E6%98%8E-netfilter-%E9%98%BB%E6%93%8B%E7%89%B9%E5%AE%9A%E4%BE%86%E6%BA%90%E5%B0%81%E5%8C%85%E7%9A%84%E5%8E%9F%E7%90%86) 這篇文章中提到,NetFilter 提供了一系列的 hook,讓使用者可以在不同的階段過濾封包。 首先我們要先==設計==一個 hook,在下面這個 hook 中,他會在 `NF_IP_PRE_ROUTING` 的位置就進行過濾,是所有 hook 中第一個接觸到封包的位置。 ```c static struct nf_hook_ops nfop_in = { .hook = hook_main, // hook function .pf = PF_INET, .hooknum = NF_INET_PRE_ROUTING, // hook position .priority = NF_IP_PRI_FIRST }; ``` hook function 需要接收三個參數 - `priv` 為在 `nf_hook_ops` 的 `priv` 提供的物件 - `skb` 為此封包的 `sk_buff` - `state` 則為此封包的各種資訊,包括裝置、網路的命名空間等 在 hook function 中,我們可以查看流動到該 hook 的所有封包的內容,並且可以決定最終我們要對此封包執行的動作,包括: - `NF_DROP` : 丟棄封包。 - `NF_ACCEPT` : 允許封包通過。 - `NF_STOLEN` : 將封包的所有權轉移給這個 hook function,同時也意味著需要自己管理封包佔用的資源。 - `NF_QUEUE` : 將封包送往 nfqueue。 - `NF_REPEAT` : 重新呼叫這個 hook function。 設計完 hook 之後,要==註冊==到 linux 核心當中: ```c nf_register_net_hook(&init_net, &nfop_in); // 註冊 nf_unregister_net_hook(&init_net, &nfop_in); // 取消註冊 ``` ## TODO: 使 [RJ FireWall](https://github.com/RicheyJang/RJFireWall) 程式碼得以正確運作於 Linux v6.8+ > 確保 Linux 核心模組運作符合預期,要有對應的測試程式及流程,證明你的投入是有效益的 ### 防火牆功能 #### 新增過濾規則 每個添加的規則都會是一個叫做 [`IPRule`](https://github.com/jujuegg/RJFireWall/blob/master/common/include/common.h#L36) 的結構,而全部的規則則會形成一個串列,該串列的頭部叫做 `ipRuleHead`。 :::danger 不是「剛好」,這是當初的設計考量,不要反果為因! ::: 其中一個問題是要如何儲存 IPv4 的位址,因 IPv4 的位址的範圍是 0.0.0.0 ~ 255.255.255.255,可以由 4 組 8 個位元的資料來代表,所以我使用 `unsigned int` 來儲存此 IP 位址。而使用者在輸入 IP 時讀取的是字串,所以我們要將該 IP 轉為整數的型態儲存,方便未來做 mask 的操作。 這邊是獲取 mask 的部分,讀到 `/` 字元後面的數字即為 mask 的長度。 ```c for(i = 0; i < strlen(ipStr); i++){ if(p != -1){ len *= 10; len += ipStr[i] - '0'; } else if(ipStr[i] == '/') p = i; } if(p != -1){ if(len) r_mask = 0xFFFFFFFF << (32 - len); } else r_mask = 0xFFFFFFFF; ``` 將字串型態的 IP 轉成整數型態儲存,遇到 `.` 字元就將已存入的部分左移。 > 舉例來說: > 192.168.0.1 轉成無號整數的型態用二進位表示就會變成: > 11000000 10101000 00000000 00000001 > 所以 192 要左移 24 位,168 要左移 16 位,0 要左移 8 位 ```c for(i = 0; i < (p>=0 ? p : strlen(ipStr)); i++){ if(ipStr[i] == '.'){ r_ip = r_ip | (tmp << (8 * (3 - count))); tmp = 0; count++; continue; } tmp *= 10; tmp += ipStr[i] - '0'; if(tmp > 256 || count > 3) return -2; } ``` 當 kernel space 接收到 user space 想要新增過濾規則的請求時,他會讀取傳過來的 IPRule 結構,並將其加入串列中指定的位置。 #### 過濾封包 當我們向 linux 核心註冊了 hook 之後,我們就可以管理經過該 hook 的所有封包了,這些封包可以經由上面提到的 hook function 來執行我們想要的操作。 #### [`hook_main`](https://github.com/jujuegg/RJFireWall/blob/master/kernel_mod/hooks/hook_main.c#L7) 使用 `ip_hdr` 可以得到該封包的 header。 ```c struct iphdr *header = ip_hdr(skb); ``` :::danger 改進你的漢語表達。 ::: 這邊要先介紹一下 `ntohs`、`ntohl`、`htons`、`htonl` 這 4 個函式,在 [linux man page](https://linux.die.net/man/3/ntohl) 中寫到: > - The htonl() function converts the unsigned integer hostlong from host byte order to network byte order. > - The htons() function converts the unsigned short integer hostshort from host byte order to network byte order. > - The ntohl() function converts the unsigned integer netlong from network byte order to host byte order. > - The ntohs() function converts the unsigned short integer netshort from network byte order to host byte order. > > On the i386 the host byte order is Least Significant Byte first, whereas the network byte order, as used on the Internet, is Most Significant Byte first. 其實就是 Little-endian 與 Big-endian 之間的互相轉換。 :::danger 參照第一手材料,如 https://man7.org/linux/man-pages/ 不要花太多時間閱讀網際網路上殘缺的文章。 ::: ##### [`getPort`](https://github.com/jujuegg/RJFireWall/blob/master/kernel_mod/tools.c) 埠號 (port number) 則要根據他是 TCP 還是 UDP 的封包去進行解析。而因為埠號由 16 個位元組成,所以使用 `ntohs` 來獲取。 ```c tcpHeader = (struct tcphdr *)(skb->data + (hdr->ihl * 4)); *src_port = ntohs(tcpHeader->source); *dst_port = ntohs(tcpHeader->dest); udpHeader = (struct udphdr *)(skb->data + (hdr->ihl * 4)); *src_port = ntohs(udpHeader->source); *dst_port = ntohs(udpHeader->dest); ``` 使用 `ntohl` 可以獲取 ip address,因為 IPv4 的長度是 32 個 bit。 ```c sip = ntohl(header->saddr); dip = ntohl(header->daddr); ``` ##### [`matchOneRule`](https://github.com/jujuegg/RJFireWall/blob/master/kernel_mod/helpers/rule_helper.c#L97C6-L97C18) 再來我們要開始對比這個封包是否有對應到我們的防火牆內的任何一個規則。 :::danger 注意書寫規範: * 使用 lab0 規範的程式碼書寫風格,務必用 clang-format 確認一致 ::: ```c bool isIPMatch(unsigned int ipl, unsigned int ipr, unsigned int mask) { return (ipl & mask) == (ipr & mask); } bool matchOneRule(struct IPRule *rule, unsigned int sip, unsigned int dip, unsigned short sport, unsigned int dport, u_int8_t proto) { return (isIPMatch(sip,rule->saddr,rule->smask) && isIPMatch(dip,rule->daddr,rule->dmask) && (sport >= ((unsigned short)(rule->sport >> 16)) && sport <= ((unsigned short)(rule->sport & 0xFFFFu))) && (dport >= ((unsigned short)(rule->dport >> 16)) && dport <= ((unsigned short)(rule->dport & 0xFFFFu))) && (rule->protocol == IPPROTO_IP || rule->protocol == proto)); } ``` `isIPMatch` 會比較分別比較來源 IP 與目的地 IP 經過遮罩之後是否與規則中的 IP 相同。 至於中間比較埠號的過程就要解釋一下了,由於埠號其實是個 16 位元的整數,而且他其實是一個範圍,例如:8080-8090,所以我們可以把下限 16 個位元與上限 16 個位元,加起來一共 32 個位元存在一個 unsigned int 裡面。 在 [cmdAddRule](https://github.com/jujuegg/RJFireWall/blob/master/cmd/main.c#L4C23-L4C33) 這個函式,我把埠號的下限存在前 16 個 bit,而上限存在後 16 個 bit。 ```c sport = ((unsigned int)sportMin << 16) | (((unsigned int)sportMax) & 0xFFFFu) dport = ((unsigned int)dportMin << 16) | (((unsigned int)dportMax) & 0xFFFFu) ``` 所以上面在比較埠號的範圍時,`(unsigned short)(rule->sport >> 16)` 代表的是 sport 的下限,而 `(unsigned short)(rule->sport & 0xFFFFu)` 代表的則是 sport 的上限。 最後則是比較該規則的通訊協定 (TCP/UDP/ICMP) 是否符合該封包的類型。 回到 `hook_main`,如果目前這個封包有符合任何一個規則,則他會執行我們在該規則中制定的行為 (`NF_ACCEPT` 或 `NF_DROP`)。最後再回傳我們要執行的動作就大功告成了。 > 注意:他會匹配到「第一個符合的規則」,當初在制定防火牆的規則表時,使用者可以命名該規則,並可選擇加入到規則表中的任何順序。 ```c rule = matchIPRules(skb, &isMatch); if(isMatch) action = (rule.action == NF_ACCEPT) ? NF_ACCEPT : NF_DROP; return action; ``` ### 實際執行 在 make 的時候,可能會跑出錯誤訊息: ```shell warning: the compiler differs from the one used to build the kernel The kernel was built by: x86_64-linux-gnu-gcc-13 (Ubuntu 13.2.0-23ubuntu4) 13.2.0 You are using: gcc-13 (Ubuntu 13.2.0-23ubuntu4) 13.2.0 ``` 這是因為編譯器的命名不同,實際上他們的版本是相同的。 #### `netlink_kernel_cfg` 更動 在 [linux/netlink.h](https://github.com/torvalds/linux/blob/master/include/linux/netlink.h#L46C8-L46C26) 中,發現此結構的定義與以前不同,`compare` 改為 `release`。 - [commit dceb566](https://github.com/jujuegg/RJFireWall/commit/dceb56653450385ba8e22ffad0717d8901f18e66) #### `timeval` 更動 在 [fbdev: radeon: Remove 'struct timeval' usage](https://patchwork.kernel.org/project/linux-fbdev/patch/20150525040716.GA4448@tinar/) 中提到,`timeval` 這個結構使用的是 32-bit 變數,在 2038 年之後將會有 overflow 的問題,所以建議改用 `ktime_t`。 - [commit 2eacbb6](https://github.com/jujuegg/RJFireWall/commit/2eacbb672313e8babe78e99750f91742bb15ba38) ### 測試過程 先簡單寫一個 receiver 與 sender 來測試防火牆是否有用: ##### `sender.py` ```python import socket def main(): dest_ip = '127.0.0.1' dest_port = 8080 message = "Hello, UDP Receiver!" udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) udp_socket.sendto(message.encode(), (dest_ip, dest_port)) udp_socket.close() if __name__== "__main__": main() ``` ##### `receiver.py` ```python import socket def main(): local_ip = '127.0.0.1' local_port = 8080 udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) udp_socket.bind((local_ip, local_port)) print("UDP Receiver is up and listening") while True: data, addr = udp_socket.recvfrom(1024) print(f"Received message from {addr}: {data.decode()}") if __name__ == "__main__": main() ``` 在開啟防火牆阻擋來自 local ip 的 port 8080 後,發現 sender 那邊出現錯誤訊息: ``` Traceback (most recent call last): File "/home/juju/Desktop/RJFireWall/TestScript/sender.py", line 18, in <module> main() File "/home/juju/Desktop/RJFireWall/TestScript/sender.py", line 12, in main udp_socket.sendto(message.encode(), (dest_ip, dest_port)) PermissionError: [Errno 1] Operation not permitted ``` 由此可知,防火牆確實有作用,接下來要添加更多規則來執行更詳細的測試。 --- 測試完 local 端之後,我決定開另一台虛擬機來測試防火牆是否可以正常的阻擋不同主機間的通訊。 先使用 `ifconfig` 命令來看自己的 IP 位址,我的虛擬機網路設定是使用 NAT 模式,所以 2 台虛擬機都是使用 192.168 開頭的 IP。 > 有安裝防火牆的虛擬機 IP 是 192.168.60.130 > 負責打封包過去的虛擬機 IP 是 192.128.60.128 #### UDP 這邊先測試 UDP 通訊協定,我沿用上面寫的 `receiver.py`,把 IP 改為 0.0.0.0。 先用 [netcat](https://bingdoal.github.io/linux/2023/05/network-test-cmd-in-linux/#netcat) 打一個 UDP 封包過去,測試可以正常通訊: - sender : ```shell $ echo -n "hello" | nc -u -w1 192.168.60.130 8000 ``` - receiver : ```shell $ python3 receiver.py UDP Receiver is up and listening Received message from ('192.168.60.128', 34420): hello ``` 新增防火牆規則,阻擋來自 192.168.60.128 的 UDP 封包,發現沒有收到: ```shell $ ./uapp rule add add rule after [enter for adding at head]: rule name [max len=11]: r1 source ip and mask [like 127.0.0.1/16]: 192.168.60.128/32 source port range [like 8080-8031 or any]: any target ip and mask [like 127.0.0.1/16]: 192.168.60.130/32 target port range [like 8080-8031 or any]: any protocol [TCP/UDP/ICMP/any]: UDP action [1 for accept,0 for drop]: 0 is log [1 for yes,0 for no]: 1 result: From kernel: Success. ``` - sender : ```shell $ echo -n "after add rule" | nc -u -w1 192.168.60.130 8000 ``` - receiver : ```shell $ python3 receiver.py UDP Receiver is up and listening Received message from ('192.168.60.128', 34420): hello ``` 查看 log 發現確實有成功阻擋: ```shell $ ./uapp ls log sum: 1 [22437-11-07 22:26:11] [DROP] 192.168.60.128:46636->192.168.60.130:8000 proto=UDP len=22B ``` 把剛剛新加的防火牆規則刪掉,發現封包成功送到了: ```shell $ ./uapp rule del r1 succeed to delete 1 rules. ``` - sender : ```shell $ echo -n "after del rule" | nc -u -w1 192.168.60.130 8000 ``` - receiver : ```shell $ python3 receiver.py UDP Receiver is up and listening Received message from ('192.168.60.128', 34420): hello Received message from ('192.168.60.128', 40705): after del rule ``` ## TCP 再來測試 TCP 通訊協定,先用 netcat 開一個 TCP 的 port,然後用另一台虛擬機連線並傳送訊息: - sender : ```shell $ nc -v 192.168.60.130 8000 Connection to 192.168.60.130 8000 port [tcp/*] succeeded! hello ``` - receiver : ```shell $ nc -l 0.0.0.0 8000 hello ``` 新增防火牆規則,阻擋來自 192.168.60.128 的 TCP 封包,發現也沒有收到訊息: ```shell $ ./uapp rule add add rule after [enter for adding at head]: rule name [max len=11]: r2 source ip and mask [like 127.0.0.1/16]: 192.168.60.128/32 source port range [like 8080-8031 or any]: any target ip and mask [like 127.0.0.1/16]: 192.168.60.130/32 target port range [like 8080-8031 or any]: 8000-8000 protocol [TCP/UDP/ICMP/any]: TCP action [1 for accept,0 for drop]: 0 is log [1 for yes,0 for no]: 1 result: From kernel: Success. ``` - sender : ```shell $ nc -v 192.168.60.130 8000 Connection to 192.168.60.130 8000 port [tcp/*] succeeded! hello after add rule ``` - receiver : ```shell $ nc -l 0.0.0.0 8000 hello ``` 查看 log 發現也有成功阻擋: ```shell $ ./uapp ls log sum: 10 [22437-11-07 22:26:11] [DROP] 192.168.60.128:46636->192.168.60.130:8000 proto=UDP len=22B [44057-11-17 11:24:28] [DROP] 192.168.60.128:48460->192.168.60.130:8000 proto=TCP len=47B [44064-06-22 00:38:50] [DROP] 192.168.60.128:48460->192.168.60.130:8000 proto=TCP len=47B [44071-01-24 04:01:46] [DROP] 192.168.60.128:48460->192.168.60.130:8000 proto=TCP len=47B [44084-08-13 03:47:15] [DROP] 192.168.60.128:48460->192.168.60.130:8000 proto=TCP len=47B [44110-12-26 11:06:13] [DROP] 192.168.60.128:48460->192.168.60.130:8000 proto=TCP len=47B [44163-09-25 13:18:14] [DROP] 192.168.60.128:48460->192.168.60.130:8000 proto=TCP len=47B [44268-02-22 23:50:52] [DROP] 192.168.60.128:48460->192.168.60.130:8000 proto=TCP len=47B [44479-01-25 16:38:03] [DROP] 192.168.60.128:48460->192.168.60.130:8000 proto=TCP len=47B [44900-11-28 02:32:52] [DROP] 192.168.60.128:48460->192.168.60.130:8000 proto=TCP len=47B ``` 接下來測試防火牆開著的時候有沒有辦法建立 TCP 連線,發現沒有跳出成功連線的通知,卻也沒有跳出連線失敗的通知。正常來說,如果該 IP 沒有開啟該埠號的話,是會跳出連線失敗的通知的。 - sender : ```shell $ nc -v 192.168.60.130 8000 ``` 有趣的事情是,如果在短時間內把防火牆的規則刪除的話,是會自動連上去的。 ## ICMP 要測試 ICMP 通訊協定,要用到常用的 `ping` 命令: ```shell $ ping 192.168.60.130 PING 192.168.60.130 (192.168.60.130) 56(84) bytes of data. 64 bytes from 192.168.60.130: icmp_seq=1 ttl=64 time=0.500 ms 64 bytes from 192.168.60.130: icmp_seq=2 ttl=64 time=0.533 ms 64 bytes from 192.168.60.130: icmp_seq=3 ttl=64 time=0.898 ms 64 bytes from 192.168.60.130: icmp_seq=4 ttl=64 time=1.10 ms 64 bytes from 192.168.60.130: icmp_seq=5 ttl=64 time=0.971 ms ^C --- 192.168.60.130 ping statistics --- 5 packets transmitted, 5 received, 0% packet loss, time 4033ms rtt min/avg/max/mdev = 0.500/0.801/1.104/0.241 ms ``` 新增防火牆規則,阻擋來自 192.168.60.128 的 ICMP 封包,發現 `ping` 沒有得到回覆: ```shell $ ./uapp rule add add rule after [enter for adding at head]: rule name [max len=11]: r3 source ip and mask [like 127.0.0.1/16]: 192.168.60.128/32 source port range [like 8080-8031 or any]: any target ip and mask [like 127.0.0.1/16]: 192.168.60.130/32 target port range [like 8080-8031 or any]: any protocol [TCP/UDP/ICMP/any]: ICMP action [1 for accept,0 for drop]: 0 is log [1 for yes,0 for no]: 1 result: From kernel: Success. ``` - sender : ```shell $ ping 192.168.60.130 PING 192.168.60.130 (192.168.60.130) 56(84) bytes of data. ``` 把防火牆規則刪除之後,發現 `ping` 又正常運作了: ```shell $ ./uapp rule del r3 succeed to delete 1 rules. ``` - sender : ```shell $ ping 192.168.60.130 PING 192.168.60.130 (192.168.60.130) 56(84) bytes of data. 64 bytes from 192.168.60.130: icmp_seq=24 ttl=64 time=0.585 ms 64 bytes from 192.168.60.130: icmp_seq=25 ttl=64 time=1.13 ms 64 bytes from 192.168.60.130: icmp_seq=26 ttl=64 time=0.661 ms 64 bytes from 192.168.60.130: icmp_seq=27 ttl=64 time=0.525 ms 64 bytes from 192.168.60.130: icmp_seq=28 ttl=64 time=0.630 ms 64 bytes from 192.168.60.130: icmp_seq=29 ttl=64 time=0.422 ms ^C --- 192.168.60.130 ping statistics --- 29 packets transmitted, 6 received, 79.3103% packet loss, time 28631ms rtt min/avg/max/mdev = 0.422/0.659/1.134/0.225 ms ``` ## TODO: 量化網路防火牆的運作表現 > 修改既有程式碼和測試情境,使得防火牆的關鍵機制得以充分進行效能評比,善用 Linux 核心的工具 (如 Ftrace,參見《Demystifying the Linux CPU Scheduler》) ## TODO: 對現有程式碼提出改進方案並落實 > 在 GitHub 建立新的 repository,將上述修改納入,並確保所有文件和程式碼註解皆用美式英語書寫。