ネットワークの観察 - 第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