# nstack contributed by <`AlecJY`, `amikai`> 有一部份的內容在老師提供的[共筆](https://hackmd.io/s/ryfvFmZ0f)中就有提及,只是以自己的方式重新整理 ## 執行 nstack ### 環境設定 nstack 內有提供一個腳本設定網路環境,主要是建立一個 network namespace 和一對 veth devices 提供測試用的網路環境,要進行設定只要執行 ```bash sudo tools/testenv.sh start ``` 要移除網路環境設定則是執行 ```bash sudo tools/testenv.sh stop ``` #### 腳本分析 ##### 設定網路環境 ```bash=6 function start { ip netns add TEST ip link add veth0 type veth peer name veth1 ip link set dev veth0 up ip link set dev veth1 up ip addr add dev veth0 local $LOCAL_IP ip route add $STACK_IP dev veth0 ip link set dev veth1 netns TEST ip netns exec TEST ip link set dev veth1 up } ``` * 第七行加入了一個叫做 TEST 的 network namespace * 第八行加入了一對 veth devices : veth0 和 veth1 * 第九行及第十行分別啟動 veth0 和 veth1 * 第十一行設定 veth0 的 IP * 第十二行設定將 $STACK_IP 為目的地的路由到 veth0 * 第十三行將 veth1 分配至 TEST 這個 network namespace 去 * 第十四行將 TEST 中的 veth1 啟動 這時候如果輸入指令 `ip a` 就可以看到多了一張叫做 veth0 的網卡, IP 為 10.0.0.1,輸入指令 `sudo ip netns exec TEST ip a` 可以看到另一張被放進 TEST 中的網卡 veth1 ,沒有設定 IPv4 ##### 移除網路環境 ```bash=17 function stop { ip link set dev veth0 down ip link delete veth0 ip netns delete TEST } ``` * 第十八行先將 veth0 停止 * 第十九行將 veth0 刪除,由於 veth 需要成對存在,所以刪除 veth0 的同時 veth1 也會被移除 * 第二十行將 network namespace 移除 ### 啟動 nstack nstack 內有一個腳本用來啟動 nstack ,需要給要使用的網卡名字作為參數 ```bash tools/run.sh veth1 ``` #### 腳本分析 ```bash=3 # shmem for sockets dd if=/dev/zero of=/tmp/unetcat.sock bs=1024 count=1024 sudo setcap cap_net_raw,cap_net_admin,cap_net_bind_service+eip build/inetd sudo ip netns exec TEST su $USER -c "build/inetd $1" ``` * 第四行將 nstack 所使用到的 shared mem 檔案清空 * 第五行給 nastck 的主要執行檔 inetd 網路相關的權限,讓 nstack 可以使用普通使用者的權限執行 * 第六行則是在 TEST 這個 network namespace 以執行這條指令之使用者執行 inetd ## 測試 nstack ### ping test nstack 有提供一個腳本執行 ping 測試 ```bash tools/ping_test.sh ``` 一共會 ping 3 次,每次送出三個封包,每次的 sndbuf 都會加大 ### unetcat 將 nstack 編譯完成後, build 會產生一個叫做 unetcat 的執行檔,回去看原始碼後發現這支小程式是用來測試 UDP 運作的。它會從 nstack 那邊接收 udp 封包並將內容印出,在啟動 nstack 後,執行 ```bash build/unetcat ``` 接著可以利用 netcat 發送一些 UDP 封包進行測試 ```bash nc -u 10.0.0.2 10 ``` 執行這個指令後隨便輸入一些字串,然後按下 Enter 就會送出 #### 程式碼分析 ```C= #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include "nstack_socket.h" static char buf[2048]; int main(void) { void *sock; sock = nstack_listen("/tmp/unetcat.sock"); if (!sock) { perror("Failed to open sock"); exit(1); } while (1) { struct nstack_sockaddr addr; size_t r; memset(buf, 0, sizeof(buf)); r = nstack_recvfrom(sock, buf, sizeof(buf) - 1, 0, &addr); if (r > 0) write(STDOUT_FILENO, buf, r); } } ``` 在第十四行利用 `nstack_listen()` 開啟 shared mem 取得 sock 的記憶體位址,之後寫一個無窮迴圈呼叫 `nstack_recvfrom()` 讀取 UDP 封包,若有順利讀取到內容則輸出 ### udp.c 在 tests 資料夾內除了 unetcat 以外還有一段叫做 udp.c 的程式碼用來發送封包給 unetcat,只要將第十一行的 buf 內填入字串再編譯執行,它就會不斷朝向 10.0.0.2:10 發送內容為 buf 的 UDP 封包 ### Web server 在老師提供的參考[共筆](https://hackmd.io/s/ryfvFmZ0f#Web-server)內有提及一個 Web server,按照所描述的步驟可以順利啟動一個 HTTP Server ,但看不出來這個 server 跟 nstack 有什麼直接的關係,實際檢查程式碼也未找到與 nstack 相關的部分,只看到使用了 Linux 原來的 Socket API ,且 nstack 在 TCP 部分尚有功能尚未實作完成,所以認為此 server 是依靠 Linux native TCP/IP Stack 運作,而不是使用 nstack ,這部分在原來那份共筆內沒有清楚說明。 ## nstack 程式碼分析 ### nstack.c ```C=348 if (ip_config(handle, 167772162, 4294967040)) { perror("Failed to config IP"); exit(1); } ``` 這段是在 `main()` 裡面呼叫 `ip_config()` 設定 IP 的程式碼,其中的 167772162 和 4294967040 看起來好像是一串意義不明的 magic number ,去查看 `nstack_ip.h` 標頭檔的定義後,可以看到實際上這兩個數字分別對應到 IP Address 和 netmask ,而那兩個數字是以十進位表示的 10.0.0.2 及 255.255.255.0 。 ```C=106 int ip_config(int ether_handle, in_addr_t ip_addr, in_addr_t netmask); ``` --- ```C=45 static struct nstack_sock sockets[] = { { .info.sock_dom = XF_INET4, .info.sock_type = XSOCK_DGRAM, .info.sock_proto = XIP_PROTO_UDP, .info.sock_addr = (struct nstack_sockaddr){ .inet4_addr = 167772162, .port = 10, }, .shmem_path = "/tmp/unetcat.sock", }, }; ``` 這段程式碼用來建立一個 UDP socket 聽在 port 10 ,並將 shared memeory 檔案建立在 /tmp/unetcat.sock ,也就是 unetcat 這支程式所操作的檔案 ## UDP 封包接收 在 IP 處理完封包後如果是 UDP 封包會丟到 `udp_input()` ,在 `udp_input()` 中會檢查 port 是否有在 listen 並找出 listen 的 socket ```C=91 sockaddr.inet4_addr = ip_hdr->ip_dst; sockaddr.port = udp->udp_dport; sock = find_udp_socket(&sockaddr); ``` 在 udp.h 這個檔案的 `udp_input()` 中可以看到這段程式碼,會透過 `find_udp_socket()` 這個涵式搜尋 `udp_sock_tree` 這個紅黑樹,這顆樹的內容是在 `nstack_udp_bind()` 的時候建立起來的。 在找到 socket 之後會呼叫 `nstack_sock_dgram_input()` ,把封包內的資料複製至一個 `nstack_dgram` 資料結構內。一般應用程式要存取封包的時候再呼叫 `nstack_recvfrom()` 就會將封包裡面的內容取出。 ## UDP 封包傳送 UDP 封包傳送的內容最初由一般應用程式呼叫 `nstack_sendto()` 將資料傳入,之後裝進一個 `nstack_dgram` 資料結構內,然後將 socket 資訊送進 queue 內。有另一個涵式 `nstack_egress_thread()` 會檢查 queue 並且呼叫 `nstack_udp_send()` 處理封包, `nstack_udp_send()` 會填寫 source port 、 destination port 、長度及 checksum 這四個 UDP header 的資訊並且送至 IP 處理,只是實際執行上 nstack 會發出 `SIGUSR2` 導致執行中斷,不清楚是有什麼地方理解錯誤或是還有地方尚未實作完成 ## TCP 狀態圖 and 流程圖 ![](https://i.imgur.com/GnKY5jF.png) 該程式只實做了 `tcp_input` 的部份, 所以我們只能看到 recv 之後的狀態變化, 程式是以 server 的方式實做 我們可以從 `tcp_fsm` 裡看到這些狀態變化: CLOSED 狀態程式碼片段: ```c case TCP_CLOSED: LOG(LOG_INFO, "TCP state: TCP_CLOSED"); return 0; ``` 到了 CLOSED 狀態就代表連線結束了, 不用做任何事 --- LISTEN 狀態程式碼片段: ```c= case TCP_LISTEN: LOG(LOG_INFO, "TCP state: TCP_LISTEN"); if (rs->tcp_flags & TCP_SYN) { LOG(LOG_INFO, "SYN received"); rs->tcp_flags |= TCP_ACK; rs->tcp_ack_num = rs->tcp_seqno + 1; srand(time(NULL)); rs->tcp_seqno = rand() % 100; conn->state = TCP_SYN_RCVD; conn->recv_next = rs->tcp_ack_num; conn->send_next = rs->tcp_seqno + 1; LOG(LOG_INFO, "%d", ((uint32_t *) &rs)[3]); return tcp_hdr_size(rs); } return 0; ``` 先確定 tcp_flag 是否有被設為 SYN, 有的話代表這是別人開始傳送資料, 所以做以下事情: - 將 flag 設為 ACK - ACK 的數字為 seqno + 1 (line 7) - SYN 的數字為隨機 (line 8 - 9) - 將預期的 seqno 和 ackno 先設定好 --- `SYN_RCVD` 狀態程式碼片段: ```c case TCP_SYN_RCVD: LOG(LOG_INFO, "TCP state: TCP_SYN_RCVD"); if ((rs->tcp_flags & TCP_ACK) && rs->tcp_seqno == conn->recv_next && rs->tcp_ack_num == conn->send_next) { conn->state = TCP_ESTABLISHED; return 0; } rs->tcp_flags &= ~TCP_ACK; rs->tcp_ack_num = rs->tcp_seqno; rs->tcp_seqno = conn->send_next; conn->recv_next = rs->tcp_ack_num; conn->send_next = rs->tcp_seqno + 1; return tcp_hdr_size(rs); ``` 先確定 tcp_flag 是否有被設為 ACK, 並且接接收到預期的 seqno 跟 ackno, 所以做以下事情: - 將 flag 設為 ACK - ACK 的數字為 seqno + 1 (line 7) - SYN 的數字為隨機 (line 8 - 9) --- `ESTABLISHED` 狀態程式碼片段: ```c case TCP_ESTABLISHED: LOG(LOG_INFO, "TCP state: TCP_ESTABLISHED"); if ((rs->tcp_flags & TCP_ACK) && (rs->tcp_flags & TCP_PSH) && rs->tcp_seqno == conn->recv_next && rs->tcp_ack_num == conn->send_next) { /* data handling */ rs->tcp_flags &= ~TCP_PSH; rs->tcp_ack_num = rs->tcp_seqno + (bsize - tcp_hdr_size(rs)); rs->tcp_seqno = conn->send_next; conn->recv_next = rs->tcp_ack_num; conn->send_next = rs->tcp_seqno; /* TODO forward the payload to application layer */ return tcp_hdr_size(rs); } if (rs->tcp_flags & TCP_FIN) { /* Close connection. */ rs->tcp_flags |= TCP_ACK; rs->tcp_ack_num = rs->tcp_seqno + 1; rs->tcp_seqno = conn->send_next; conn->state = TCP_LAST_ACK; conn->recv_next = rs->tcp_ack_num; conn->send_next = rs->tcp_seqno + 1; return tcp_hdr_size(rs); } return 0; ``` 如果 ESTABLISHED 的 flag 有 ACK 和 PSH 並且 seqno 和 ackno 都是在預期情況, 那就是在需要回傳資料的情況, 下個判斷如果 flag 裡含有 TCP_FIN 則代表 state 將進入 CLOSE_WAIT 但是程是在實做上直接進入 LAST_ACK, 我並不知道為什麼 `FIN_WAIT_1`,`FIN_WAIT_2`, `CLOSE_WAIT`, `CLOSING `狀態程式碼片段: ```c case TCP_FIN_WAIT_1: case TCP_FIN_WAIT_2: case TCP_CLOSE_WAIT: case TCP_CLOSING: LOG(LOG_INFO, "TCP state: TCP_CLOSING"); ``` 程式中對這四個狀態並沒有做特別處理 ## 修改 nstack [程式碼](https://github.com/AlecJY/nstack) ### 增加 TCP Bind 因為這部分感覺 TCP 和 UDP 差異應該不大,所以主要的部份都是直接將 UDP 的 bind 部分移植過來 ### 增加檢查 TCP port 是否有正在 listen 在增加這項功能之前,隨便使用 telnet 連至任何一個 port 都會建立連線,這項功能的實作也與 TCP 的 bind 息息相關,在沒有 bind 的情況無法向 nstack 註冊 tcp port。主要的概念是在 tcp client 送出 SYN 的時候檢查該 port 是否有在 listen ,如果有的話就繼續進行握手,沒有就送出 RST-ACK 結束連線,並且把狀態移至 TCP_CLOSED。 ### 將 TCP 封包內容送至應用層 概念上就是在建立 TCP 連線後,將TCP 封包內的 data 取出傳至應用層,在傳輸至應用層這部分與 UDP 相差不多,所以直接使用 UDP 所使用的 `nstack_sock_dgram_input()` 傳輸至應用層,但是在 TCP header 大小的部分,由於 TCP header 的 options 欄位長度是會變動的,所以有另一個欄位 data offset 紀錄 TCP header 的大小,只是因為只占 4 bits ,所以在 nstack 的 `tcp_hdr` 結構內被一併放進 `tcp_flags` 內,因此需要將其中的最高 4 bits 取出,且由於 data offset 的單位為 32-bit word ,因此需要乘以 4 才能將單位換成 byte 。 ### 執行測試程式 tnetcat 這支測試程式基本上與 unetcat 一模一樣,只有操作的 shared mem 是不同的檔案而已,要啟動只須在 nstack 啟動後執行 ```bash build/tnetcat ``` 在 nstack 內有設定伺服器聽在 10.0.0.2:10 ,可以使用 netcat 發送一些 TCP 封包進行測試 ```bash nc 10.0.0.2 10 ``` 隨意輸入一些字串後按下 Enter 即會送出封包 也可以使用瀏覽器連線至 [http://10.0.0.2:10](http://10.0.0.2:10) 即可看到瀏覽器的請求訊息,但由於不會回傳任何封包,所以瀏覽器過一段時間後會產生 timeout error