PowershellでNTPサーバを書く
NTPの通信をテストしたいのにWindows PCしかない残念な現場があったので、PowershellでNTPサーバを書いてみました。
前提
- 今どきのWindowsなら必ず動くであろうPowershell V2.0で実装。
- NTPサーバが待ち受けするUDP/123ポートはWindows Timeというサービスが使用しているので、スクリプトを実行するときはこいつを止める必要があります。
- 通信の待ち受けは無限ループになっているので終了は×でウインドウを閉じて強制終了。
ctrl+c
でもいいんですが、リスニングしていたポートを解放する処理$client.close()
がうまく実装できなかったので、次回以降起動できません。ウインドウを閉じると解放されるみたいです。- NTPはバージョン3or4です。
- 精度は度外視です。NTPの動作が確認できることを目的に作成しています。
コード
param([switch]$v) function Convert-Datetime2Byte{ param([datetime]$datetime) $arrInt_unixtime=@() $arrByte_unixtime=New-Object System.Byte[] 0 $double_unixtime=($datetime-([datetime]"1900/1/1 9:0:0")).TotalSeconds $arrInt_unixtime+=[System.UInt32]([math]::Truncate($double_unixtime)) $arrInt_unixtime+=[system.uint32]($double_unixtime%1*1000000000) $arrInt_unixtime | ForEach-Object { for($i=3;$i -ge 0;$i--){ $arrByte_unixtime+=(($_-band (255*([math]::Pow(256,$i))))/([math]::Pow(256,$i))) } } return $arrByte_unixtime } $ErrorActionPreference="stop" if($v){ $VerbosePreference="continue" }else{ $VerbosePreference="silentlycontinue" } $client=New-Object System.Net.Sockets.UdpClient(123) while($true){ $handler=$client.BeginReceive($null,$null) Write-Verbose "Listen UDP/123..." while(!$handler.IsCompleted){ } $rcvTime=Get-Date $remoteEnd=New-Object System.Net.IPEndPoint([system.net.ipaddress]::None,$null) $rcvByte=$client.EndReceive($handler,[ref]$remoteEnd) switch($rcvByte[0] -band 63){ 35{ Write-Verbose "NTP ver.4 mode client received from $remoteEnd." $responseFlags=36 } 27{ Write-Verbose "NTP ver.3 mode client received from $remoteEnd." $responseFlags=28 } default{ Write-Verbose "Illegal packet or unsupported NTP version received from $remoteEnd." $responseFlags=$null } } if($responseFlags -ne $null){ $sndByte=New-Object System.Byte[] 0 $sndByte+=$responseFlags $sndByte+=@(1,0,0,0,0,0,0,0,0,0,0,0,0,0,0) $sndByte+=Convert-Datetime2Byte (Get-Date) $sndByte+=$rcvByte[($rcvByte.Length-8)..$rcvByte.Length] $sndByte+=Convert-Datetime2Byte $rcvTime $sndByte+=Convert-Datetime2Byte (Get-Date) $client.Send($sndByte,$sndByte.Length,$remoteEnd) >$null Write-Verbose "Sent NTP response to $remoteEnd." } }
使い方
Server-NTP.ps1
-v
オプションでうるさくなります。
終了するときはウインドウごと閉じます。
コード解説
おおまかに言うと、.NETのUDPClientを使ってUDP/123をリッスンし、リクエストが来たら返答する簡単なスクリプトです。
リクエスト待ち受け
$client=New-Object System.Net.Sockets.UdpClient(123) while($true){ $handler=$client.BeginReceive($null,$null) Write-Verbose "Listen UDP/123..." while(!$handler.IsCompleted){ } $rcvTime=Get-Date $remoteEnd=New-Object System.Net.IPEndPoint([system.net.ipaddress]::None,$null) $rcvByte=$client.EndReceive($handler,[ref]$remoteEnd)
UDP/123を非同期でリスニングします。
$true
になったら即座に$rcvTime=Get-Date
でその時刻を記録します。
本当はwhile()
で判定するのではなく、$client.BeginReceive($null,$null)
でコールバックを指定したかったんですが、使い方がいまいちわからなかった。コールバックのほうが精度も高くなる気がする。
パケットを受け取ったらバイト型配列$rcvByte
に格納します。
リクエストの識別
switch($rcvByte[0] -band 63){ 35{ Write-Verbose "NTP ver.4 mode client received from $remoteEnd." $responseFlags=36 } 27{ Write-Verbose "NTP ver.3 mode client received from $remoteEnd." $responseFlags=28 } default{ Write-Verbose "Illegal packet or unsupported NTP version received from $remoteEnd." $responseFlags=$null } }
受信したパケットの先頭1byteにバージョン情報が含まれているので識別します。
上位2bitは不要なので$rcvByte[0] -band 63
でマスクをかけて除外しています。(63
は0b00111111
)
レスポンス送信
if($responseFlags -ne $null){ $sndByte=New-Object System.Byte[] 0 $sndByte+=$responseFlags $sndByte+=@(1,0,0,0,0,0,0,0,0,0,0,0,0,0,0) $sndByte+=Convert-Datetime2Byte (Get-Date) $sndByte+=$rcvByte[($rcvByte.Length-8)..$rcvByte.Length] $sndByte+=Convert-Datetime2Byte $rcvTime $sndByte+=Convert-Datetime2Byte (Get-Date) $client.Send($sndByte,$sndByte.Length,$remoteEnd) >$null Write-Verbose "Sent NTP response to $remoteEnd." }
レスポンスを作成し、送信します。時刻はConvert-Datetime2Byte
でNTPの64bitのフォーマットに変換しています。
Convert-Datetime2Byte
function Convert-Datetime2Byte{ param([datetime]$datetime) $arrInt_unixtime=@() $arrByte_unixtime=New-Object System.Byte[] 0 $double_unixtime=($datetime-([datetime]"1900/1/1 9:0:0")).TotalSeconds $arrInt_unixtime+=[System.UInt32]([math]::Truncate($double_unixtime)) $arrInt_unixtime+=[system.uint32]($double_unixtime%1*1000000000) $arrInt_unixtime | ForEach-Object { for($i=3;$i -ge 0;$i--){ $arrByte_unixtime+=(($_-band (255*([math]::Pow(256,$i))))/([math]::Pow(256,$i))) } } return $arrByte_unixtime }
DateTime
型の日時をNTPのフォーマットに変換し、バイト型配列で返します。
NTPではUTCをやり取りするので日本標準時のオフセットを加えて、[datetime]"1900/1/1 9:0:0"
としています。
9~10行目で浮動小数点型の秒数を整数部32bit、小数点以下32bitに分割し、配列に代入しています。
$arrByte_unixtime+=(($_-band (255*([math]::Pow(256,$i))))/([math]::Pow(256,$i)))
は、32bitの数字を8bitごとに分割するために、0b11111111=255
のマスクを上から順々にかけて値を取り出しています。
イケてないところ
どなたか教えてください、、、
- スクリプト終了時に
$client.close()
を実行できていないので、終了後にポートが解放されない。 - パケットの待ち受けが
while()
のループ。$client.BeginReceive($null,$null)
でコールバックを使いたい。