シェルでWake-on-LAN (WoL)を自作する

マシンを遠隔起動出来るWake-on-LAN(WoL)はとても便利です。
通常はWoL専用のツールを用いますが、そういうものをインストール出来ない、したくないといった状況でも、結構簡単にbashだけで自作することができます。

```shell:wake_on_lan.bash
#!/bin/bash
readonly MAC=01:23:45:67:89:AB
readonly BROADCAST=255.255.255.255
readonly PORT=9

(
        # FFを6回、改行なしで出力
        for a in {1..6}; do
                echo -en '\xFF'
        done
        # MACアドレスのコロンを取り除きバイナリ化したものを16回
        for a in {1..16}; do
                echo -en '\x'${MAC//:/\\x}
        done
) | nc -b -w1 -u $BROADCAST $PORT
```
# WoLの解説
まずWoLの仕組みを解説します。
WoLでマシンを起動するには、「マジックパケット」を同じネットワークにあるマシンからブロードキャスト[^1]します。
マジックパケットについてはWikipediaの説明がわかりやすいので引用します。

> マジックパケットは、FF:FF:FF:FF:FF:FFに続けて起動したい装置のMACアドレスを16回繰り返したデータパターン(AMD Magic Packet Format) がペイロードのどこかに含まれているようなパケットである。
>-- <cite>[「Wake-on-LAN」（2019年3月7日 04:16‎ (UTC)の版）『ウィキペディア日本語版』。](https://ja.wikipedia.org/wiki/Wake-on-LAN#%E3%83%9E%E3%82%B8%E3%83%83%E3%82%AF%E3%83%91%E3%82%B1%E3%83%83%E3%83%88)</cite>　

このデータをUDPで[^2]ブロードキャスト[^1]すれば、該当マシンが起動してくれます。なおポート番号は何番でも良いです(パケットの中身しか見ていないので)が、慣例として9番が指定されることが多いです。

#シェルスクリプトの解説
WoLの仕組みを踏まえて、上記シェルスクリプトの中身を解説します。
まず、最初に定義されている変数については以下の通りです。

- `MAC` 起動したいマシンのMACアドレスを`:`区切りで指定します。ここはスクリプト内変数じゃなくてコマンドライン引数で実装するのも良い考えだと思います。
- `BROADCAST` WoLの送信先のブロードキャストアドレス。通常は`255.255.255.255`を指定しておけば良いですが、送信先ネットワークを指定したい場合などは変更して下さい。
- `PORT` ポート番号。一応変数にしていますが、何番でも良いです。

`{1..6}`は`1 2 3 4 5 6`に展開されます。

```shell-session
$ echo {1..6}
1 2 3 4 5 6
```
for文に渡すことで、6回コマンドを繰り返すことが出来ます。
`echo`コマンドの`-n`オプションは改行を出力しないとうオプションです。`-e`オプションはバックスラッシュによるエスケープを有効にします。WoLはバイナリデータなので必要です。

`${MAC//:/\\x}`は`MAC`という変数の`:`を`\x`に置換した文字列を表します。

```shell-session
$ MAC=01:23:45:67:89:AB
$ echo '\x'${MAC//:/\\x}
\x01\x23\x45\x67\x89\xAB
```
それを`-en`オプションを付けることで、バイナリで出力することになります。

なお、`nc`コマンドの`-b`オプションは、必要な環境と不要な環境があります。例えばcygwinはその一つです。必要に応じて外して下さい。

またこのシェルスクリプトはzshでもそのまま実行できます。zshの場合、`echo`コマンドはエスケープがオプションなしで有効になっており、`-e`は単純に無視されるので、問題なく動きます。アンチGNUの人はzshで実行すると良いでしょう。
もともとこのシェルスクリプトを作ろうと考えたときは、POSIX互換で作るつもりだったんですが、

- `echo`コマンドは互換性に難がある
- `printf`コマンドでバイナリを出力する場合、POSIX互換なら8進数指定が必要で面倒

ということが分かり、諦めました。[^3]

#ncコマンド
シェルだけといいつつ、ncコマンドは使っています、、、
実はbashにはTCPやUDP通信を行う機能があります。[bashのmanページ](https://www.gnu.org/software/bash/manual/bash.txt)を見てみましょう。

```text
   Bash handles several filenames specially when they are used in
redirections, as described in the following table.  If the operating
system on which Bash is running provides these special files, bash will
use them; otherwise it will emulate them internally with the behavior
described below.
```
(略)

```text
'/dev/udp/HOST/PORT'
     If HOST is a valid hostname or Internet address, and PORT is an
     integer port number or service name, Bash attempts to open the
     corresponding UDP socket.
```
つまりbash上で`/dev/udp/HOST/PORT`ファイルに書き込むとUDP通信が行われます。これを使えばncコマンドが不要になり、本当にbashのみでWoLが出来る、と期待したのですが、うまく行きませんでした。どうやらブロードキャストは出来ないようです。


[^1]: ターゲットマシンにマジックパケットが届けば、同じネットワークでなくてもいいし、ブロードキャストでなくてもいいですが、起動していないマシンにはIPアドレスがないので(簡単には)パケットを送ることができません。
[^2]: ペイロードの中に含まれていればいいだけなので、別にUDPじゃなくてping(ICMP echo)でも生IPでも生ethernetフレームでも良いのですが、UDPが通常一番簡単です。
[^3]: 参考：[どの環境でも使えるシェルスクリプトを書くためのメモ ver4.60](https://qiita.com/richmikan@github/items/bd4b21cf1fe503ab2e5c)の[echoコマンド](https://qiita.com/richmikan@github/items/bd4b21cf1fe503ab2e5c#echo%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89)と[printf（コマンドおよびAWKの中の関数）](https://qiita.com/richmikan@github/items/bd4b21cf1fe503ab2e5c#printf%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%81%8A%E3%82%88%E3%81%B3awk%E3%81%AE%E4%B8%AD%E3%81%AE%E9%96%A2%E6%95%B0)