イントラ向けサーバーをSSHポートフォワード経由でインターネット経由に切り替える

# 概要

イントラネット経由で社内向けサービスをホストしている際に、ネットワーク機器トラブルなどで、セグメント間が切れてしまうことがあります。内部サービスを一時的にインターネットから見えるようにすることで、緊急避難的にサービスを維持するための設定です。原理は単純でSSHポートフォワーディングとsocatによりソケット通信をリレーするだけのものです。

# 背景

イントラネット内で社内向けサービスをホストすることは多いと思いますが、そんな場合でもネットワークトラブルはつきものです。お互いにインターネット接続が利用可能であれば、ポートフォワードを使うことで、構成の変更を最小限にして、その場しのぎのサービス維持を行いました。

![network1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111095/2b57fe99-8a8a-8b59-171a-74c26f545c86.png)

構成は図に示したとおりです。緑が通常のルートで、ユーザーとサービスはイントラネットのセグメントが異なっており、間にファイアウォールが入っています。今回、ファイアウォールが不調になり、Network AとNetwork Bが接続できなくなってしまいました。そのため、急遽、インターネットに転送用のサーバーを立て、そのサーバー経由でサービスを維持することにしました。

# 手順

- インターネット用フォワードサーバー -- AWS EC2 Amazon Linux 2023

大まかな手順は以下です。

1. AWS EC2で中継用のサーバーを立てる(以降EC2サーバー)
1. サービス提供用セグメント(Network B)からそのEC2サーバーにSSHで接続しリモートポートフォワードを行う
1. EC2サーバー内ではsocatで通信をリレーする

これにより、図のa.→b.→c.の経路でサービスに接続することができます。

## EC2の準備

ここではt4g.microでAmazon Linux 2023を使い、EC2インスタンスを起動しました。この方法ではsocatを使うので、socatが使える環境であればお好みのもので十分です。また、パケットをリレーするだけなので、マシンパワーもそれほど必要ないと思います。インターネットから直接接続できれば、AWSである必要もないです。

利用者であるNetwork AおよびNetwork Bからのみ接続できるようにセキュリティグループを適用します。通常であれば443のみ(必要なら80も)Network Aに対して開き、SSH接続をするため、22をNetwork Bに対して開いておきます。

## サービス側からEC2サーバーへSSHで接続

EC2の準備ができたら、Network BからSSHで接続し、リモートポートフォワードします。これはサービスをホストしているサーバーからでも、その他のサーバーからでもいいですが、サービス提供サーバーに接続できる必要があります。

また、途中のルーターの設定によってはセッションがタイムアウトしてしまう場合がありますので、SSHのServerAliveInterval設定を追加してタイムアウトを防止します。ここでは120(秒)にしていますが、環境により調節が必要になるかもしれません。

複数のポートを転送したい場合(例えば80と443など)、SSHのオプションで全て設定してもいいと思います。

### サービス提供サーバーから接続する場合

```bash
$ ssh -i .ssh/private.pem \
    -R 8443:localhost:443 \
    -o ServerAliveInterval=120 \
    ec2-user@<EC2サーバーのグローバルIPアドレス>
```

### Network Bの他サーバーから接続する場合

```bash
$ ssh -i .ssh/private.pem \
    -R 8443:<サービス提供サーバーのIPアドレス>:443 \
    -o ServerAliveInterval=120 \
    ec2-user@<EC2サーバーのグローバルIPアドレス>
```

ここでのサービス提供サーバーとは、sshコマンドを実行するサーバーから見たサービス提供サーバーのアドレスですので、ローカルIPアドレスを使います。

### リッスンポートの確認

待ち受けポートを確認します。

```bash
$ netstat -ltn
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN
tcp        0      0 127.0.0.1:8443          0.0.0.0:*               LISTEN
tcp6       0      0 ::1:8443                :::*                    LISTEN
tcp6       0      0 :::22                   :::*                    LISTEN
```

このようにlocalhost(127.0.0.1)でポート8443を待ち受けていることがわかります。
localhostでしか待ち受けていないと外部から接続できないので、このポートに向けてパケットを転送します。

:::note info
/etc/ssh/sshd_configの設定で `GatewayPorts yes` にすると、待ち受けアドレスを0.0.0.0にすることができます。この際にポート443を直接開けばsocatは不要になりますが、特権ポートの場合、rootアカウントを使う必要があるため、ここではディフォルトの `GatewayPorts no` を前提として進めます。
:::

このとき、localhostにアクセスするとサービス提供サーバーと通信できるはずです。

```bash
$ curl https://localhost -k
```

## socatによるパケットのリレーを設定する

グローバルにアクセスできるようにするために、socatを使ってパケットをリレーします。単純にポート443に届いたパケットを8443にリレーするだけです。

```bash
$ sudo yum install -y socat
$ sudo socat tcp4-listen:443,reuseaddr,fork TCP:127.0.0.1:8443
```

socatを起動している間、パケットがリレーされ、外部から接続できるようになります。

## DNSの設定

接続できるようになったら、DNSレコードを修正します。現在向けている内部向けIPアドレスを外部向けIPアドレスに変更します。

# スクリプトで一括設定する

これらの手順はスクリプトでまとめることができます。例えばSSHポートフォワードとリモートコマンド実行を同時に行い、終了時はsocatのプロセスをkillすることで、手順を簡単にすることができました。この方法の場合、一つのポートが一つのsocatプロセスに結びつけられるため、ポートごとに実行します。

## 起動時

```bash
#!/bin/bash
set -e
SSHKEY=~/.ssh/private.pem
INT=8443
HOST=localhost
PORT=443
REMOTE=ec2-user@xxx.xxx.xxx.xxx
PIDFILE=forward_$PORT.pid

ssh -i $SSHKEY -f \
  -R $INT:$HOST:$PORT \
  -o ServerAliveInterval=120 \
  $REMOTE \
  "sudo socat tcp4-listen:$PORT,reuseaddr,fork TCP:127.0.0.1:$INT & echo \$!" > $PIDFILE
```

SSHでEC2にリモートポートフォワードありで接続し、socatをバックグラウンドで実行し、PIDをPIDFILEに書き込んでおきます。このときSSHは `-f` オプションを付けることでバックグラウンドで実行します。`$INT` は中継用ポートで、SSHでポートフォワードするポートです(127.0.0.1:8443として待ち受ける)。

## 終了時

socatを終了すればSSH接続も切断されるので、socatをkillします(SSHプロセスを先にkillすると、socatプロセスだけ残ってしまうので注意！)

```bash
#!/bin/bash
set -e
SSHKEY=~/.ssh/private.pem
REMOTE=ec2-user@xxx.xxx.xxx.xxx
PORT=443
PIDFILE=forward_$PORT.pid
PID=$(cat $PIDFILE)

ssh -i $SSHKEY \
  $REMOTE \
  sudo kill $PID
rm -f $PIDFILE
```

## 全部まとめる

それぞれの動作を分けていますが、設定ファイルを外部化して、複数ポートに対応させました。
`config`と`set-forward.sh`を同じディレクトリに配置して実行します。

```bash:config
SSHKEY=~/.ssh/private.pem
REMOTE=ec2-user@xxx.xxx.xxx.xxx
PORTS="localhost:80 localhost:443"
```

PORTSをスペースで区切って複数指定することができます。

```bash:set-forward.sh
#!/bin/bash
# 外部サーバーを経由して内部のサービスに接続する
# 2023-05-23 nobrin
# https://qiita.com/nobrin/items/cb344550b378eb8ed01c
set -e
CUR=$(cd $(dirname $0); pwd)
. $CUR/config

function show () {
  echo ===
  echo netstat on $REMOTE
  echo ===
  ssh -i $SSHKEY $REMOTE netstat -ltn
}

case $1 in
  connect )
    # 接続する
    for port in $PORTS; do
      HOST=$(echo $port | cut -d: -f1 -)
      PORT=$(echo $port | cut -d: -f2 -)
      INT=$(printf "8%03d" $PORT)
      PIDFILE=forward_$PORT.pid

      ssh -i $SSHKEY -f \
        -R $INT:$HOST:$PORT \
        -o ServerAliveInterval=120 \
        $REMOTE \
        "sudo socat tcp4-listen:$PORT,reuseaddr,fork TCP:127.0.0.1:$INT & echo \$!" > $PIDFILE
    done
  ;;

  disconnect )
    # 転送を終了する
    for port in $PORTS; do
      HOST=$(echo $port | cut -d: -f1 -)
      PORT=$(echo $port | cut -d: -f2 -)
      PIDFILE=forward_$PORT.pid
      PID=$(cat $PIDFILE)

      ssh -i $SSHKEY \
        $REMOTE \
        sudo kill $PID
      rm -f $PIDFILE
    done
  ;;

  show )
    show
    exit 0
  ;;

  * )
    echo "usage $0 {connect|disconnect|show}"
    exit 1
  ;;
esac

show
```

## 実行方法
### 接続
configファイルの内容をもとに接続します。接続後、netstatを表示します。

```bash
$ ./set-forward.sh connect
===
netstat on ec2-user@xxx.xxx.xxx.xxx
===
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 127.0.0.1:8443          0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:443             0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN
tcp        0      0 127.0.0.1:8080          0.0.0.0:*               LISTEN
tcp6       0      0 ::1:8443                :::*                    LISTEN
tcp6       0      0 :::22                   :::*                    LISTEN
tcp6       0      0 ::1:8080                :::*                    LISTEN
```

### 終了
現在の接続を終了します。接続終了後、netstatを表示します。

```bash
$ ./set-forward.sh disconnect
===
netstat on ec2-user@xxx.xxx.xxx.xxx
===
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN
tcp6       0      0 :::22                   :::*                    LISTEN
```

### ポート状態確認
リモートサーバーのnetstatを確認します。

```bash
$ ./set-forward.sh show
===
netstat on ec2-user@xxx.xxx.xxx.xxx
===
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 127.0.0.1:8443          0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:443             0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN
tcp        0      0 127.0.0.1:8080          0.0.0.0:*               LISTEN
tcp6       0      0 ::1:8443                :::*                    LISTEN
tcp6       0      0 :::22                   :::*                    LISTEN
tcp6       0      0 ::1:8080                :::*                    LISTEN
```

# まとめ

以上で緊急避難的ですがサービス継続が可能です。あくまで緊急避難ですので、短時間と割り切って実施するとよいかなと思います。

# 参考

- SSH Remote Port Forwarding のsocatの部分
  - https://qiita.com/Daisuke-Otaka/items/45828bcbcf871a67debe