全自動でWake On LANしたい

本稿は[KLab Engineer Advent Calendar 2019](https://qiita.com/advent-calendar/2019/klab)の7日目の記事です。デバッグに手間取っていたら遅刻しました…。

systemdとTCPプロキシとWake On LANの話題です。

## 動機とあらすじ

私はMacユーザーなのですが、自宅のWindowsマシンをリモートデスクトップ経由で使うことがあります。

このWindowsマシンは普段スリープ状態にしているので、使いたいときはまずWake On LANのマジックパケットを投げて起こしてやる必要があります[^1]。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46856/7e7c7cce-ad16-a7dd-329d-18649c2b838f.png)

[^1]: リモートデスクトップ専用機で普段は使わないため手の届かないところに置いてあるのです

しかし、Windows機のMACアドレスを忘れていたり、そもそもマジックパケットを投げるコマンドを忘れていたりして手間取ることが珍しくありません。

もちろん以前使ったコマンドが履歴に残っているので何とかなるのですが、そもそもマジックパケットを毎回手動で投げるのが面倒に思えてきました。

そこで、systemdでTCPプロキシを構成し、さらにプロキシ接続前に自動的にマジックパケットを投げるような仕組みを作ってみました。我が家には常時起動しているRaspberry Piがあるので、ここでTCPプロキシを運用することにしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46856/b1ef953a-77f8-6494-c49f-c40395b22919.png)

これにより手元のMacでマジックパケットを投げる一手間が不要になり、リモートデスクトップのGUIから接続先を選ぶだけでWindowsにログオンできるようになります。

本稿ではこの仕組みの詳細を紹介します。プロトコルに依存した部分はないのでリモートデスクトップ以外でも利用できるはずです。

## systemdからマジックパケットを投げる

systemdは最近のLinuxで採用されている高機能なサービス管理プログラムです。

まずはマジックパケットを投げる処理を紹介します。これはoneshotのサービステンプレートで記述しました。

```ini:/etc/systemd/system/wol@.service
[Unit]
Description = Sending Wake on LAN packet (oneshot)
After = network-online.target
Wants = network-online.target

[Service]
Type = oneshot
ExecStart = /usr/sbin/etherwake %i
```

サービステンプレートというのはサービス名の一部（インスタンス文字列）を可変文字列にしてサービスに引き渡すような仕組みです。ここではインスタンス文字列としてMACアドレスを指定して使っています。

```
$ sudo systemctl start wol@00:00:5e:00:53:01.service
```

こうするとMACアドレス 00:00:5e:00:53:01 宛にマジックパケットが送られます。

## TCPプロキシで接続前にマジックパケットを投げる

次にsystemdでTCPプロキシを作っていきます。こちらは2ファイル構成です。

```ini:/etc/systemd/system/rdp-proxy.socket
[Unit]
Description = Socket for RDP (Remote Desktop Protocol) proxy

[Socket]
ListenStream = 0.0.0.0:3389
Accept = yes

[Install]
WantedBy = sockets.target
```

上記ファイルを設置した上で自動起動を有効にします。

```
$ sudo systemctl enable rdp-proxy.socket
$ sudo systemctl start rdp-proxy.socket
```

`ListenStream=`の設定により、TCPの3389番で待ち受けてサービスに受け渡します[^2]。

また、`Accept = yes` の場合はソケット接続をacceptするたびに同名のサービステンプレート（この場合なら`rdp-proxy@.service`）が毎回呼び出されます。

[^2]: 他にも`ListenDatagram=`を使えばUDPで待ち受けられます。

```ini:/etc/systemd/system/rdp-proxy@.service
[Unit]
Description = Server for RDP (Remote Desktop Protocol) Proxy
Requires = wol@00:00:5e:00:53:01.service
After = network-online.target wol@00:00:5e:00:53:01.service
Wants = network-online.target

[Service]
Type = simple
ExecStartPre = /usr/bin/timeout 60 /bin/sh -c 'until /bin/nc -w 5 -z 192.0.2.1 3389; do sleep 1; done'
ExecStart = /bin/nc -q 10 192.0.2.1 3389
StandardInput = socket
StandardOutput = socket
StandardError = journal
```

`Requires` で先ほど作ったマジックパケットを投げるサービスを指定しています。

メインの処理としてはncコマンドを使ってプロキシしています。既に説明したように1コネクションごとに1プロセスが起動することになります。

ただし、単にnetcatでプロキシするだけでは期待通りに動きませんでした。というのも、マジックパケットを投げてからリモートデスクトップサーバがlisten状態になるまでタイムラグがあるので、しばらく待ってからでないと接続できないのです。そこで接続前に`ExecStartPre`を使ってTCP接続できるかどうかのチェックをして、接続できるようになってからプロキシするようにしました。

IPアドレスとMACアドレスは例示用ですので、上記設定を利用する際は適宜変更してご利用ください。

## 実際に使ってみて

実際に使ってみると予想以上に実用的でした。プロキシに使っているのはRaspberry Pi 2ですが、性能面の問題はなさそうです（実験した限りではCPU負荷やネットワーク帯域など余裕があったため）。

ちなみに私の環境だとプロキシに接続してからスリープしていたWindowsにリモートデスクトップ接続するまで約30秒のタイムラグがあります。耐えられないほどではないのですが、遅すぎる気がするので何か改善ポイントがあるのかもしれません。

## まとめ

systemdでTCPプロキシを作り、その接続前に任意の外部コマンドが実行できることを示しました。今回は外部コマンドでWake On LANのマジックパケットを投げましたが、AWSインスタンスを立ち上げてから接続するなどの応用もできそうです。

それにしてもsystemdって何でもできますね…。

## 参考URL

- [systemd\.socket(5)](https://www.freedesktop.org/software/systemd/man/systemd.socket.html)
- [Linux女子部　systemd徹底入門](https://www.slideshare.net/enakai/linux-27872553)
- [systemd と netcat で簡易中継サーバを作る \- Qiita](https://qiita.com/ngyuki/items/9ad3fc51800fef646e0e)