TCP/IPとは、インターネットや社内LANの通信を成立させるために使われるプロトコル(通信規約)の集合体です。「TCP/IP」という名前の単一のプロトコルがあるわけではなく、中心的な役割を担うTCPとIPの名前を取って、関連するプロトコル群をまとめてこう呼んでいます。この記事では図解だけで終わらせず、Ubuntu 24.04のDocker検証環境で実際に実行したコマンド出力を並べながら、4階層の役割・カプセル化・3ウェイハンドシェイク・TCPとUDPの違いまでを確かめていきます。
curl -sv -o /dev/null http://172.30.0.10/
* Trying 172.30.0.10:80...
* Connected to 172.30.0.10 (172.30.0.10) port 80
> GET / HTTP/1.1
> Host: 172.30.0.10
< HTTP/1.1 200 OK
この数行のやり取りの裏で、4つの階層がそれぞれ仕事をしています。本記事の中心は「1回のHTTPリクエストのパケットを16進ダンプし、4階層のヘッダが実際に何バイト目から始まっているかを数える」パートです。用語の暗記ではなく、自分の目で層の境界を確認できるところまで進みます。
TCP/IPとは|単一の規格ではなくプロトコルの集合体
TCP/IPは「TCP/IPプロトコルスイート」「インターネットプロトコルスイート」とも呼ばれます。スイート(suite)は「一式」という意味で、HTTP・DNS・TCP・UDP・IP・ICMP・ARPといった多数のプロトコルをまとめた総称がTCP/IPです。
初心者がつまずきやすいのがこの点です。「TCP/IPを設定する」という表現を見て単一の設定項目を探しても見つかりません。実際に設定するのはIPアドレス・デフォルトゲートウェイ・DNSサーバーといった個別の要素で、それらの集合をTCP/IPと呼んでいるだけです。
TCPとIPはそれぞれ何を担当しているのか
名前に採用された2つのプロトコルの役割を、ひと言で分けると次のようになります。
| プロトコル | 正式名称 | 担当 | やっていること |
|---|---|---|---|
IP | Internet Protocol | 「どこへ」 | 宛先IPアドレスを見て、パケットを目的のネットワークまで中継する。届いたかどうかは確認しない |
TCP | Transmission Control Protocol | 「どのように」 | 相手と接続を確立し、順序の入れ替わりや消失を検出して再送する |
郵便に例えるなら、IPは「宛先を見て配達する仕組み」、TCPは「書留にして、届かなければ出し直す仕組み」です。IPは配達を保証しないため、その上に載せるTCPが信頼性を担当する、という分業になっています。この「保証しない層の上に、保証する層を重ねる」構造こそが、次に説明する階層モデルの考え方です。
TCP/IPの4階層モデル|各層の役割と代表プロトコル
TCP/IPでは通信の機能を4つの階層に分けて整理します。下の層ほどハードウェアに近く、上の層ほどアプリケーションに近い位置づけです。
| 階層 | 名称 | 役割 | 代表プロトコル | 扱う住所 |
|---|---|---|---|---|
| 第4層 | アプリケーション層 | アプリ同士がやり取りするデータの形式を決める | HTTP、DNS、SSH、SMTP、FTP | URL・ドメイン名 |
| 第3層 | トランスポート層 | どのアプリ宛かを区別し、信頼性を担保する | TCP、UDP | ポート番号 |
| 第2層 | インターネット層 | ネットワークをまたいで宛先まで中継する | IP、ICMP | IPアドレス |
| 第1層 | ネットワークインターフェース層 | 直接つながっている機器同士でデータを渡す | Ethernet、ARP、PPP | MACアドレス |
重要なのは各層が「自分の1つ下の層に運搬を任せる」形になっていることです。HTTPは「TCPが確実に届けてくれる」前提で書かれ、TCPは「IPが相手のネットワークまで運んでくれる」前提で動きます。だからこそ、無線でも有線でも光ファイバーでも、下の層を差し替えるだけで上位の仕組みをそのまま使い回せます。
層と確認コマンドの対応表
Linuxでは、それぞれの層を担当するコマンドがきれいに分かれています。この対応を覚えておくと、後述するトラブル切り分けがそのまま実行できます。
| 階層 | 確認コマンド | 見えるもの |
|---|---|---|
| アプリケーション層 | curl -v、dig | HTTPヘッダ、名前解決の結果 |
| トランスポート層 | ss -tn、ss -tln | ポート番号、接続状態(ESTABLISHED / LISTEN) |
| インターネット層 | ip route、ping、traceroute | 経路、到達性、TTL |
| ネットワークインターフェース層 | ip -br link、ip neigh | リンク状態、MACアドレス、ARPテーブル |
4階層を実際に見る|層ごとにコマンドを実行する
ここからは実際の出力です。検証はDockerで作った隔離ネットワーク(172.30.0.0/24)で行い、クライアント(172.30.0.20)からnginxサーバー(172.30.0.10)へアクセスしています。同じ環境の作り方は記事後半に載せているので、手元で再現しながら読み進められます。
第1層:ネットワークインターフェース層をipコマンドで見る
まずは一番下の層です。ip -br linkでインターフェースとMACアドレスを確認します。-br(brief)を付けると1行1インターフェースで読みやすくなります。
ip -br link
lo UNKNOWN 00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP>
eth0@if262 UP 62:5a:32:27:a9:ef <BROADCAST,MULTICAST,UP,LOWER_UP>
eth0の62:5a:32:27:a9:efが自分のMACアドレスです。UPとLOWER_UPの両方が付いていれば、設定上も物理リンク上も有効になっています。LOWER_UPが無い場合はケーブル未接続やリンクダウンの疑いがあり、この時点で上位層を調べても意味がありません。
この層では、同じネットワーク内の相手をMACアドレスで指定します。IPアドレスとMACアドレスの対応表がARPテーブルで、ip neighで確認できます。
ip neigh
172.30.0.10 dev eth0 lladdr 9e:73:11:57:14:c5 REACHABLE
172.30.0.1 dev eth0 lladdr ee:80:17:91:a3:e3 REACHABLE
宛先サーバー172.30.0.10のMACアドレスが9e:73:11:57:14:c5だと分かりました。この値は後ほど16進ダンプの先頭に登場しますので、覚えておいてください。
第2層:インターネット層を経路とTTLで見る
IPは「宛先IPアドレスを見て、どのインターフェースからどこへ渡すか」を決めます。その判断材料がルーティングテーブルです。
ip route
default via 172.30.0.1 dev eth0
172.30.0.0/24 dev eth0 proto kernel scope link src 172.30.0.20
2行目は「172.30.0.0/24宛ならeth0から直接投げる」という意味です。今回の宛先172.30.0.10はこの範囲に入るので、ルーター経由ではなく直接届きます。範囲外の宛先はすべて1行目のdefault、つまりデフォルトゲートウェイ172.30.0.1へ送られます。
この層の到達性を確認するのがpingです。pingが使うICMPもインターネット層のプロトコルで、TCPやUDPを介さずIPの上に直接載ります。
ping -c 2 172.30.0.10
PING 172.30.0.10 (172.30.0.10) 56(84) bytes of data.
64 bytes from 172.30.0.10: icmp_seq=1 ttl=64 time=0.051 ms
64 bytes from 172.30.0.10: icmp_seq=2 ttl=64 time=0.088 ms
--- 172.30.0.10 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1021ms
ここに出ているttl=64がIPヘッダのTTL(Time To Live)です。TTLはルーターを1つ通過するたびに1ずつ減り、0になるとパケットは破棄されます。無限ループを防ぐ仕組みで、-tで意図的に小さくすると動作を確認できます。
ping -c 1 -t 1 1.1.1.1
PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data.
From 172.30.0.1 icmp_seq=1 Time to live exceeded
TTLを1にしたため、最初のルーター(172.30.0.1)でTTLが0になり、そこから「Time to live exceeded」が返ってきました。tracerouteはこのTTLを1、2、3……と増やしながら送ることで、経路上のルーターを1台ずつ炙り出しています。仕組みが分かると、tracerouteの出力の意味も一気に読みやすくなります。
第3層:トランスポート層をssコマンドで見る
IPアドレスだけでは「どのアプリ宛か」が決まりません。1台のサーバーでWebもSSHもメールも動いているからです。そこを区別するのがポート番号で、トランスポート層が担当します。
サーバー側で待ち受けているポートはss -tlnp(t=TCP、l=LISTEN、n=名前解決しない、p=プロセス表示)で確認します。
ss -tlnp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 127.0.0.11:46303 0.0.0.0:*
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=1,fd=6))
LISTEN 0 511 [::]:80 [::]:* users:(("nginx",pid=1,fd=7))
nginxが80番ポートで待っていることが分かります。0.0.0.0:80は「すべてのIPv4アドレス宛の80番を受け付ける」という意味で、[::]:80はIPv6側の同じ待ち受けです。次にクライアント側で、接続中のソケットを見てみます。
ss -tnp state established '( dport = :80 )'
Recv-Q Send-Q Local Address:Port Peer Address:Port Process
0 0 172.30.0.20:38764 172.30.0.10:80 users:(("nc",pid=2956,fd=3))
ここが理解のポイントです。1つのTCP接続は「送信元IP・送信元ポート・宛先IP・宛先ポート」の4点セットで一意に識別されます。サーバー側の80番は固定ですが、クライアント側のポート(この例では38764)は接続のたびにOSが自動で割り当てます。同じサーバーにブラウザのタブを複数開いても混線しないのは、このクライアントポートが毎回違うからです。
第4層:アプリケーション層をcurl -vで見る
一番上の層は、アプリ同士が交わすデータそのものです。curl -vを使うと、実際に流れたHTTPヘッダを見られます。>が送信、<が受信です。
curl -sv -o /dev/null http://172.30.0.10/
* Trying 172.30.0.10:80...
* Connected to 172.30.0.10 (172.30.0.10) port 80
> GET / HTTP/1.1
> Host: 172.30.0.10
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.31.3
< Content-Type: text/html
< Content-Length: 896
< Connection: keep-alive
注目したいのは* Connected to ...の行です。HTTPのデータを1バイトも送る前に、すでに「接続済み」になっていることが読み取れます。この裏で行われているのが、後述する3ウェイハンドシェイクです。
カプセル化を1パケットで見る|tcpdumpの16進ダンプ
ここが本記事の核心です。上位層のデータに下位層がヘッダを付けて包んでいくことをカプセル化と呼びます。図で説明されることが多い概念ですが、tcpdumpの-e(リンク層ヘッダを表示)と-XX(ヘッダ込みで16進ダンプ)を組み合わせると、実際のバイト列として観察できます。
tcpdump -i eth0 -nn -e -XX -c 4 'tcp port 80 and tcp[tcpflags] & tcp-push != 0'
別の端末からcurl http://172.30.0.10/を実行すると、GETリクエストのパケットが捕まります。
10:54:49.041693 62:5a:32:27:a9:ef > 9e:73:11:57:14:c5, ethertype IPv4 (0x0800), length 140: 172.30.0.20.48774 > 172.30.0.10.80: Flags [P.], seq 520556405:520556479, ack 4224207453, win 502, length 74: HTTP: GET / HTTP/1.1
0x0000: 9e73 1157 14c5 625a 3227 a9ef 0800 4500 .s.W..bZ2'....E.
0x0010: 007e 2063 4000 4006 c1bc ac1e 0014 ac1e .~.c@.@.........
0x0020: 000a be86 0050 1f07 0f75 fbc8 4a5d 8018 .....P...u..J]..
0x0030: 01f6 58cb 0000 0101 080a e18d f8a1 ab00 ..X.............
0x0040: 19f1 4745 5420 2f20 4854 5450 2f31 2e31 ..GET./.HTTP/1.1
0x0050: 0d0a 486f 7374 3a20 3137 322e 3330 2e30 ..Host:.172.30.0
0x0060: 2e31 300d 0a55 7365 722d 4167 656e 743a .10..User-Agent:
0x0070: 2063 7572 6c2f 382e 352e 300d 0a41 6363 .curl/8.5.0..Acc
0x0080: 6570 743a 202a 2f2a 0d0a 0d0a ept:.*/*....
この140バイトが、4階層すべてを含んだ1つのフレームです。バイト位置ごとに切り分けると、次のように4層がきれいに積み重なっています。
| バイト位置 | 長さ | 階層 | 内容 |
|---|---|---|---|
0x00〜0x0d | 14バイト | ネットワークインターフェース層 | Ethernetヘッダ |
0x0e〜0x21 | 20バイト | インターネット層 | IPヘッダ |
0x22〜0x41 | 32バイト | トランスポート層 | TCPヘッダ(オプション12バイト込み) |
0x42〜0x8b | 74バイト | アプリケーション層 | HTTPリクエスト本文 |
14 + 20 + 32 + 74 = 140となり、tcpdumpが表示したlength 140と一致します。実際に読んでみましょう。
先頭14バイト:Ethernetヘッダ(第1層)
9e73 1157 14c5 | 625a 3227 a9ef | 0800
| バイト列 | 意味 | 値 |
|---|---|---|
9e73 1157 14c5 | 宛先MACアドレス | 9e:73:11:57:14:c5 |
625a 3227 a9ef | 送信元MACアドレス | 62:5a:32:27:a9:ef |
0800 | EtherType | 中身はIPv4 |
先ほどip neighで見たARPテーブルの172.30.0.10 → 9e:73:11:57:14:c5が、そのまま宛先MACとして書き込まれています。ARPテーブルの中身が実際のパケットに使われていることが、これで確認できました。
続く20バイト:IPヘッダ(第2層)
4500 007e 2063 4000 4006 c1bc ac1e 0014 ac1e 000a
| バイト列 | 意味 | 読み方 |
|---|---|---|
45 | バージョンとヘッダ長 | 上位4ビット4=IPv4、下位4ビット5=5×4=20バイト |
007e | 全体長 | 126バイト(140 − Ethernet14) |
40 | TTL | 64。pingで見たttl=64と同じ初期値 |
06 | 上位プロトコル番号 | 6=TCP(UDPなら17) |
ac1e 0014 | 送信元IP | 172.30.0.20 |
ac1e 000a | 宛先IP | 172.30.0.10 |
acは10進で172、1eは30、14は20。つなげると172.30.0.20で、ip -br addrが表示した自分のIPアドレスと一致します。プロトコル番号06があるからこそ、受信側は「この先はTCPヘッダだ」と判断できます。層の橋渡しがバイト1つで行われているわけです。
続く32バイト:TCPヘッダ(第3層)
be86 0050 1f07 0f75 fbc8 4a5d 8018 01f6 58cb 0000 (+オプション12バイト)
| バイト列 | 意味 | 値 |
|---|---|---|
be86 | 送信元ポート | 48774(クライアントの一時ポート) |
0050 | 宛先ポート | 80(HTTP) |
1f07 0f75 | シーケンス番号 | 520556405 |
fbc8 4a5d | 確認応答番号 | 4224207453 |
8018 | ヘッダ長とフラグ | 上位4ビット8=8×4=32バイト、0x18=PSH+ACK |
01f6 | ウィンドウサイズ | 502 |
0050が16進の80、つまりHTTPのポート番号です。TCPヘッダは通常20バイトですが、ここでは8018の上位4ビットが8なので32バイト。差の12バイトがオプション領域(タイムスタンプなど)です。
残り74バイト:HTTPリクエスト(第4層)
14 + 20 + 32 = 66、16進で0x42。ダンプの0x0040の行を見ると、3バイト目(0x42)から4745 5420が始まっています。
0x0040: 19f1 4745 5420 2f20 4854 5450 2f31 2e31 ..GET./.HTTP/1.1
47=G、45=E、54=T。ここからがアプリケーション層のデータです。「HTTPのGETは、Ethernet・IP・TCPの3つのヘッダに包まれて67バイト目から始まる」という事実を、自分の目で数えて確認できました。これがカプセル化の実体です。
受信側は逆の手順を踏みます。Ethernetヘッダを外し、EtherType0800を見てIP層へ渡す。IPヘッダを外し、プロトコル番号06を見てTCP層へ渡す。TCPヘッダを外し、ポート番号0050を見てnginxへ渡す。これが非カプセル化(脱カプセル化)で、各層のヘッダに次の宛先が書いてあるからこそ成立しています。
3ウェイハンドシェイクをtcpdumpで確認する
TCPはデータを送る前に、必ず相手と接続を確立します。この手順が3ウェイハンドシェイクです。実際に捕まえてみます。
tcpdump -i eth0 -nn -c 12 'tcp port 80'
172.30.0.20.41580 > 172.30.0.10.80: Flags [S], seq 192987365, win 64240, length 0
172.30.0.10.80 > 172.30.0.20.41580: Flags [S.], seq 1405869039, ack 192987366, win 65160, length 0
172.30.0.20.41580 > 172.30.0.10.80: Flags [.], ack 1, win 502, length 0
172.30.0.20.41580 > 172.30.0.10.80: Flags [P.], seq 1:75, ack 1, length 74: HTTP: GET / HTTP/1.1
172.30.0.10.80 > 172.30.0.20.41580: Flags [.], ack 75, win 509, length 0
172.30.0.10.80 > 172.30.0.20.41580: Flags [P.], seq 1:239, ack 75, length 238: HTTP: HTTP/1.1 200 OK
tcpdumpのフラグ表記は略号なので、対応を押さえておくと読めるようになります。.(ドット)はACKを表します。
| 順番 | 方向 | フラグ | 意味 |
|---|---|---|---|
| 1 | クライアント → サーバー | [S] | SYN。「接続したい」と申し込む |
| 2 | サーバー → クライアント | [S.] | SYN+ACK。「了解、こちらも接続したい」 |
| 3 | クライアント → サーバー | [.] | ACK。「了解」。ここで接続確立 |
| 4 | クライアント → サーバー | [P.] | PSH+ACK。ようやくHTTPのGETを送信 |
HTTPのデータが流れるのは4番目のパケットからです。それ以前の3往復はすべて「接続の準備」に費やされています。curl -vで見た* Connected to ...は、この3番目が終わった瞬間を指しています。
また、1番目のSYNのシーケンス番号は192987365、2番目の応答の確認応答番号は192987366と1つ増えています。「受け取った番号+1を返す」ことで、どこまで届いたかを相手に伝えているのがTCPの基本動作です。
通信の終わりにはFIN([F.])が使われます。同じキャプチャの末尾を見ると、切断も対称的に行われているのが分かります。
172.30.0.20.41580 > 172.30.0.10.80: Flags [F.], seq 75, ack 1135, length 0
172.30.0.10.80 > 172.30.0.20.41580: Flags [F.], seq 1135, ack 76, length 0
172.30.0.20.41580 > 172.30.0.10.80: Flags [.], ack 1136, length 0
この「接続確立に3パケット、切断にも数パケット」というオーバーヘッドが、次に見るUDPとの決定的な違いになります。
TCPとUDPの違いを実測する|パケットロス20%環境での挙動
「TCPは信頼性が高い、UDPは速いが保証しない」という説明はよく見かけますが、どう違うのかを実際に測ってみます。tcコマンドのnetemで意図的に20%のパケットロスを発生させ、同じ転送を比較しました。
tc qdisc add dev eth0 root netem loss 20%
TCPの場合:時間は延びるが、データは完全に届く
5MBのファイルをHTTP(TCP)でダウンロードし、ロスなし・ロス20%それぞれ3回ずつ計測しました。
curl -s -o /tmp/b.bin -w "time=%{time_total}s size=%{size_download} http=%{http_code}\n" http://172.30.0.10/big.bin
| 条件 | 所要時間(3回) | 受信サイズ | HTTPステータス |
|---|---|---|---|
| ロスなし | 0.0054s / 0.0064s / 0.0060s | 5,242,880バイト | 200 |
| ロス20% | 0.0086s / 0.0099s / 0.0137s | 5,242,880バイト | 200 |
所要時間は約1.6〜2.3倍に延びましたが、受信サイズは1バイトも欠けていません。念のためハッシュ値も照合しました。
md5sum /tmp/a.bin /tmp/b.bin
e87fd72c8c41763e4c3ec6190cd199ad /tmp/a.bin
e87fd72c8c41763e4c3ec6190cd199ad /tmp/b.bin
ロスなしのファイルと20%ロス下のファイルのMD5が完全に一致しました。TCPは失われたパケットを自動的に再送するため、パケットロスは「データの欠損」ではなく「所要時間の増加」として現れます。アプリケーション側(curl)は再送が起きたことすら知りません。
UDPの場合:失われたデータは戻らない
同じ20%ロス環境で、UDPを使って200個のデータグラムを送りました。nc -uでUDPモードにします。
# 受信側
nc -u -l -p 9999 > /tmp/udp_recv.txt
# 送信側(200行を送る)
for i in $(seq 1 200); do echo "line$i"; sleep 0.005; done | nc -u -w 3 172.30.0.10 9999
wc -l < /tmp/udp_recv.txt
162
200個送って届いたのは162個。38個が消えたまま、再送も通知も行われませんでした。どの行が失われたかを確認すると、歯抜けになっているのが分かります。
for i in 1 2 3 4 5 6 7 8 9 10; do grep -qx "line$i" /tmp/udp_recv.txt || echo " line$i LOST"; done
line2 LOST
line9 LOST
送信側は最後まで正常終了しており、エラーも出ていません。UDPには「届いたか確認する仕組み」自体が無いためです。音声通話や動画配信でUDPが使われるのは、0.5秒前の音声を再送してもらっても遅すぎて使い道がないからで、欠落を許容する代わりに遅延を抑える設計になっています。
パケット数の差も明確です。DNS問い合わせ(UDP)をキャプチャすると、往復たった2パケットで完結しています。
tcpdump -i eth0 -nn 'udp port 53'
172.30.0.20.42343 > 1.1.1.1.53: 10341+ [1au] A? www.iana.org. (53)
1.1.1.1.53 > 172.30.0.20.42343: 10341$ 3/0/1 CNAME www.iana.org.cdn.cloudflare.net., A 104.18.24.232, A 104.18.25.232 (118)
TCPなら接続確立だけで3パケット必要でしたが、UDPは質問と回答の2パケットだけ。DNSのような「小さく速い問い合わせ」にUDPが向いている理由が、パケット数として現れています。TCPとUDPの使い分けはTCPとUDPの違いを実測で解説|パケット数・ヘッダ・確認コマンドでも整理しています。
TCP/IPとOSI参照モデルの違い
ネットワークの学習では、TCP/IPの4階層とは別に「OSI参照モデル」の7階層が登場します。両者は競合する規格ではなく、同じ通信を違う粒度で切り分けた地図だと考えると混乱しません。
| OSI参照モデル(7階層) | TCP/IPモデル(4階層) | 代表プロトコル |
|---|---|---|
| 第7層 アプリケーション層 第6層 プレゼンテーション層 第5層 セッション層 | アプリケーション層 | HTTP、DNS、SSH |
| 第4層 トランスポート層 | トランスポート層 | TCP、UDP |
| 第3層 ネットワーク層 | インターネット層 | IP、ICMP |
| 第2層 データリンク層 第1層 物理層 | ネットワークインターフェース層 | Ethernet、Wi-Fi |
| 観点 | OSI参照モデル | TCP/IPモデル |
|---|---|---|
| 階層数 | 7 | 4 |
| 位置づけ | 理論・教育向けの整理 | 実装され動いている枠組み |
| 主な用途 | 用語の共通化、資格試験 | 実際のインターネット通信 |
実務では「L3スイッチ」「L7ロードバランサー」のようにOSIの層番号で会話し、動いている実体はTCP/IPという使い分けが一般的です。なおOSIの層番号で言う「L4」はトランスポート層ですが、TCP/IPモデルでトランスポート層は第3層にあたります。数字だけで話すと食い違うため、慣れるまでは層の名前で言うほうが安全です。
ポート番号とプロトコルの対応を確認する
「HTTPは80番」といった対応は、Linuxでは/etc/servicesに一覧として置かれています。
grep -E '^(ssh|http|https|domain|ftp|smtp)\s' /etc/services
ftp 21/tcp
ssh 22/tcp # SSH Remote Login Protocol
smtp 25/tcp mail
domain 53/tcp # Domain Name Server
domain 53/udp
http 80/tcp www # WorldWideWeb HTTP
https 443/tcp # http protocol over TLS/SSL
https 443/udp # HTTP/3
2点、実務的に押さえておきたい情報が読み取れます。1つはdomain(DNS)が53/tcpと53/udpの両方に登録されていること。DNSは通常UDPを使いますが、応答が大きい場合やゾーン転送ではTCPに切り替わります。ファイアウォールでUDP 53しか開けていないと、特定の問い合わせだけ失敗する原因になります。
もう1つはhttps 443/udp # HTTP/3の行です。HTTP/3はTCPではなくUDP上のQUICを使います。「Webは必ずTCP」という前提はすでに崩れているため、443/udpを塞いだままだとHTTP/3が使えず、TCPへのフォールバックで余計な遅延が生じることがあります。
なお/etc/servicesはあくまで名前とポートの対応表であり、ここに書いたからといってサービスが動くわけではありません。実際に何番で待ち受けているかはss -tlnpで確認します。詳しい手順はLinuxのポート確認方法とポート競合の解消手順にまとめています。
通信トラブルをTCP/IPの層で切り分ける
階層モデルを覚える最大の実用的メリットが、これです。「繋がらない」と言われたとき、下の層から順に潰していけば、原因の範囲が機械的に絞れます。上から闇雲に試すより圧倒的に速く終わります。
| 順 | 階層 | 確認コマンド | 失敗したときに疑うもの |
|---|---|---|---|
| 1 | ネットワークインターフェース層 | ip -br link | ケーブル未接続、インターフェースがダウン、ドライバ |
| 2 | インターネット層 | ip -br addr / ip route | IPアドレス未設定、サブネット誤り、デフォルトGW未設定 |
| 3 | インターネット層 | ping <宛先IP> | 経路の断絶、ICMPの遮断、相手ホストの停止 |
| 4 | アプリケーション層(名前解決) | dig <ドメイン> | DNSサーバー設定、レコード不備 |
| 5 | トランスポート層 | ss -tlnp(サーバー側) | サービス未起動、待ち受けIPが127.0.0.1のみ |
| 6 | トランスポート層 | nc -vz <宛先> <ポート> | ファイアウォール、セキュリティグループ |
| 7 | アプリケーション層 | curl -v | アプリの設定、証明書、仮想ホスト |
とくに間違えやすいのが5番です。pingは通るのにサービスに繋がらない場合、サーバーが127.0.0.1:80でしか待ち受けていないことがよくあります。先ほどのss -tlnpの出力が0.0.0.0:80だったのはすべてのアドレスで待ち受けている状態で、これが127.0.0.1:80だと外部からは一切繋がりません。ping(インターネット層)は成功するのに接続(トランスポート層)が失敗する典型例です。
6番のnc -vzは、実際にTCP接続だけを試して切断するコマンドです。開いているポートと閉じているポートでは、次のように応答が変わります。
nc -vz 172.30.0.10 80
nc -vz -w 2 172.30.0.10 8080
Connection to 172.30.0.10 80 port [tcp/http] succeeded!
nc: connect to 172.30.0.10 port 8080 (tcp) failed: Connection refused
ここで区別したいのが「Connection refused」と「タイムアウト(無応答)」の違いです。Connection refusedはパケットが相手ホストまで届いたうえでRSTが返ってきた状態で、経路は生きておりサービスが起動していないことを意味します。一方、応答が無いまま待たされる場合はファイアウォールがパケットを破棄している可能性が高くなります。ここで失敗するならファイアウォール、成功するのにアプリが応答しないなら7番のアプリ設定、と切り分けられます。各コマンドの詳細は接続不良の原因を特定!ネットワークトラブルシュートの基本コマンド一覧を参照してください。
検証環境の作り方|Dockerで安全に試す
本記事の出力は、すべて次のDocker環境で取得しました。隔離されたネットワークなので、実際のLANや業務環境に影響を与えずにパケットキャプチャやパケットロスの実験ができます。
# 専用ネットワークを作る
docker network create --subnet 172.30.0.0/24 tcpip-lab
# Webサーバー役
docker run -d --name lab-web --network tcpip-lab --ip 172.30.0.10 nginx:alpine
# クライアント役(キャプチャと帯域制御のため権限を付与)
docker run -d --name lab-cli --network tcpip-lab --ip 172.30.0.20 \
--cap-add=NET_RAW --cap-add=NET_ADMIN ubuntu:24.04 sleep 3600
# 必要なコマンドを入れる
docker exec lab-cli sh -c 'apt-get update -qq && \
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
tcpdump curl iproute2 iputils-ping dnsutils netcat-openbsd netbase'
# クライアントに入る
docker exec -it lab-cli bash
--cap-add=NET_RAWはtcpdumpでパケットをキャプチャするため、--cap-add=NET_ADMINはtcでパケットロスを発生させるために必要です。片付けは次のとおりです。
docker rm -f lab-web lab-cli
docker network rm tcpip-lab
実機で試す場合、tcpdumpにはroot権限(またはCAP_NET_RAW)が必要です。本番サーバーでのtc qdisc add ... netem lossは実際に通信を壊すため、絶対に実行しないでください。検証は必ず使い捨ての環境で行い、終わったらtc qdisc del dev eth0 rootで元に戻します。
よくある質問
Q. TCP/IPというプロトコルが1つ存在するのですか?
いいえ。TCPとIPを中心としたプロトコル群の総称です。実体はHTTP・DNS・TCP・UDP・IP・ICMP・ARPなど多数のプロトコルの集まりで、「TCP/IPという名前の単独の規格」は存在しません。
Q. TCP/IPは4階層ですか、5階層ですか?
文献によって異なります。もっとも一般的なのは本記事で扱った4階層ですが、最下層を「データリンク層」と「物理層」に分けて5階層とする教科書もあります。層の数より、どの機能がどの層に属するかの理解が重要です。資格試験ではどちらのモデルを前提にしているか、問題文を確認してください。
Q. OSI参照モデルとどちらを覚えればよいですか?
両方に触れておくのが結局は近道です。実際に動いているのはTCP/IPですが、現場の会話や機器の仕様書では「L3」「L7」というOSIの層番号が使われます。TCP/IPで動作を理解し、OSIで用語を合わせるという役割分担で捉えるとよいでしょう。
Q. IPアドレスがあるのに、なぜMACアドレスも必要なのですか?
担当する範囲が違うためです。IPアドレスは「最終的な宛先」を示し、MACアドレスは「次に渡す隣の機器」を示します。パケットがルーターを通過するたびに宛先IPアドレスは変わりませんが、宛先MACアドレスは書き換えられます。IPが目的地の住所、MACが区間ごとの受け渡し相手だと考えると分かりやすくなります。
Q. tcpdumpが「permission denied」で実行できません
パケットキャプチャにはroot権限(CAP_NET_RAW)が必要です。sudo tcpdump -i eth0のように実行してください。コンテナ内で使う場合は起動時に--cap-add=NET_RAWを付けます。
Q. pingは通るのにブラウザで開けません
層が違うためです。pingが確認しているのはインターネット層までで、ポートを使うトランスポート層より上は一切検証していません。サーバー側でss -tlnpを実行してサービスが待ち受けているか、クライアントからnc -vz <宛先> 80でポートに到達できるかを順に確認してください。ファイアウォール(ufwやiptables)でポートが塞がれているケースも多く見られます。
Q. ネットワークが遅いのはTCPのせいですか?
パケットロスが起きている場合、TCPの再送によって「遅い」という形で症状が出ます。本記事の実測でも、20%のロス下で所要時間が約2倍になりました。データが壊れず遅くなるだけなのはTCPが正しく仕事をしている証拠で、真の原因は下位層のロスにあります。pingのpacket loss率を確認するのが第一歩です。
Q. IPv6でも同じ4階層モデルですか?
はい。階層構造はそのままで、インターネット層のプロトコルがIPv4からIPv6に置き換わります。TCPやUDP、HTTPといった上位層はほぼ変更なく使えます。これが階層化の利点で、1つの層を差し替えても上下の層に影響しない設計になっています。
まとめ
- TCP/IPは単一の規格ではなく、TCPとIPを中心としたプロトコルの集合体
- IPは「どこへ」(IPアドレス)、TCPは「どのように」(確実に)を担当する分業になっている
- 4階層はそれぞれ
ip -br link/ip route/ss -tn/curl -vで目視確認できる - カプセル化は
tcpdump -e -XXの16進ダンプで、14+20+32バイトのヘッダとして実際に数えられる - 3ウェイハンドシェイクにより、HTTPのデータが流れるのは4パケット目から
- 20%のパケットロス下でも、TCPはMD5が一致する完全なデータを届ける(時間は約2倍)。同条件のUDPは200個中162個しか届かない
- 「繋がらない」ときは下の層から
ip link→ip route→ping→ss -tlnp→curl -vの順に切り分ける
階層モデルは暗記するものではなく、トラブル時に「どこを見ればいいか」を教えてくれる地図です。まずは手元のサーバーでip -br addrとss -tlnpを実行し、自分の環境がどの層でどう見えているかを確かめるところから始めてみてください。
関連記事
- TCPとUDPの違いを実測で解説|パケット数・ヘッダ・確認コマンド
- tcpdump – ネットワークパケットをキャプチャ・解析するコマンド
- ss – ソケットの状態を表示するコマンド
- ip – ネットワーク設定と管理を行うコマンド
- arp – ARPテーブルを表示・操作するコマンド
- ping – ネットワーク疎通を確認するコマンド
- Linuxのtracerouteコマンド使い方|経路調査とオプション一覧
- Linuxのポート確認方法とポート競合の解消手順|netstat・lsof・ss完全ガイド
- 接続不良の原因を特定!ネットワークトラブルシュートの基本コマンド一覧
- DNSサーバーとは?仕組み・種類・確認方法をコマンドで解説
- FQDNとは?ドメイン名・ホスト名との違いと確認コマンドを実例で解説
- LinuxでIPアドレスを確認する方法|ip・hostname・nmcliコマンド
- linux コマンド一覧|用途別に整理した初心者向け完全ガイド

コメント