nstack

contributed by <AlecJY, amikai>
有一部份的內容在老師提供的共筆中就有提及,只是以自己的方式重新整理

執行 nstack

環境設定

nstack 內有提供一個腳本設定網路環境,主要是建立一個 network namespace 和一對 veth devices 提供測試用的網路環境,要進行設定只要執行

sudo tools/testenv.sh start

要移除網路環境設定則是執行

sudo tools/testenv.sh stop

腳本分析

設定網路環境
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

移除網路環境
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 ,需要給要使用的網卡名字作為參數

tools/run.sh veth1

腳本分析

# 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 測試

tools/ping_test.sh

一共會 ping 3 次,每次送出三個封包,每次的 sndbuf 都會加大

unetcat

將 nstack 編譯完成後, build 會產生一個叫做 unetcat 的執行檔,回去看原始碼後發現這支小程式是用來測試 UDP 運作的。它會從 nstack 那邊接收 udp 封包並將內容印出,在啟動 nstack 後,執行

build/unetcat

接著可以利用 netcat 發送一些 UDP 封包進行測試

nc -u 10.0.0.2 10

執行這個指令後隨便輸入一些字串,然後按下 Enter 就會送出

程式碼分析

#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

在老師提供的參考共筆內有提及一個 Web server,按照所描述的步驟可以順利啟動一個 HTTP Server ,但看不出來這個 server 跟 nstack 有什麼直接的關係,實際檢查程式碼也未找到與 nstack 相關的部分,只看到使用了 Linux 原來的 Socket API ,且 nstack 在 TCP 部分尚有功能尚未實作完成,所以認為此 server 是依靠 Linux native TCP/IP Stack 運作,而不是使用 nstack ,這部分在原來那份共筆內沒有清楚說明。

nstack 程式碼分析

nstack.c

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 。

int ip_config(int ether_handle, in_addr_t ip_addr, in_addr_t netmask);

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

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 流程圖


該程式只實做了 tcp_input 的部份, 所以我們只能看到 recv 之後的狀態變化, 程式是以 server 的方式實做
我們可以從 tcp_fsm 裡看到這些狀態變化:

CLOSED 狀態程式碼片段:

case TCP_CLOSED:
    LOG(LOG_INFO, "TCP state: TCP_CLOSED");
    return 0;

到了 CLOSED 狀態就代表連線結束了, 不用做任何事


LISTEN 狀態程式碼片段:

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 狀態程式碼片段:

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 狀態程式碼片段:

    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 狀態程式碼片段:

    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

程式碼

增加 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 啟動後執行

build/tnetcat

在 nstack 內有設定伺服器聽在 10.0.0.2:10 ,可以使用 netcat 發送一些 TCP 封包進行測試

nc 10.0.0.2 10

隨意輸入一些字串後按下 Enter 即會送出封包

也可以使用瀏覽器連線至 http://10.0.0.2:10 即可看到瀏覽器的請求訊息,但由於不會回傳任何封包,所以瀏覽器過一段時間後會產生 timeout error