TCP ハンドシェイクを3行で捕まえる
このラボはVM上で動きます
ループバック(lo)上でncを使ってサーバーとクライアントを作り、tcpdumpでその間を捕まえます。外部のネットワークが不要なので、捕まえるパケットはすべて自分のものです。
目標
接続が開いて閉じる過程をSYN・SYN-ACK・ACK・FINのフラグで読み取り、閉じたポートと開いたポートの違い、TIME-WAIT、エフェメラルポート、そしてポートを掴んでいるプロセスの探し方を身につけます。
なぜ重要なのか
「接続できない」という1つの文に、異なる3つのことが混ざっています。SYNを送ったのにRSTが返ってくるなら、相手は生きていてポートだけが閉じており、何も返ってこないなら、ファイアウォールが捨てているか経路がなく、SYN-ACKまで来たのにデータが来ないなら、アプリケーションの問題です。この3つはユーザーには同じく「つながらない」と見えますが、直す人はまったく違います。フラグを読めるようになれば、最初の30秒でその3つが分かれます。
ステップ
nc -lk 127.0.0.1 9000のサーバーをバックグラウンドで起動し(受け取ったデータは/root/tcp/recv.txtへ)、ss -ltnの出力を/root/tcp/listen.txtに保存してください。loのポート9000をtcpdumpで捕まえながらhelloを1行送って、ハンドシェイクと終了を含むキャプチャを/root/tcp/handshake.txtに保存してください。- 誰も待ち受けていないポート9001への接続を試み、エラーメッセージとそのときのキャプチャ(RSTが見える)を
/root/tcp/refused.txtにまとめて保存してください。 - ポート9000にもう一度接続して閉じた直後に、
ss -tanのTIME-WAITの行を/root/tcp/timewait.txtに保存してください。 - エフェメラルポートの範囲と、ステップ2のキャプチャでクライアントが使ったポートを、
/root/tcp/ports.txtにrange=・client_port=の2行で保存してください。 - Pythonで
127.0.0.1:9000にもう一度bindを試み、そのエラーを/root/tcp/inuse.txtに保存してください。 ss -ltnpでポート9000を掴んでいるプロセスを探し、その行を/root/tcp/owner.txtに保存してください。/root/tcp/report.mdに、client_port=・server_port=の2行と、3段階のハンドシェイクとTIME-WAITがなぜあるのかを書いてください。
参考
- サーバーは
setsid nc -lk 127.0.0.1 9000 > /root/tcp/recv.txt 2>&1 < /dev/null &のように起動してください。-kは、接続が終わっても待ち受けを続けるオプションです。 - キャプチャ:
timeout 6 tcpdump -i lo -nn -l 'tcp port 9000' > 파일 2>&1 &を先に起動しておき、echo hello | nc -q 1 127.0.0.1 9000を実行します(プレースホルダーはファイル名です)。 - フラグ:
[S]はSYN、[S.]はSYN+ACK、[.]はACK、[P.]はデータ、[F.]はFIN、[R.]はRSTです。 - エフェメラルポートの範囲:
sysctl net.ipv4.ip_local_port_range。 - よくある間違い: サーバーを起動して、標準出力をターミナルにつないだままにすることです。ほかのコマンドの出力が混ざってファイルが絡まるので、必ずファイルへリダイレクトしてください。
待ち受けソケット
nc -lk 127.0.0.1 9000のサーバーをバックグラウンドで起動し(受け取ったデータは /root/tcp/recv.txt へ)、ss -ltnの出力を /root/tcp/listen.txt に保存してください。
setsid nc -lk 127.0.0.1 9000 > /root/tcp/recv.txt 2>&1 < /dev/null &。ss -ltnは、TCP(-t)の待ち受けソケット(-l)を数値(-n)で表示します。127.0.0.1:9000がLISTENと表示されている必要があります。
3行のハンドシェイク
loのポート9000をtcpdumpで捕まえながらhelloを1行送って、ハンドシェイクと終了を含むキャプチャを /root/tcp/handshake.txt に保存してください。
先にキャプチャを起動します: timeout 6 tcpdump -i lo -nn -l 'tcp port 9000' > /root/tcp/handshake.txt 2>&1 &。1秒後にecho hello | nc -q 1 127.0.0.1 9000を実行します。ファイルに、[S]、[S.]、[.]の順で始まり[F.]で終わる行がある必要があり、recv.txtにはhelloが入ります。
閉じたポートはRSTで答える
誰も待ち受けていないポート9001への接続を試み、エラーメッセージとそのときのキャプチャ(RSTが見える)を /root/tcp/refused.txt にまとめて保存してください。
キャプチャtimeout 5 tcpdump -i lo -nn -l 'tcp port 9001' >> /root/tcp/refused.txt 2>&1 &を起動し、nc -zv -w 1 127.0.0.1 9001 >> /root/tcp/refused.txt 2>&1を実行します。ファイルにConnection refusedとFlags [R.]の両方がある必要があります。
TIME-WAIT
ポート9000にもう一度接続して閉じた直後に、ss -tanのTIME-WAITの行を /root/tcp/timewait.txt に保存してください。
echo again | nc -q 1 127.0.0.1 9000の直後にss -tan | grep TIME-WAIT > /root/tcp/timewait.txtを実行します。先に閉じた側(クライアント)に残ります。60秒後に消えるので、すぐに確認してください。
エフェメラルポート
エフェメラルポートの範囲と、ステップ2のキャプチャでクライアントが使ったポートを、/root/tcp/ports.txt にrange=・client_port=の2行で保存してください。
範囲はsysctl -n net.ipv4.ip_local_port_range(2つの数字)です。クライアントのポートは、handshake.txtの[S]の行にある127.0.0.1.<포트> >のポートです(プレースホルダーはポート番号です)。その値が範囲の中にある必要があります。
ポートは1つしか掴めない
Pythonで127.0.0.1:9000にもう一度bindを試み、どこにbindしたかの1行とそのエラーを /root/tcp/inuse.txt にまとめて保存してください。
{ echo 'bind 127.0.0.1:9000'; python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1", 9000))' 2>&1; } > /root/tcp/inuse.txt。Address already in useが出る必要があります。同じアドレス・ポートは、1つのソケットしか持てません。どのポートで起きた衝突かがファイルに残っている必要があります。別のポートの衝突メッセージは、このステップの答えではありません。
誰が掴んでいるか
ss -ltnpでポート9000を掴んでいるプロセスを探し、その行を /root/tcp/owner.txt に保存してください。
ss -ltnp | grep ':9000 '。-pがusers:(("nc",pid=…))を付け足してくれます。rootでなければ、他のユーザーのプロセスは見えません。
学んだこと
/root/tcp/report.md に、client_port=・server_port=の2行と、3段階のハンドシェイクとTIME-WAITがなぜあるのかを書いてください。
client_portはステップ5の値、server_portは9000です。本文にSYNとTIME-WAITという語を含める必要があります。