# 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