見出し画像

パケットをキャプチャして理解するTCP

こちらは、公式アドベントカレンダー2024_A IT技術関連トピック Day.16 の記事です。
公式アドベントカレンダー2024_B 仕事術・キャリア・体験記も毎日記事を公開していますので、ぜひあわせてご覧下さい。

★Day15のアドベントカレンダー記事
「AWS完全初学者・非エンジニアでも分かる!AWSはじめの一歩」(田代 崚)


はじめに


こんにちは、SHIFTの開発部門に所属している佐藤です。
私は普段、Webエンジニアとして業務を行っています。
しかし、Web技術の根幹を支える重要な要素である「TCP」について、理論的な理解が不十分である事に気が付きました。

そこで今回は、TCPを用いた通信の仕組みや流れを具体的に確認するため、簡単なエコーサーバーを作成し、その通信の動作を観察して理解を深めていきます。
これを通じて、3wayハンドシェイクやデータ送信、コネクションの終了といったTCPの基本動作について、実例を交えて学んでいきます。

事前知識の確認


3wayハンドシェイクとデータ送信

3wayハンドシェイクとは、TCPで用いられる通信確立の方法です。
以下が手順です(以下の 1~3 までが 3wayハンドシェイクです。)

  1. 接続をリクエスト(送信元:クライアント)(パケットのフラグ:SYN)
    通信を開始する側(クライアント)が相手(サーバ)に対して接続をリクエストします。

  2. 接続のリクエストを承認(サーバー)(SYN + ACK)
    リクエストを承認する旨として、クライアントから受信したパケットに対して返信します。

  3. 承認を確認(クライアント)(ACK)
    承認を確認した旨を伝えるため、再度サーバーにパケットを送信します。ここで通信が確立します。

  4. データを送信(クライアント)(PSH + ACK)
    PSHフラグは、データの送信を意味します。PSHとACKを同時に送信することで、データ送信が2wayで達成されます。これで遅延が抑えられます。

  5. データの受信を報告(サーバー)(ACK)

  6. コネクションの終了を要求(クライアント)(FIN + ACK)
    通信を終了することを意味するFINを送信します。又、既に受信済みの全てのデータに対する確認応答の為のACKも同時に送信します。

  7. コネクションの終了を確認(サーバー)(FIN + ACK)
    クライアントからのコネクション切断を確認した旨のACKを添えて、FINを返答します。

  8. コネクションを終了(クライアント)(ACK)
    サーバーがコネクションの切断を承認した事を確認した旨のACKを送信します。これでコネクションが終了します。

ソケット
通信に利用するIPアドレスとポートの組み合わせ。
通信に必要なAPIを提供するインターフェース。
通信のエンドポイント。

ストリーム
データの連続したバイト列としての転送。
TCPはストリーム指向の通信プロトコルです。

セッション
通信が成立している期間のこと。TCPは、セッションベースの通信を行います。

コネクション
データが行き来するための物理的あるいは論理的な接続のこと。

動作環境


  • Windows 11

  • WSL2 (Ubuntu 22.04.2 LTS)

  • rustc 1.71.0 (8ede3aae2 2023-07-12)

  • Wireshark 3.6.2

環境構築


cargo new echoserver

Cargo.toml


[package]
name = "echoserver"
version = "0.0.1"
edition = "2021"

[dependencies]
# 無し

実装


echoserver/src/main.rs

use std::error::Error;
use std::io::{Read, Write};
use std::net::TcpListener;
use std::{env, str, thread};

fn main() -> Result<(), Box<dyn Error>> {
    let args: Vec<String> = env::args().collect();
    let addr = &args[1];
    echo_server(addr)
}

fn echo_server(address: &str) -> Result<(), Box<dyn Error>> {
    let listener = TcpListener::bind(address)?; // 1
    loop {
        let (mut stream, client_address) = listener.accept()?; // 2
        println!("New client: {client_address:?}");

        thread::spawn(move || {
            let mut buf = [0u8; 1024]; // 3
            loop {
                match stream.read(&mut buf) { // 4
                    Ok(nbytes) => { 
                        if nbytes == 0 { // 5
                            return;
                        }
                        println!("Received: {}", str::from_utf8(&buf[..nbytes]).unwrap());
                        if let Err(e) = stream.write_all(&buf[..nbytes]) { // 6
                            eprintln!("{}", e);
                            return;
                        }
                    }
                    Err(e) => {
                        eprintln!("{}", e);
                        return;
                    }
                }
            }
        });
    }
}

実装の解説


1.サーバーを作成

let listener = TcpListener::bind(address)?; // 1

TCPソケットサーバーを作成します。
ここで言うTCPソケットサーバーとは、TCPでクライアントからの接続を待ち受け、クライアントとの間で双方向の通信を行うサーバーアプリケーションです。

2.ソケットを作成

let (mut stream, client_address) = listener.accept()?; // 2

クライアントの接続を待機します。
接続が確立されると、そのクライアントとの通信専用のソケット(通信のエンドポイント)とストリーム(データのフロー)が作成されます。

3.バッファーを作成

let mut buf = [0u8; 1024]; // 3

クライアントから受ったデータを一時保存するためのバッファーを作成します。

4.受信したデータを読む

match stream.read(&mut buf) { // 4

バッファーに格納された、クライアントから受信したデータを読み取ります。

5.接続を終了する

if nbytes == 0 { // 5

nbytes は、クライアントから受信したデータのバイト数です。これが空(==0)の時、クライアントとの接続を終了します。

6.クライアントにデータを送信する

if let Err(e) = stream.write_all(&buf[..nbytes]) { // 6

ストリームにデータを書き込み、送信します。

パケットをキャプチャする


Wiresharkをインストール

sudo apt install wireshark

Wireshark とは、オープンソースのパケット解析ツールです。
ネットワークトラフィックを監視します。
キャプチャしたパケットを解析・表示することで、ネットワークのトラブルシューティングやセキュリティ診断、プロトコルの調査などに利用されます。

Wiresharkを起動

sudo wireshark

今回はWSL2内でWSLgが稼働している前提です。なので、Xサーバー等の設定はここでは割愛します。

パケットキャプチャの設定

起動直後の画面

監視するインターフェースを選択します。Loopbackを選択して下さい。
ここでいうLoopbackは、ループバックインタフェースと呼ばれます。
コンピュータ内部でのローカルホストへの通信をキャプチャするためのインターフェースです。
つまり、コンピュータ上で動作しているプロセス間通信や、ローカルホストを経由して行われるネットワーク通信がこれを通過します。

監視中の画面

パケットをフィルタリングするための設定をします。

(tcp.srcport == 50000) or (tcp.srcport == 50001)

サーバーとクライアントのポートで絞り込みます。 パケットを監視

コネクションの確立

サーバーを起動

cargo run 127.0.0.1:50000

クライアントを起動

nc -p 50001 127.0.0.1 50000

-pオプションでnetcatを起動するポートを明示します。
これでエコーサーバー(ポート50000)とクライアント(ポート50001)間でコネクションが確立されました。

コネクション確立時の監視状況

1行目:クライアントがサーバーに接続をリクエスト。
2行目:サーバーがリクエストを承諾。
3行目:クライアントが承諾を確認。

パケットを送信する

クライアント側で、任意のテキストを送信します。
ここでは2回送信しています。

shota@shotamouse:~/study/tcp/mytcp$ nc -p 50001 127.0.0.1 50000
hoge
hoge
テキスト送信時の監視状況

赤枠1行目:クライアントがサーバーにデータを送信。
赤枠2行目:サーバーがデータ受信の旨をクライアントに報告。
赤枠3行目:クライアントがサーバーにデータを送信。
赤枠4行目:サーバーがデータ受信の旨をクライアントに報告。

通信を切断する

クライアント側(netcat)を遮断します。すると、コネクションが切断されます。

コネクション切断時の監視状況

赤枠1行目:クライアントがサーバーにコネクションの終了を要求。
赤枠2行目:サーバーが終了の要求を承諾。
赤枠3行目:クライアントが承諾を確認。コネクション終了。

まとめ


今回の記事では、TCPを用いた通信の仕組みを、Rustでのエコーサーバー作成とWiresharkを使用したパケットキャプチャを通して確認しました。

  • 3wayハンドシェイク、データ送信、コネクション終了の一連の流れを観察

  • ソケットやストリームといった基本概念を理解

  • Wiresharkで具体的な通信状況を可視化

これらを通じて、TCP通信の基礎を理論と実践の両面から学ぶことができました。
今後はこれを基にさらに複雑な通信プロトコルやセキュリティについても探求していきたいと考えています。

参考文献


Rustで始めるTCP自作入門


執筆者プロフィール:佐藤翔太
社会人4年目、SHIFT2年目。
前職では、SESとしてクラウド請求書発行システムの開発に従事。現在、PoCの開発を担当している。
日々答えの無い課題に四苦八苦しながら、技術による課題解決にやりがいを感じている。
趣味は楽器(ギター、ベース、ドラム、コントラバス)。
いつかオランダに住みたい。

SHIFTグループ公式アドベントカレンダー2024【A】 IT技術関連トピック Day17は「スクラムで性格診断を活用したミーティング活性化【ソーシャルスタイル編】」(伊藤 慶紀)

お問合せはお気軽に

SHIFTについて(コーポレートサイト)

SHIFTのサービスについて(サービスサイト)

SHIFTの導入事例

お役立ち資料はこちら

SHIFTの採用情報はこちら

PHOTO:UnsplashVisax