SSH ポート転送時のSSH Connection Protocolについて

SSH ポート転送（Port Forwarding）の実行時、プロトコル的に何が起こっているか気になったので調べた。

**いわゆるリモートポート転送（`-R`オプション）は調べていません！**

# RFC 4254 SSH Connection Protocolとは

[こちら](https://tools.ietf.org/html/rfc4254)

SSH Transport Layerの上で動作するプロトコルで、インタラクティブなセッション、コマンド実行、ポート転送などを提供します。

今回、注目するのは`TCP/IP Port Forwarding`の部分です。

ポート転送時、このプロトコルがどのように使われているのか確認してみます。

# 確認方法

OpenSSHを使います。

以下の環境を用意しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547524/6558f972-1bba-fbc4-5690-0cc03387b028.png)


- ローカルサーバ　`192.168.122.80`
    - CentOS 7.8.2003 / OpenSSH 7.4p1
- リモートサーバ `192.168.122.61`
    - CentOS 7.8.2003 / OpenSSH 7.4p1
- 接続先サーバ `192.168.122.111`

ローカルサーバからリモートサーバにSSHで接続し、
ポート転送で接続先サーバへTCP通信してみます。

本来はSSHのメッセージをキャプチャしたいのですが、SSHを復号する方法がわからない…ため、
リモートサーバのsshdログからメッセージのやりとりを探ってみます。

```bash:/etc/ssh/sshd_config
# 抜粋。デバッグログを出力。
#LogLevel INFO
LogLevel DEBUG3
```
同様にローカルサーバのSSHクライアントも`-vvv`オプションをつけてみます。

```bash
$ ssh -vvv -N -L 10022:192.168.122.111:22 192.168.122.61
```

また、ローカルサーバには確認のためlsofをインストールしました。

# `-L` ローカルポート転送

ローカルサーバのTCPポートへの通信を接続先サーバのポートへ転送します。

```bash:-L
# -Nはログイン先で何もしない
[testing@local ~]$ ssh -vvv -N -L 10022:192.168.122.111:22 192.168.122.61
```

## SSH開始時

上のコマンドを実行したタイミングで、sshdのログ（/var/log/secure）を見てみます。

```bash
$ sudo tailf /var/log/secure | grep packet:
<略>
Sep  6 12:03:18 remote sshd[8365]: debug3: send packet: type 20 [preauth]
Sep  6 12:03:18 remote sshd[8365]: debug3: receive packet: type 20 [preauth]
Sep  6 12:03:18 remote sshd[8365]: debug3: receive packet: type 30 [preauth]
Sep  6 12:03:18 remote sshd[8365]: debug3: send packet: type 31 [preauth]
Sep  6 12:03:18 remote sshd[8365]: debug3: send packet: type 21 [preauth]
Sep  6 12:03:18 remote sshd[8365]: debug3: send packet: type 7 [preauth]
Sep  6 12:03:18 remote sshd[8365]: debug3: receive packet: type 21 [preauth]
Sep  6 12:03:18 remote sshd[8365]: debug3: receive packet: type 5 [preauth]
Sep  6 12:03:18 remote sshd[8365]: debug3: send packet: type 6 [preauth]
Sep  6 12:03:18 remote sshd[8365]: debug3: receive packet: type 50 [preauth]
Sep  6 12:03:18 remote sshd[8365]: debug3: send packet: type 51 [preauth]
Sep  6 12:03:18 remote sshd[8365]: debug3: receive packet: type 50 [preauth]
Sep  6 12:03:18 remote sshd[8365]: debug3: send packet: type 60 [preauth]
Sep  6 12:03:18 remote sshd[8365]: debug3: receive packet: type 50 [preauth]
Sep  6 12:03:18 remote sshd[8365]: debug3: send packet: type 52 [preauth]
Sep  6 12:03:18 remote sshd[8368]: debug3: send packet: type 80
Sep  6 12:03:18 remote sshd[8368]: debug3: receive packet: type 80
```
receiveしたpacketのtypeに注目すると、`type 90 (SSH_MSG_CHANNEL_OPEN)`がありません。
（typeの一覧は[rfc4250](https://tools.ietf.org/html/rfc4250)に記載があります。）

また、最後の`type 80 (SSH_MSG_GLOBAL_REQUEST)`は、
ログを確認すると、どちらもOpenSSHの拡張機能であることがわかります。
（[こちら](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL)。`hostkeys-00@openssh.com`と`no-more-sessions@openssh.com`）

```bash:/var/log/secure（リモートサーバ）
Sep  6 12:03:18 remote sshd[8368]: debug3: notify_hostkeys: sent 3 hostkeys
Sep  6 12:03:18 remote sshd[8368]: debug3: send packet: type 80
<略>
Sep  6 12:03:18 remote sshd[8368]: debug3: receive packet: type 80
Sep  6 12:03:18 remote sshd[8368]: debug1: server_input_global_request: rtype no-more-sessions@openssh.com want_reply 0
```
```bash:クライアントログ（ローカルサーバ）
debug1: Requesting no-more-sessions@openssh.com
debug3: send packet: type 80
debug1: Entering interactive session.
debug1: pledge: network
debug3: receive packet: type 80
debug1: client_input_global_request: rtype hostkeys-00@openssh.com want_reply 0
```
このように、この段階ではポート転送に関わるメッセージは送信されていません。

## 転送元ポート接続時

次に、ローカルサーバで転送元ポートに接続してみます。

```bash
[testing@local ~]$ ssh -p 10022 127.0.0.1
testing@127.0.0.1's password:
```
ここでsshdのログを確認すると、`type 90 (SSH_MSG_CHANNEL_OPEN)`を受信し、`type 91 (SSH_MSG_CHANNEL_OPEN_CONFIRMATION)`を送信しています。

```bash
[testing@remote ~]$ sudo tailf /var/log/secure | grep packet:
<略>
Sep  6 12:03:45 remote sshd[8368]: debug3: receive packet: type 90
Sep  6 12:03:45 remote sshd[8368]: debug3: send packet: type 91
```

grepを外して詳しく確認してみます。

```bash:/var/log/secure
Sep  6 12:03:45 remote sshd[8368]: debug3: receive packet: type 90
Sep  6 12:03:45 remote sshd[8368]: debug1: server_input_channel_open: ctype direct-tcpip rchan 2 win 2097152 max 32768
Sep  6 12:03:45 remote sshd[8368]: debug1: server_request_direct_tcpip: originator 127.0.0.1 port 53988, target 192.168.122.111 port 22
Sep  6 12:03:45 remote sshd[8368]: debug2: fd 8 setting O_NONBLOCK
Sep  6 12:03:45 remote sshd[8368]: debug2: fd 8 setting TCP_NODELAY
Sep  6 12:03:45 remote sshd[8368]: debug1: connect_next: host 192.168.122.111 ([192.168.122.111]:22) in progress, fd=8
Sep  6 12:03:45 remote sshd[8368]: debug3: fd 8 is O_NONBLOCK
Sep  6 12:03:45 remote sshd[8368]: debug3: fd 8 is O_NONBLOCK
Sep  6 12:03:45 remote sshd[8368]: debug1: channel 0: new [direct-tcpip]
Sep  6 12:03:45 remote sshd[8368]: debug1: server_input_channel_open: confirm direct-tcpip
Sep  6 12:03:45 remote sshd[8368]: debug3: channel 0: waiting for connection
Sep  6 12:03:45 remote sshd[8368]: debug1: channel 0: connected to 192.168.122.111 port 22
Sep  6 12:03:45 remote sshd[8368]: debug3: send packet: type 91
```

唐突ですが、今回受信したメッセージのフォーマットは以下だと考えられます。
（rfcより抜粋）

```
      byte      SSH_MSG_CHANNEL_OPEN
      string    "direct-tcpip"
      uint32    sender channel
      uint32    initial window size
      uint32    maximum packet size
      string    host to connect
      uint32    port to connect
      string    originator IP address
      uint32    originator port
```

ログの2行目と3行目に注目して、受信したメッセージを推測してみます。

2行目、`rchan 2 win 2097152 max 32768`部分は、`sender channel`、`initial window size`、`maximum packet size` に対応していそうです。
（別の値を受信して、同意できるものを表示している可能性もありますが…）

3行目、`originator 127.0.0.1 port 53988`はそのまま`originator IP address`と`originator port`に、
残った`target 192.168.122.111 port 22`は`host to connect`と`port to connect`に対応していると思われます。

大体のメッセージがわかったところで各パラメータについて考えます。

- `sender channel`はクライアントが割り当てるチャンネル番号と一致します。
    - クライアントのログで確認できます。
- `initial window size`、`maximum packet size`はわかりません。クライアントのデフォルト？
- `host to connect`、`port to connect`は転送先のアドレス、ポートと一致します。

最後に`originator IP address`、`originator port`です。
転送元ポートへの接続時、接続元ポートに何を使っているか確認します。

```bash:
[testing@local ~]$ lsof -i
COMMAND  PID    USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
<略>
ssh     4585 testing    6u  IPv4  37972      0t0  TCP localhost:10022->localhost:53988 (ESTABLISHED)
ssh     4586 testing    3u  IPv4  37971      0t0  TCP localhost:53988->localhost:10022 (ESTABLISHED)
```
53988を使っています。これはログの`originator port`部分と一致します。

せっかくなので、さらに転送元ポートへ接続してみます。
sshdログで新しいメッセージの受信を確認できます。

```bash
Sep  6 12:09:57 remote sshd[8368]: debug3: receive packet: type 90
Sep  6 12:09:57 remote sshd[8368]: debug1: server_input_channel_open: ctype direct-tcpip rchan 3 win 2097152 max 3
2768
Sep  6 12:09:57 remote sshd[8368]: debug1: server_request_direct_tcpip: originator 127.0.0.1 port 53990, target 19
2.168.122.111 port 22
```
前回と比べると、`sender channel`が`3`に、`originator port`が`53900`になったようです。

やはり`originator port`は接続元ポートと一致します。

```bash
$ lsof -i
COMMAND  PID    USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
<略>
ssh     4585 testing    7u  IPv4  38063      0t0  TCP localhost:10022->localhost:53990 (ESTABLISHED)
ssh     4591 testing    3u  IPv4  38062      0t0  TCP localhost:53990->localhost:10022 (ESTABLISHED)
```
## 切断時

転送元ポートへのTCP接続を切断してみます。
SSHを`Ctrl+C`で中断します

すると、`type 96 (SSH_MSG_CHANNEL_EOF)`と`type 97 (SSH_MSG_CHANNEL_CLOSE)`が送受信されます。

```bash
[testing@remote ~]$ sudo tailf /var/log/secure | grep packet:
<略>
Sep  6 12:29:29 remote sshd[8368]: debug3: receive packet: type 96
Sep  6 12:29:29 remote sshd[8368]: debug3: send packet: type 96
Sep  6 12:29:29 remote sshd[8368]: debug3: send packet: type 97
Sep  6 12:29:29 remote sshd[8368]: debug3: receive packet: type 97
```
##ローカルポート転送まとめ
- `SSH_MSG_CHANNEL_OPEN`が送信されるのは、SSH接続時ではなく、転送元ポートへのTCP接続時です。
- メッセージのパラメータは以下になります。
    - `sender channel`はクライアントのチャンネル番号です。
    - `host to connect`、`port to connect`は転送先ホスト、ポートです。
    - `originator IP address`、`originator port`は転送元ポートへの接続元アドレス、ポートです。
- TCP接続が終了すると、`SSH_MSG_CHANNEL_EOF`、`SSH_MSG_CHANNEL_CLOSE`が送受信されます。

# `-D` Dynamicポート転送

ローカルサーバのTCPポートを指定すると、そのポートへの接続時**SOCKS**サーバとして動作します。

```bash
[testing@local ~]$ ssh -vvv -N -D 1080 192.168.122.61
```

## SSH開始時
ローカルポート転送と同様、ポート転送に関わる通信は発生しないようです。
（最後の`type 80`についても同様です。）

```bash
Sep  6 13:06:29 remote sshd[9061]: debug3: send packet: type 20 [preauth]
Sep  6 13:06:29 remote sshd[9061]: debug3: receive packet: type 20 [preauth]
Sep  6 13:06:30 remote sshd[9061]: debug3: receive packet: type 30 [preauth]
Sep  6 13:06:30 remote sshd[9061]: debug3: send packet: type 31 [preauth]
Sep  6 13:06:30 remote sshd[9061]: debug3: send packet: type 21 [preauth]
Sep  6 13:06:30 remote sshd[9061]: debug3: send packet: type 7 [preauth]
Sep  6 13:06:30 remote sshd[9061]: debug3: receive packet: type 21 [preauth]
Sep  6 13:06:30 remote sshd[9061]: debug3: receive packet: type 5 [preauth]
Sep  6 13:06:30 remote sshd[9061]: debug3: send packet: type 6 [preauth]
Sep  6 13:06:30 remote sshd[9061]: debug3: receive packet: type 50 [preauth]
Sep  6 13:06:30 remote sshd[9061]: debug3: send packet: type 51 [preauth]
Sep  6 13:06:30 remote sshd[9061]: debug3: receive packet: type 50 [preauth]
Sep  6 13:06:30 remote sshd[9061]: debug3: send packet: type 60 [preauth]
Sep  6 13:06:30 remote sshd[9061]: debug3: receive packet: type 50 [preauth]
Sep  6 13:06:30 remote sshd[9061]: debug3: send packet: type 52 [preauth]
Sep  6 13:06:30 remote sshd[9064]: debug3: send packet: type 80
Sep  6 13:06:30 remote sshd[9064]: debug3: receive packet: type 80
```
## SOCKS接続時

SOCKS経由でのHTTP接続をやってみます。

```
[testing@local ~]$ curl -i --SOCKS5 127.0.0.1:1080 http://192.168.122.111:80 
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.8.2
Date: Sun, 06 Sep 2020 17:07:46 GMT
Content-type: text/html; charset=utf-8
Content-Length: 843

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
```
この時、SOCKSの通信は以下のようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547524/d9a28f3c-97c7-b63a-e29c-a4cd17fd8573.png)

SOCKSの認証についてやり取りした後、接続元から`Connect`が送信されます。
ここで`Remote Address`は`192.168.122.111`、`Port`は`80`です。
また、SOCKSの通信と後に続くHTTP通信は、どちらも接続元ポート`39884`を使っています。

ここで、sshdのログを確認します。

```bash:
[testing@remote ~]$ sudo tailf /var/log/secure | grep packet:
<略>
Sep  6 13:07:46 remote sshd[9064]: debug3: receive packet: type 90
Sep  6 13:07:46 remote sshd[9064]: debug3: send packet: type 91
Sep  6 13:07:46 remote sshd[9064]: debug3: send packet: type 96
Sep  6 13:07:46 remote sshd[9064]: debug3: receive packet: type 96
Sep  6 13:07:46 remote sshd[9064]: debug3: receive packet: type 97
Sep  6 13:07:46 remote sshd[9064]: debug3: send packet: type 97
```
`type 90`について詳しく確認します。

```bash:/var/log/secure
Sep  6 13:07:46 remote sshd[9064]: debug3: receive packet: type 90
Sep  6 13:07:46 remote sshd[9064]: debug1: server_input_channel_open: ctype direct-tcpip rchan 2 win 2097152 max 32768
Sep  6 13:07:46 remote sshd[9064]: debug1: server_request_direct_tcpip: originator 127.0.0.1 port 39884, target 192.168.122.111 port 80
```
見たところ、

- `host to connect`、`port to connect`はSOCKS Connectの`Remote Address`、`Port`
- `originator IP address`、`originator port`はSOCKSの接続元アドレス、ポート

なっていそうです。


今度は別のポートで待ち受けるHTTPサーバに接続してみます。

```
[testing@local ~]$ curl -i --SOCKS5 127.0.0.1:1080 http://192.168.122.111:8000
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.8.2
Date: Sun, 06 Sep 2020 17:13:53 GMT
Content-type: text/html; charset=utf-8
Content-Length: 843

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547524/8252bd2c-cc7e-9655-88cf-a5f43209d188.png)
SOCKSのConnectで`Remote Address`は`192.168.122.111`、`Port`は`8000`です。
接続元ポートは`39886`を使っています。


同様に確認します。

```bash
Sep  6 13:13:53 remote sshd[9064]: debug3: receive packet: type 90
Sep  6 13:13:53 remote sshd[9064]: debug3: send packet: type 91
Sep  6 13:13:53 remote sshd[9064]: debug3: send packet: type 96
Sep  6 13:13:53 remote sshd[9064]: debug3: receive packet: type 96
Sep  6 13:13:53 remote sshd[9064]: debug3: receive packet: type 97
Sep  6 13:13:53 remote sshd[9064]: debug3: send packet: type 97
```

```bash
Sep  6 13:13:53 remote sshd[9064]: debug3: receive packet: type 90
Sep  6 13:13:53 remote sshd[9064]: debug1: server_input_channel_open: ctype direct-tcpip rchan 2 win 2097152 max 3
2768
Sep  6 13:13:53 remote sshd[9064]: debug1: server_request_direct_tcpip: originator 127.0.0.1 port 39886, target 19
2.168.122.111 port 8000
```
やはり一回目と同様です。


## Dynamicポート転送まとめ
- SOCKS Connectの受信後に、以下パラメータで`SSH_MSG_CHANNEL_OPEN`が送信されます。
    - `host to connect`、`port to connect`はSOCKS Connectで指定したリモートアドレス、ポートです。
    - `originator IP address`、`originator port`はSOCKS接続時の接続元アドレス、ポートです。



# 終わりに

SSHトンネル、なんて言い方をするので恒久的なイメージだったが、
TCP接続に合わせてChannelを作っていることがわかった。