リモートデスクトップの中継サーバーを作ろう

# はじめに

中継サーバーは、クライアントとサーバーの間に位置して、プロトコルを中継する役割を果たすサーバーです。WebにおけるProxyサーバーのような役割を果たすものです。

今回作成するものは、そのような中継サーバーのリモートデスクトップ版です。

# 今回作成する中継サーバーの構成

今回作成する中継サーバーは、以下の構成としています。

![中継サーバーの構成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/381550/1ae47132-038e-fe38-23d1-7e4ee02e0f9b.png)

sshには、ポート転送機能があります。実線のssh接続を行うことで、点線のポート転送機能を有効にすることができます。①によって②が有効に、③によって④が有効になります。

2種類のポート転送機能②④を繋ぎ合わせることで、点線の接続経路を作成することができます。そして、点線の接続経路を用いてRDPサーバーのリモートデスクトップポートに接続することができます。

点線の接続経路はsshによって保護されています。

# 今回作成する構成の特徴

サーバー自身は外部にリモートデスクトップサービスを公開する必要はありません。そのため、RDPサーバーのファイヤウォールは全ポートcloseで構いません。NAT越し、VPN超しでも構いません。

世界中のどこからでも接続可能です。①は常時接続のため、②の経路は常時保たれた状態となっています。必要なタイミングで④の経路を作成することで、世界中のどこからでもRDPサーバーに接続可能となります。

認証に公開鍵認証を使用しています。鍵を持っていない端末からは接続できない仕組みとしてあります。RDPの接続には、SSHの公開鍵認証＋RDPパスワード認証が必要です。

![RDPサーバーがファイヤウォールに守られていることを表す図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/381550/e53eca34-2f7d-748b-be4d-b7336eed620e.png)

# 構築

## 中継サーバーのセットアップ

中継サーバーをセットアップします。

どことは言いませんが、RDPサーバーが設置されているところとは縁も所縁もないVPSを使用しています。

VPSの最小プランを選択しました。中継サーバーには、CPUスペックやハードディスク容量は不要です。ネットワーク転送量も上限まで使うことはないでしょう。ネットワーク速度は早ければ早いほど良いです。但し、本体サーバーのネットワーク速度で頭打ちになるので、一定以上あれば十分です。

OSにはお手軽インストール可能な[Lubuntu-18.04.1(LTS)](https://lubuntu.me/)を使用しました。

### インストール

画面選択で注意して選ぶのは、日本語、最小インストール、ディスク全体を上書きでインストール、この3つくらいです。後はデフォルト設定のままでOKです。日本語を選べばキーボードもタイムゾーンも日本になります。

インストール後、メディアをunmountして再起動します。VPSによっては自動的にunmountされます。

### SSHサーバーのインストールとセットアップ

VPSで提供されているコンソール画面を使用して、順次コマンドを入力します。

```
apt update
apt install openssh-server
/etc/init.d/ssh stop
perl -pi -e 's/^#Port 22$/Port 54322/' /etc/ssh/sshd_config
/etc/init.d/ssh start
ss -lntp
```

リモート作業可能になったはずです。リモートからsshログインしてみます。以下はWindowsから接続するためのバッチファイルです。

```
cd /d %~dp0
ssh ^
	-l lubuntu ^
	-p 54322 ^
	xxx.xxx.xxx.xxx
```

接続できることが確認できました。ここで、ファイヤウォールを設定しておきます。以下は、中継サーバーで実行します。

```
ufw allow 54322
ufw reload
ufw enable
ufw status
```

実際は"ufw reload"はエラーとなります。インストール直後は"ufw disable"状態のためです。最終的な状態を"ufw status"で確認しておきます。sshの振り替えポート番号だけがopenになっていればOKです。再起動時にファイヤウォールが有効となるか確認するため、再起動しておくのがよいでしょう。

公開鍵認証で接続するための各種設定を行います。以下は、RDPサーバー上で実施します。[Windows10なら公式sshクライアントでよい](https://qiita.com/szly/items/e774961a41895ac50cf4)でしょう。

```
ssh-keygen -t ed25519 -f id_ed25519 -C 'RDP server'
```

作成された"id_ed25519.pub"を中継サーバーに追加します。

```
cd ~
mkdir .ssh
chmod go= .ssh
touch .ssh/authorized_keys
chmod go= .ssh/authorized_keys
vi ~/.ssh/authorized_keys
```

これで鍵を使ってログインできるはずです。RDPサーバーからログインできることを確認します。

```
ssh -l lubuntu -p 54322 -i id_ed25519 xxx.xxx.xxx.xxx
```

鍵認証できたので、パスワード認証を無効化します。

```
/etc/init.d/ssh stop
perl -pi -e 's/^#PasswordAuthentication yes$/PasswordAuthentication no/' /etc/ssh/sshd_config
/etc/init.d/ssh start
```

### シェルスクリプトの作成

シェルスクリプトを配置します。

```
cd ~/bin
touch run_rdp_check.sh
chmod u+x run_rdp_check.sh
vi run_rdp_check.sh
```

シェルスクリプトの内容は以下のようになっています。

```
#!  /bin/bash

pidfile=~/tmp/$( basename $0 .sh ).dat
if [ -f ${pidfile} ]; then
        cat ${pidfile} \
        | xargs -r -n 1 kill
fi
echo ${PPID} > ${pidfile}

while true; do
    date +"%Y/%m/%d %H:%M:%S"
    nc -z -v localhost 3389
    if [ "$?" != "0" ]; then
        break
    fi
    sleep 30
done
```

前段は、RDPポート転送セッションの残骸の削除です。詳細は後述します。

後段は、RDPポート転送セッションの維持状態の確認です。RDPのポート番号3389に実際に接続してみて、繋がらなくなったら処理を終了します。本当は終了コード"$?"は数値だとか、色々突っ込みどころはあるでしょうが、そのあたりの精査は追々していきます。

## RDPサーバーのセットアップ

### RDPサーバーにセットアップするバッチファイルの作成

前項で作成した"id_ed25519"を使って接続するためのバッチファイルを作成し、実行します。

```
cd /d %~dp0

:loop

echo %date% %time%

ssh ^
	-l lubuntu ^
	-p 54322 ^
	-i id_ed25519 ^
	-R 3389:127.0.0.1:3389 ^
	xxx.xxx.xxx.xxx ^
	bin/run_rdp_check.sh

timeout /t 10

goto :loop
```

前項で作成したシェルスクリプトを呼び出しています。スクリプトから帰ってきたらsshを終了します。ssh終了後は、再度サーバーにssh接続します。

この仕組みにより、①のssh常時接続、②のRDP経路維持を可能にしています。

## クライアントのセットアップ

### クライアントにセットアップするスクリプトファイル

クライアントでは、以下のバッチファイルを使用します。

```
ssh ^
	-l lubuntu ^
	-p 54322 ^
	-i id_ed25519 ^
	-L 3389:127.0.0.1:3389 ^
	xxx.xxx.xxx.xxx
```

これを実行することにより④の経路が作成されることになります。

この"id_ed25519"は、RDPサーバーの"id_ed25519"とは別のものにするべきです。また、RDPサーバーの"id_ed25519"はパスフレーズなしでしたが、こちらはパスフレーズありにするべきでしょう。RDPサーバーは自動接続、クライアントは手動接続だからです。"id_ed25519"の作成手順とセットアップ手順はほぼ同じであるため、ここでは記載を省略しました。

# 解説

## ポート転送

RDPサーバーのsshのオプションには"-R 3389:127.0.0.1:3389"を指定してあります。sshサーバーからsshクライアント方向へのポート転送です。これが、中継サーバーからRDPサーバーへのポート転送(②)を指しています。

また、クライアントのsshのオプションには"-L 3389:127.0.0.1:3389"を指定してあります。sshクライアントからsshサーバー方向へのポート転送です。これがクライアントから中継サーバーへのポート転送(④)を指しています。

sshとポート転送の向きが両者で異なる点が、オプションの"-R"と"-L"の使い分けとなっています。

## シェルスクリプトの前段

シェルスクリプトの前段は、ポート転送プロセスの残骸を除去するためのものです。再掲します。

```
pidfile=~/tmp/$( basename $0 .sh ).dat
if [ -f ${pidfile} ]; then
        cat ${pidfile} \
        | xargs -r -n 1 kill
fi
echo ${PPID} > ${pidfile}
```

前回処理時のポート転送プロセスを"kill"し、今回処理時のポート転送プロセスのPIDをファイルに記録するという仕組みとなっています。

ポート転送"-R"付きでssh接続すると、以下のようなプロセス状況になります。

```
lubuntu@linserver:~$ ps ax -o user,pid,ppid,cmd | grep -v grep | grep sshd
root       424     1 /usr/sbin/sshd -D
root      2493   424 sshd: lubuntu [priv]
lubuntu   2557  2493 sshd: lubuntu@notty
    :
lubuntu@linserver:~$
```

1行目がsshd本体です。2行目はsshログインしたセッションのサーバー側プロセスです。3行目はポート転送を扱うサーバー側プロセスです。3行目の親プロセスが2行目、2行目の親プロセスが1行目になっていることが分かります。

ポート転送を扱うサーバー側プロセスには、ターミナルが割り当てられません。そのため"notty"と表示されます。ターミナルが割り当てられていないことは、以下のように確認できます。

```
root@lin07:~# ls -l /proc/2557/fd/{0,1,2}
lrwx------ 1 root root 64  5月  1 00:00 /proc/2557/fd/0 -> /dev/null
lrwx------ 1 root root 64  5月  1 00:00 /proc/2557/fd/1 -> /dev/null
lrwx------ 1 root root 64  5月  1 00:00 /proc/2557/fd/2 -> /dev/null
root@lin07:~#
```

3行目がポート転送を扱うプロセスであることは、以下のように確認できます。どちらのコマンドもPIDが2557であることを示しています。

```
root@linserver:~# ss -lntp | grep 3389
LISTEN   0         128               127.0.0.1:3389             0.0.0.0:*        users:(("sshd",pid=2557,fd=10))
LISTEN   0         128                   [::1]:3389                [::]:*        users:(("sshd",pid=2557,fd=9))
root@linserver:~#
root@linserver:~# lsof -i:3389
COMMAND  PID    USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
sshd    2557 lubuntu    9u  IPv6  28523      0t0  TCP ip6-localhost:3389 (LISTEN)
sshd    2557 lubuntu   10u  IPv4  28524      0t0  TCP localhost:3389 (LISTEN)
    :
root@linserver:~#
```

このポート転送プロセスがポートを掴みっぱなしになることが稀にあるようです。そのため、そのプロセスを"kill"する処理を入れてあります。

シェルスクリプトのPPIDが、2行目(sshログインしたセッションのプロセス)ではなく3行目(ポート転送のプロセス)という点が意外でした。

## シェルスクリプトの後段

シェルスクリプトの後段は、RDP接続の確認処理です。再掲します。

```
while true; do
    date +"%Y/%m/%d %H:%M:%S"
    nc -z -v localhost 3389
    if [ "$?" != "0" ]; then
        break
    fi
    sleep 30
done
```

こちらは難しいところはないと思います。ncを用いて実際に接続できるか確認するという処理を定期的に行うというものです。"nc -z"での確認のため、相手がRDPかどうかといったところまでは確認していません。

たまたま組み込んだ"date"や"nc -v"がキープアライブ的な効果を発揮しているようです。RDP未接続でも、RDPサーバー側のsshセッションが切断されることはない動きをしています。

接続できなくなったら処理を終了します。終了後はsshセッションも終了します。その後、RDPサーバーのバッチ処理内のループにより、再度sshの接続を行うことになります。