⏩
ネットワークの観察 - 第5回 SSHポートフォワーディングを解析する
はじめに
Mac(ホスト)とDocker環境でSSHポートフォワーディングの3種類(ローカル-L、リモート-R、ダイナミック-D)を実験・観察する手順です。
マシンスペック
MacBook Air M2 arm64
Docker上で実施
準備
必要なツール
# sshpass(パスワード認証用)
brew install hudochenkov/sshpass/sshpass
# Wireshark(任意・パケット解析用)
brew install --cask wireshark
プロジェクト構成
ssh-portforward-lab/
├── docker-compose.yml
├── server/
│ └── Dockerfile
├── client/
│ └── Dockerfile
└── web/
└── Dockerfile
server/Dockerfile(SSHサーバ)
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
openssh-server tcpdump iproute2 procps net-tools vim \
&& mkdir -p /var/run/sshd \
&& useradd -m test && echo 'test:test' | chpasswd \
&& ssh-keygen -A \
&& echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config \
&& echo 'PermitRootLogin no' >> /etc/ssh/sshd_config \
&& echo 'GatewayPorts yes' >> /etc/ssh/sshd_config
WORKDIR /work
EXPOSE 22
CMD ["/usr/sbin/sshd","-D","-e"]
client/Dockerfile(SSHクライアント)
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
openssh-client curl netcat-traditional tcpdump bash vim
CMD ["sleep","infinity"]
web/Dockerfile(テスト用HTTPサーバ)
FROM python:3.12-alpine
WORKDIR /site
RUN echo "Hello via port-forwarding!" > index.html
EXPOSE 8000
CMD ["python", "-m", "http.server", "8000"]
docker-compose.yml
services:
ssh-server:
build: ./server
container_name: ssh-server
hostname: ssh-server
ports:
- "2222:22"
- "9000:9000" # R-forward用
networks: [labnet]
ssh-client:
build: ./client
container_name: ssh-client
hostname: ssh-client
networks: [labnet]
web:
build: ./web
container_name: web
hostname: web
networks: [labnet]
networks:
labnet:
driver: bridge
Dockerビルド・起動
# プロジェクトディレクトリ作成(任意)
mkdir -p ssh-portforward-lab/{server,client,web}
cd ssh-portforward-lab
# コンテナビルド・起動
docker compose build
docker compose up -d
実験
ポートフォワーディング
Macローカルのポート8080→SSHサーバ経由→web:8000
# SSH接続+ローカルフォワード設定(バックグラウンド)
sshpass -p 'test' ssh -f -N -L 8080:web:8000 -o StrictHostKeyChecking=no test@localhost -p 2222
# テスト
curl http://localhost:8080
# => "Hello via port-forwarding!"
# 終了
pkill -f "ssh.*-L 8080:web:8000"
リモートポートフォワーディング(-R)
ssh-serverのポート9000→ssh-client:9001にフォワード
# client側の準備(Pythonとネットワークツールのインストール)
docker exec ssh-client bash -c 'apt-get update -qq && apt-get install -y -qq python3 curl netcat-openbsd net-tools'
# client側でHTTPサービス起動
docker exec ssh-client bash -c 'cd /tmp && echo "From client via R-forward FIXED" > index.html && nohup python3 -m http.server 9001 --bind 0.0.0.0 > /dev/null 2>&1 &'
# クライアントサービスの動作確認
docker exec ssh-client curl -s http://localhost:9001
# => "From client via R-forward FIXED"
# SSH接続+リモートフォワード設定(ホストキーの問題がある場合は先にクリア)
ssh-keygen -R "[localhost]:2222" # 必要に応じて
sshpass -p 'test' ssh -f -N -R 9000:ssh-client:9001 -o StrictHostKeyChecking=no test@localhost -p 2222
# テスト用ツールのインストール(ssh-server側)
docker exec ssh-server bash -c 'apt-get install -y -qq netcat-openbsd telnet'
# 接続テスト(telnetで確認)
docker exec ssh-server bash -c 'timeout 3 telnet localhost 9000'
# => "Connected to localhost." が表示されれば成功
# HTTPテスト(docker-composeでポート9000をマップ済み)
curl -m 3 http://localhost:9000
# => 接続は確立するが、Python HTTP serverとの通信で"Empty reply"が発生する場合あり
# 終了
pkill -f "ssh.*-R 9000"
ダイナミックフォワーディング(-D, SOCKS5)
MacローカルのSOCKS5プロキシ(1080)経由でトンネル
# SSH接続+ダイナミックフォワード設定
sshpass -p 'test' ssh -f -N -D 1080 -o StrictHostKeyChecking=no test@localhost -p 2222
# テスト(IPアドレス指定が必要)
curl --socks5 localhost:1080 http://172.25.0.3:8000
# => "Hello via port-forwarding!"
# 終了
pkill -f "ssh.*-D 1080"
パケットキャプチャ
# SSH暗号化パケットをキャプチャ
docker exec -d ssh-server tcpdump -i any -w /tmp/ssh_tunnel.pcap port 22
# トラフィック生成
sshpass -p 'test' ssh -L 8080:web:8000 -o StrictHostKeyChecking=no test@localhost -p 2222 'sleep 2'
# キャプチャ停止・回収
docker exec ssh-server pkill tcpdump
docker cp ssh-server:/tmp/ssh_tunnel.pcap ./ssh_tunnel.pcap
# Wiresharkで解析
open ssh_tunnel.pcap
データ
1 0.000000 192.168.65.1 172.25.0.4 TCP 80 50399 → 22 [SYN] Seq=0 Win=65408 Len=0 MSS=65495 TSval=2192218866 TSecr=0 WS=128
2 0.000094 172.25.0.4 192.168.65.1 TCP 80 22 → 50399 [SYN, ACK] Seq=0 Ack=1 Win=65160 Len=0 MSS=1460 TSval=2104112435 TSecr=2192218866 WS=128
3 0.000953 192.168.65.1 172.25.0.4 TCP 72 50399 → 22 [ACK] Seq=1 Ack=1 Win=65408 Len=0 TSval=2192218867 TSecr=2104112435
4 0.001071 192.168.65.1 172.25.0.4 SSHv2 93 Client: Protocol (SSH-2.0-OpenSSH_9.9)
5 0.001093 172.25.0.4 192.168.65.1 TCP 72 22 → 50399 [ACK] Seq=1 Ack=22 Win=65152 Len=0 TSval=2104112436 TSecr=2192218867
6 0.014984 172.25.0.4 192.168.65.1 SSHv2 115 Server: Protocol (SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.13)
7 0.015294 192.168.65.1 172.25.0.4 TCP 72 50399 → 22 [ACK] Seq=22 Ack=44 Win=524160 Len=0 TSval=2192218882 TSecr=2104112450
8 0.016667 172.25.0.4 192.168.65.1 SSHv2 1192 Server: Key Exchange Init
9 0.016910 192.168.65.1 172.25.0.4 TCP 72 50399 → 22 [ACK] Seq=22 Ack=1164 Win=523136 Len=0 TSval=2192218883 TSecr=2104112452
10 0.017195 192.168.65.1 172.25.0.4 TCP 1520 50399 → 22 [ACK] Seq=22 Ack=1164 Win=524288 Len=1448 TSval=2192218884 TSecr=2104112452 [TCP PDU reassembled in 11]
11 0.017213 192.168.65.1 172.25.0.4 SSHv2 192 Client: Key Exchange Init
12 0.017251 172.25.0.4 192.168.65.1 TCP 72 22 → 50399 [ACK] Seq=1164 Ack=1590 Win=63616 Len=0 TSval=2104112453 TSecr=2192218884
13 0.034194 192.168.65.1 172.25.0.4 SSHv2 1280 Client: Diffie-Hellman Key Exchange Init
14 0.052950 172.25.0.4 192.168.65.1 SSHv2 1604 Server: Diffie-Hellman Key Exchange Reply, New Keys, Encrypted packet (len=284)
15 0.053131 192.168.65.1 172.25.0.4 TCP 72 50399 → 22 [ACK] Seq=2798 Ack=2696 Win=522752 Len=0 TSval=2192218920 TSecr=2104112488
16 0.059043 192.168.65.1 172.25.0.4 SSHv2 156 Client: New Keys, Encrypted packet (len=68)
17 0.059052 192.168.65.1 172.25.0.4 SSHv2 116 Client: Encrypted packet (len=44)
18 0.059076 172.25.0.4 192.168.65.1 TCP 72 22 → 50399 [ACK] Seq=2696 Ack=2926 Win=62336 Len=0 TSval=2104112494 TSecr=2192218925
19 0.059128 172.25.0.4 192.168.65.1 SSHv2 116 Server: Encrypted packet (len=44)
20 0.059223 192.168.65.1 172.25.0.4 TCP 72 50399 → 22 [ACK] Seq=2926 Ack=2740 Win=524160 Len=0 TSval=2192218926 TSecr=2104112495
21 0.059340 192.168.65.1 172.25.0.4 SSHv2 132 Client: Encrypted packet (len=60)
22 0.060140 172.25.0.4 192.168.65.1 SSHv2 336 Server: Encrypted packet (len=264)
23 0.060266 192.168.65.1 172.25.0.4 TCP 72 50399 → 22 [ACK] Seq=2986 Ack=3004 Win=523904 Len=0 TSval=2192218927 TSecr=2104112496
24 0.065633 192.168.65.1 172.25.0.4 SSHv2 156 Client: Encrypted packet (len=84)
25 0.106970 172.25.0.4 192.168.65.1 TCP 72 22 → 50399 [ACK] Seq=3004 Ack=3070 Win=62336 Len=0 TSval=2104112542 TSecr=2192218932
26 0.119849 172.25.0.4 192.168.65.1 SSHv2 100 Server: Encrypted packet (len=28)
27 0.120008 192.168.65.1 172.25.0.4 TCP 72 50399 → 22 [ACK] Seq=3070 Ack=3032 Win=524160 Len=0 TSval=2192218986 TSecr=2104112555
28 0.120320 192.168.65.1 172.25.0.4 SSHv2 184 Client: Encrypted packet (len=112)
29 0.120323 172.25.0.4 192.168.65.1 TCP 72 22 → 50399 [ACK] Seq=3032 Ack=3182 Win=62336 Len=0 TSval=2104112556 TSecr=2192218987
30 0.125320 172.25.0.4 192.168.65.1 SSHv2 700 Server: Encrypted packet (len=628)
31 0.125435 192.168.65.1 172.25.0.4 TCP 72 50399 → 22 [ACK] Seq=3182 Ack=3660 Win=523648 Len=0 TSval=2192218992 TSecr=2104112561
32 0.125440 172.25.0.4 192.168.65.1 SSHv2 116 Server: Encrypted packet (len=44)
33 0.125526 192.168.65.1 172.25.0.4 TCP 72 50399 → 22 [ACK] Seq=3182 Ack=3704 Win=523904 Len=0 TSval=2192218992 TSecr=2104112561
34 0.127657 192.168.65.1 172.25.0.4 SSHv2 192 Client: Encrypted packet (len=120)
35 0.127852 172.25.0.4 192.168.65.1 SSHv2 144 Server: Encrypted packet (len=72)
36 0.127925 192.168.65.1 172.25.0.4 TCP 72 50399 → 22 [ACK] Seq=3302 Ack=3776 Win=524160 Len=0 TSval=2192218994 TSecr=2104112563
37 2.132419 172.25.0.4 192.168.65.1 SSHv2 248 Server: Encrypted packet (len=176)
38 2.132966 192.168.65.1 172.25.0.4 TCP 72 50399 → 22 [ACK] Seq=3302 Ack=3952 Win=524032 Len=0 TSval=2192220999 TSecr=2104114568
39 2.133659 192.168.65.1 172.25.0.4 SSHv2 168 Client: Encrypted packet (len=96)
40 2.134929 192.168.65.1 172.25.0.4 TCP 72 50399 → 22 [FIN, ACK] Seq=3398 Ack=3952 Win=524288 Len=0 TSval=2192221001 TSecr=2104114568
41 2.136675 172.25.0.4 192.168.65.1 TCP 72 22 → 50399 [FIN, ACK] Seq=3952 Ack=3399 Win=62336 Len=0 TSval=2104114572 TSecr=2192221000
42 2.136968 192.168.65.1 172.25.0.4 TCP 72 50399 → 22 [ACK] Seq=3399 Ack=3953 Win=524160 Len=0 TSval=2192221003 TSecr=2104114568
まとめ
| 区間 (#) | 相対時刻例 | フェーズ / イベント | 主なパケット例 | 何が起きているか(推測含む) |
|---|---|---|---|---|
| 1–3 | 0.0000s | TCP 3-way ハンドシェイク | SYN / SYN,ACK / ACK | 192.168.65.1(クライアント) → 172.25.0.4(サーバ) へ22/TCP接続開始。 |
| 4 & 6 | 0.0011s / 0.0150s | プロトコルバージョン交換(平文) |
SSH-2.0-OpenSSH_9.9 / SSH-2.0-OpenSSH_9.6p1 …
|
ここまで平文。双方がSSH実装/バージョンを通知。 |
| 8 & 11 | 0.0167s / 0.0172s | KEXINIT 交換 | Server / Client: Key Exchange Init | 対応暗号・MAC・圧縮アルゴリズム候補を提示。 |
| 13–14 | 0.034–0.053s | Diffie-Hellman鍵交換 & New Keys | Client DH Init / Server Reply + NewKeys | 共有秘密生成。以降のペイロードは暗号化。 |
| 16–23 | 0.059–0.060s | 暗号化直後の初期制御 | Encrypted packet (len 44–336) | 認証要求/応答、チャネル確立準備など(内容は暗号化で見えない)。 |
| 24–36 | 0.066–0.128s | 認証〜チャネル確立完了と推定 | Encrypted 84B / 700B / 72B など | ユーザ認証成功、セッション/シェルチャネルOPEN/REQなどの制御。 |
| 37–39 | 2.132–2.134s | 終盤の制御・応答 | Server 176B → Client 96B (Encrypted) | コマンド終了/ログアウト処理など最終やり取り。 |
| 40–42 | 2.135–2.137s | TCP セッション終了(FIN/ACK) | FIN,ACK(ク→サ→ク) | 正常クローズ。クライアントからFIN、サーバもFIN、最後にACKで終了。 |
Discussion