Rustで始めるTCP自作入門
準備
ローカル環境はホストがIntel MacでOSがMonterey。スクリプトの実行はDockerでDebian環境を作成して実行する。
RustのVersion
code:sh
% rustc --version
rustc 1.61.0 (fe5b13d68 2022-05-18)
エコーサーバーを作る
リスニングソケット
クライアントからのコネクション確立要求を待機するソケット
エコーサーバー実行中に1つのみ生成される
接続済みソケット
新しくスレッドが確立され、クライアントとのやりとりを行うソケット
新たなコネクションごとに1つ生成されデータの送受信を行う
ソケット
通信のエンドポイントとなるオブジェクト。送受信対象とするプロトコルを選択できる。
ストリームソケット(TCP+IP+データリンク+物理)
データグラムソケット(UDP+IP+データリンク+物理)
生ソケット(IP+データリンク+物理)
パケットソケット(物理)
TCPやUDPのトランスポートプロトコルを実装する場合は生ソケットを使う。このソケットにデータの書き込みを行うだけでIPヘッダを内部で付与して宛先に送信してくれる。抽象化されてて便利。
作業用のLinux環境を作る
MacOS上ではやりにくいので壊してもいい作業環境をDockerで作成する。
Dockerfile
code:Dockerfile
FROM rust:1.62-buster
RUN apt-get update && \
apt-get -y install sudo iptables ethtool curl less git bridge-utils && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
WORKDIR /workspace
docker-compose.yml
code:docker-compose.yml
version: "3.8"
volumes:
db-store:
services:
app:
container_name: app
build:
context: .
dockerfile: Dockerfile
command: /bin/sh -c "sudo ./setup.sh && tail -f /dev/null"
tty: true
stdin_open: true
privileged: true
volumes:
- ./:/workspace
networks:
- dev-app-net
networks:
dev-app-net:
driver: bridge
ハマりどころ
+ sudo ip netns add host1 mount --make-shared /var/run/netns failed: Operation not permitted
priviledgedオプションを有効にしないとsetup.shが実行できない。でもこれ安易に設定していいのかわからん...
mount自体はpriviledgedを設定してもホスト側に大きな問題はでなそうだったので有効にする
Dockerコンテナについて復習
setup.sh
code:setup.sh
# !/bin/bash
set -eux
sudo ip netns add host1
sudo ip netns add router
sudo ip netns add host2
sudo ip link add name host1-veth1 type veth peer name router-veth1
sudo ip link add name router-veth2 type veth peer name host2-veth1
sudo ip link set host1-veth1 netns host1
sudo ip link set router-veth1 netns router
sudo ip link set router-veth2 netns router
sudo ip link set host2-veth1 netns host2
sudo ip netns exec host1 ip addr add 10.0.0.1/24 dev host1-veth1
sudo ip netns exec router ip addr add 10.0.0.254/24 dev router-veth1
sudo ip netns exec router ip addr add 10.0.1.254/24 dev router-veth2
sudo ip netns exec host2 ip addr add 10.0.1.1/24 dev host2-veth1
sudo ip netns exec host1 ip link set host1-veth1 up
sudo ip netns exec router ip link set router-veth1 up
sudo ip netns exec router ip link set router-veth2 up
sudo ip netns exec host2 ip link set host2-veth1 up
sudo ip netns exec host1 ip link set lo up
sudo ip netns exec router ip link set lo up
sudo ip netns exec host2 ip link set lo up
sudo ip netns exec host1 ip route add 0.0.0.0/0 via 10.0.0.254
sudo ip netns exec host2 ip route add 0.0.0.0/0 via 10.0.1.254
sudo ip netns exec router sysctl -w net.ipv4.ip_forward=1
# drop RST
sudo ip netns exec host1 sudo iptables -A OUTPUT -p tcp --tcp-flags RST RST -j DROP
sudo ip netns exec host2 sudo iptables -A OUTPUT -p tcp --tcp-flags RST RST -j DROP
# turn off checksum offloading
sudo ip netns exec host2 sudo ethtool -K host2-veth1 tx off
sudo ip netns exec host1 sudo ethtool -K host1-veth1 tx off
https://scrapbox.io/files/62e89152de50c200234b53b9.png
何をやっているか->LinuxのNetwork Namespaceを作成している。下記を読むと大体理解できる。
この本を買うともっと理解できそう
TCPセグメントを生ソケットに送信する
echoclientを作る(host1からリクエストを送信する用)
TCPセグメント(IPパケットのペイロード)を生ソケットへ書き込む実装
packet/socket/tcp/tcpflagsのファイルを実装する
10.0.0.1 -> 10.0.1.1 へのSYNの送信
TCPヘッダーが空なので不正なセグメントと認識され10.0.1.1からの返信は無い
試す。tcpdumpにSYNの送信のログだけが表示されるはず
code:sh
sudo ip netns exec host2 nc -l 10.0.1.1 4000
sudo ip netns exec host1 tcpdump -l
sudo ip netns exec host1 ./target/debug/examples/echoclient 10.0.1.1 40000
スリーウェイハンドシェイク
アクティブオープン
コネクション
送信元から宛先へ「切れ目のない1つの大きなデータ」を「どこまで送信したか」「どこまで受信したか」「送信したものがどこまで相手に届いたか」などの状態を保持し管理する必要がある。
2つのソケット間に保持される状態 = プログラム上におけるコネクション、に対応する
https://scrapbox.io/files/62ea84b1d6154b001dc9d786.png
RFC793をもとにまずはTCPセグメントを実装する
https://scrapbox.io/files/62ea8a1aae2172001f167f05.png
ソケットへコネクションの状態を追加する
send_param,recv_param
どこまで送信したかや確認応答番号の情報を保持
status
TCPソケットが管理するコネクションの状態の保持
TCPヘッダーの構造体を定義
sender/receiver/observerの3スレッドを立てる。
実際に通信してみる
SYNを送ってもACKが返ってこない...なぜ...
とりあえず原因不明だが写経間違いもなさそうだし一旦先に進んでみるか
-pでport指定すると動いた。本書ではsudo ip netns exec host2 nc -l 10.0.1.1 40000となっているので40000の前に-pを付与すると動く。誤植かも。
code:sh
// host2(別タブ) -l(リッスンモード。つまりサーバー状態。) -p(ポート指定)
$ sudo ip netns exec host2 nc -l 10.0.1.1 -p 40000
// host1(別タブ)
$ sudo ip netns exec host2 ./target/debug/examples/echoclient 10.0.0.1 40000
パッシブオープン
https://scrapbox.io/files/62ef5baad3d0d90023aebcf4.png
SYNを受け取るソケットを生成
ACK | SYNを返す
SYNRCVDを受けて生成済みのソケットでコネクションを確立する
code:sh
// host1(別タブ)
$ sudo ip netns exec host1 ./target/debug/examples/echoserver 10.0.0.1 40000
// host2(別タブ)
$ sudo ip netns exec host2 ./target/debug/examples/echoclient 10.0.0.1 40000
ペイロードの送信
再送制御。再送時間(RTT)は動的決定するのが仕様だがここでは定数秒でタイムアウトさせている。
スライディングウィンドウ。送れるところまで送って、ウィンドウサイズ一杯になったらブロック、ACKが返ってきたらまた送る感じ。
ペイロードの受信
バッファを用意して受信したペイロードを一時保存。途中で欠けたりしてないかチェックする。
コネクションを閉じる
FIN|ACK を受けて処理。アクティブクローズ(ホスト側からFIN ACKを送って切断。ctrl+cみたいなやつ)とパッシブクローズ(FINを受けてクローズするやつ)。