WOLによる社内PCの電源管理について

# はじめに
職場で新型コロナによるBCP対応という事で，在宅勤務が施行されました。
業務の環境は自席PCからシンクライアントを利用した環境に変更となりました。
しかし，一部の業務では自席PCを利用する必要があったため，WOL(**W**ake **O**n **L**an)を利用することにしました。
この時，社内環境の制約など，いろいろな課題に直面したので，覚書として解決策を残しておきます。

# 社内環境
社内環境では以下の制約がありました。

- シンクライアントでは許可以外のexe実行が許されていない。
- ゲートウェイのARPキャッシュが4時間で破棄される。
- 外部からのブロードキャストはゲートウェイが破棄してゲートウェイ傘下には届かない。

上記より，WOLを実行するためのマジックパケットを送付するために，exeのフリーウェアを使用することが出来ませんでした。
また，シンクライアントと自席PCはサブネットが異なるため，ブロードキャストによるマジックパケットの送付は出来ませんでした。
そのため，IPアドレスを直指定してマジックパケットを送付する必要がありましたが，ARPキャッシュが4時間で破棄されるので，一晩立つと起動できないといった事態に見舞われました。

# 解決策
これらを踏まえて以下の方法を採用しました。

- シンクライアントでも使用可能なPowerShellを利用する。
- 自席PCと同一のサブネット傘下にマジックパケットをサブネット内ブロードキャスト発信する踏み台サーバを立てる。
- シンクライアントからはマジックパケットは送信せず，踏み台サーバにMACアドレスを通知する。

## 送信クライアント
送信クライアントは踏み台PCのIPアドレスを設定してユーザに配布しました。
ユーザには自席PCのMACアドレスを設定してもらいました。

また，PowerShellは何も設定していないとセキュリティで実行できないので，**-ExecutionPolicy**オプションを指定した実行用ショートカットも合わせて配布しました。
`%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -NoExit -ExecutionPolicy RemoteSigned .\StartWOL.ps1`

PowerShellでは.netのTCPクライアントでMACアドレスを送付しています。

``` PowerShell:StartWOL.ps1
# 踏み台PCのIPアドレスを設定( . 区切りで指定)
$IP_ADDR = @("xxx.xxx.xxx.xxx")
# 起動対象PCのMACアドレスを設定( - 区切りで指定)
$MAC_ADDR = @("xx-xx-xx-xx-xx-xx")

# IPアドレス，MACアドレスが設定されていない場合は設定する。
if($IP_ADDR -eq "xxx.xxx.xxx.xxx")
{
    $IP_ADDR = Read-Host "踏み台PCのIPアドレスを入力して下さい。( . 区切りで指定) > "
}
if($MAC_ADDR -eq "xx-xx-xx-xx-xx-xx")
{
    $MAC_ADDR = Read-Host "起動対象PCのMACアドレスを入力して下さい。( - 区切りで指定) > "
}

Write-Host "踏み台PC   IPアドレス ：" $IP_ADDR
Write-Host "起動対象PC MACアドレス：" $MAC_ADDR
Write-Host "踏み台PCへWOLのマジックパケットを送信を依頼します。"

try
{
    # IPアドレスインスタンス生成
    $target = [System.Net.IPAddress]::Parse($IP_ADDR) 

    # TCPクライアント生成
    $client = New-Object System.Net.Sockets.tcpClient;

    # 対象に接続
    $client.Connect($target, 11026);

    # 送信データ生成
    $sendData = [System.Text.Encoding]::UTF8.GetBytes($MAC_ADDR)

    # MACアドレス送信
    $client.GetStream().Write($sendData, 0, $sendData.Length)
    $client.close()

    Write-Host "送信が完了しました。" -ForegroundColor Green
}
catch
{
    Write-Host "エラーが発生しました。" -ForegroundColor Red
}
```

## 踏み台サーバ
C#で実装しました。

- TCPリスナーで接続を待っている。
- MACアドレスを受け取るとそれをマジックパケットに変換してサブネット内ブロードキャスト発信する。

``` C#:TransportWOL.cs
using System;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;

namespace TransportWOL
{
    class Program
    {
        private static readonly string VVRR = "01-00";

        static void Main(string[] args)
        {
            int port;

            if (args.Length == 1 && int.TryParse(args[0], out port))
            {
                if (0 <= port && port <= 65535)
                {
                    WriteTitle();

                    SocketThreadserver = new SocketThread(port);

                    server.Run();
                }
                else
                {
                    Console.WriteLine($"ポート番号は0～65535の範囲で指定してください。");
                }
            }
            else
            {
                Console.WriteLine($"ポート番号を指定してください。");
            }
        }

        static private void WriteTitle()
        {
            Console.WriteLine($" ################################################################################");
            Console.WriteLine($" ##                                                                            ##");
            Console.WriteLine($" ## WOL 踏み台サーバ                                                           ##");
            Console.WriteLine($" ##                                                                Ver : {VVRR} ##");
            Console.WriteLine($" ##                                                                            ##");
            Console.WriteLine($" ################################################################################");
            Console.WriteLine($"");
        }
    }

    class SocketThread
    {
        private readonly int HEADLEN = 6;
        private readonly int MACLEN = 6;
        private readonly int MACCOUNT = 16;
        private readonly int SENDPORT = 9;
        private readonly int CATEGORYLEN = 20;

        private string Address;
        private string SubnetMask;
        private string BroadCast;
        private int Port;

        // サーバーのエンドポイント
        public IPEndPoint ServerEndPoint;
        // ブロードキャストのエンドポイント
        public IPEndPoint BroadCastEndPoint;

        // スレッド待機用
        private ManualResetEvent AllDone = new ManualResetEvent(false);

        //送受信文字列エンコード
        private Encoding enc = Encoding.UTF8;

        // コンストラクタ
        public SvSocketThread(int port)
        {
            // IPアドレス取得
            string hostname = Dns.GetHostName();
            var address = Dns.GetHostAddresses(hostname);
            foreach (var item in address)
            {
                if (item.AddressFamily == AddressFamily.InterNetwork && item.ToString().StartsWith("10."))
                {
                    this.Address = item.ToString();
                    Console.WriteLine($" サーバIPアドレス        ：{this.Address}");
                }
            }

            // サブネットマスクの取得
            var info = NetworkInterface.GetAllNetworkInterfaces();
            foreach (var item in info)
            {
                if (item.OperationalStatus == OperationalStatus.Up &&
                    item.NetworkInterfaceType != NetworkInterfaceType.Loopback &&
                    item.NetworkInterfaceType != NetworkInterfaceType.Tunnel)
                {
                    var ipp = item.GetIPProperties();
                    if (ipp != null)
                    {
                        foreach (var ip in ipp.UnicastAddresses)
                        {
                            if (this.Address == ip.Address.ToString())
                            {
                                this.SubnetMask = ip.IPv4Mask.ToString();
                                Console.WriteLine($" サーバサブネットマスク  ：{this.SubnetMask}");
                            }
                        }
                    }
                }
            }

            this.Port = port;
            Console.WriteLine($" サーバ待機ポート        ：{this.Port}");

            // ブロードキャストアドレスの取得
            byte[] ipb = new byte[4];
            var ips = this.Address.Split('.');
            var masks = this.SubnetMask.Split('.');
            for (int i = 0; i < 4; i++)
            {
                ipb[i] = (byte)(byte.Parse(ips[i]) | ~byte.Parse(masks[i]));
            }
            this.BroadCast = $"{ipb[0].ToString()}.{ipb[1].ToString()}.{ipb[2].ToString()}.{ipb[3].ToString()}";
            Console.WriteLine($" ブロードキャストアドレス：{this.BroadCast}\n");

            this.ServerEndPoint = new IPEndPoint(IPAddress.Parse(this.Address), Convert.ToInt32(this.Port));
            this.BroadCastEndPoint = new IPEndPoint(IPAddress.Parse(this.BroadCast), this.SENDPORT);
        }

        // サーバー起動
        public void Run()
        {
            using
            (
              var listenerSocket = new Socket(AddressFamily.InterNetwork,
                                              SocketType.Stream,
                                              ProtocolType.Tcp)
            )
            {
                // ソケットをアドレスにバインドする
                listenerSocket.SetSocketOption(SocketOptionLevel.Socket,
                                               SocketOptionName.ReuseAddress,
                                               true);
                listenerSocket.Bind(this.ServerEndPoint);

                // 接続待機開始
                listenerSocket.Listen(10);
                WriteLog($"接続待機開始", $"{listenerSocket.LocalEndPoint.ToString()}");

                // 接続待機のループ
                while (true)
                {
                    AllDone.Reset();
                    listenerSocket.BeginAccept(new AsyncCallback(AcceptCallback), listenerSocket);
                    // 接続があるまでスレッドを待機させる
                    AllDone.WaitOne();
                }
            }
        }

        // 接続受付時のコールバック処理
        private void AcceptCallback(IAsyncResult asyncResult)
        {
            // 待機スレッドが進行するようにシグナルをセット
            AllDone.Set();

            // ソケットを取得
            var listenerSocket = asyncResult.AsyncState as Socket;
            var clientSocket = listenerSocket.EndAccept(asyncResult);

            // 接続中のクライアントを追加
            WriteLog($"接続確認", $"{clientSocket.RemoteEndPoint}");

            // StateObjectを作成
            var state = new StateObject();
            state.ClientSocket = clientSocket;

            // 受信時のコードバック処理を設定
            clientSocket.BeginReceive(state.Buffer,
                         0,
                         StateObject.BufferSize,
                         0,
                         new AsyncCallback(ReceiveCallback),
                         state);
        }

        // 受信時のコードバック処理
        private void ReceiveCallback(IAsyncResult asyncResult)
        {
            // StateObjectとクライアントソケットを取得
            var state = asyncResult.AsyncState as StateObject;
            var clientSocket = state.ClientSocket;

            try
            {
                // クライアントソケットから受信データを取得終了
                int bytes = clientSocket.EndReceive(asyncResult);
                string category = $"データ受信[{bytes,2}バイト]";
                string detail;

                if (bytes > 0)
                {
                    // 受信した文字列を表示
                    var content = enc.GetString(state.Buffer, 0, bytes);
                    detail = $"\"{content}\"";

                    Regex reg = new Regex("^([0-9a-fA-F][0-9a-fA-F])-([0-9a-fA-F][0-9a-fA-F])-([0-9a-fA-F][0-9a-fA-F])-([0-9a-fA-F][0-9a-fA-F])-([0-9a-fA-F][0-9a-fA-F])-([0-9a-fA-F][0-9a-fA-F])$");

                    if (reg.IsMatch(content))
                    {
                        detail += $" MACアドレスデータを受信しました。";
                        WriteLog(category, detail);

                        byte[] sendBytes = new byte[HEADLEN + MACLEN * MACCOUNT];

                        int i;

                        // ヘッダデータを生成
                        for (i = 0; i < HEADLEN; i++)
                        {
                            sendBytes[i] = 0xFF;
                        }

                        // マジックパケットを生成
                        foreach (Match m in reg.Matches(content))
                        {
                            for (i = 0; i < MACLEN; i++)
                            {
                                byte convert = Convert.ToByte(m.Groups[i + 1].Value, 16);

                                for (int j = 0; j < MACCOUNT; j++)
                                {
                                    sendBytes[HEADLEN + MACLEN * j + i] = convert;
                                }
                            }
                        }

                        UdpClient udp = new UdpClient();

                        // マジックパケットを送信する
                        udp.Send(sendBytes, sendBytes.Length, this.BroadCastEndPoint);

                        // ソケットクローズ
                        udp.Close();

                        WriteLog($"データ送信", $"マジックパケットを送信しました。");
                    }
                    else
                    {
                        detail += $" 不明なデータを破棄しました。";
                        WriteLog(category, detail);
                    }

                    // 受信時のコードバック処理を再設定
                    clientSocket.BeginReceive(state.Buffer,
                                 0,
                                 StateObject.BufferSize,
                                 0,
                                 new AsyncCallback(ReceiveCallback),
                                 state);
                }
                else
                {
                    // 0バイトデータの受信時は、切断されたとき
                    clientSocket.Close();
                    WriteLog(category, $"通信を切断しました。");
                }
            }
            catch (SocketException e)
            {
                if (e.NativeErrorCode.Equals(10054))
                {
                    // 既存の接続が、リモート ホストによって強制的に切断されました
                    // 保持しているクライアントの情報をクリアする
                    clientSocket.Close();
                    WriteLog($"強制切断", $"クライアントが強制切断しました。");
                }
                else
                {
                    WriteLog($"強制切断", $"Error Code {e.NativeErrorCode} : {e.Message}");
                }
            }
            catch (Exception ex)
            {
                WriteLog($"例外発生", $"{ex.Message}");
            }
        }

        private void WriteLog(string category, string detail)
        {
            Encoding sjisEnc = Encoding.GetEncoding("Shift_JIS");
            string padStr = category + new string(' ', CATEGORYLEN - sjisEnc.GetByteCount(category));

            Console.WriteLine($" <{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.ffffff")}> -{padStr} ： {detail}");
        }
    }
    
    // 接続されたクライアントの情報を格納するクラス
    public class StateObject
    {
        public Socket ClientSocket { get; set; }
        public const int BufferSize = 1024;
        public byte[] Buffer { get; } = new byte[BufferSize];
    }
}
```