2015-03-14 20:28:14 +03:00
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Contributor: Julien Vehent jvehent@mozilla.com [:ulfr]
2015-08-27 17:41:13 +03:00
/ * The timedrift module evaluate the local time of a target against
network time retrieved using NTP .
Usage documentation is online at http : //mig.mozilla.org/doc/module_timedrift.html
* /
2018-07-11 20:11:22 +03:00
package timedrift /* import "github.com/mozilla/mig/modules/timedrift" */
2015-03-14 20:28:14 +03:00
import (
"bufio"
"encoding/json"
"flag"
"fmt"
2018-07-11 20:11:22 +03:00
"github.com/mozilla/mig/modules"
2015-03-14 20:28:14 +03:00
"net"
"os"
2015-10-19 13:56:53 +03:00
"strings"
2015-03-14 20:28:14 +03:00
"time"
)
2015-05-22 04:24:16 +03:00
type module struct {
}
2015-05-22 05:22:06 +03:00
func ( m * module ) NewRun ( ) modules . Runner {
2015-05-22 04:51:04 +03:00
return new ( run )
2015-05-22 04:24:16 +03:00
}
2015-03-14 20:28:14 +03:00
func init ( ) {
2015-05-22 04:24:16 +03:00
modules . Register ( "timedrift" , new ( module ) )
2015-03-14 20:28:14 +03:00
}
2015-05-22 04:51:04 +03:00
type run struct {
2015-03-14 20:28:14 +03:00
Parameters params
2015-04-29 20:10:30 +03:00
Results modules . Result
2015-03-14 20:28:14 +03:00
}
// a simple parameters structure, the format is arbitrary
type params struct {
Drift string ` json:"drift" `
}
2015-04-29 20:10:30 +03:00
type elements struct {
2015-03-14 20:28:14 +03:00
HasCheckedDrift bool ` json:"hascheckeddrift" `
IsWithinDrift bool ` json:"iswithindrift,omitempty" `
Drifts [ ] string ` json:"drifts,omitempty" `
LocalTime string ` json:"localtime" `
}
type statistics struct {
ExecTime string ` json:"exectime" `
NtpStats [ ] ntpstats ` json:"ntpstats,omitempty" `
}
type ntpstats struct {
Host string ` json:"host" `
Time time . Time ` json:"time" `
Latency string ` json:"latency" `
Drift string ` json:"drift" `
Reachable bool ` json:"reachable" `
}
2015-10-19 13:56:53 +03:00
var NtpBackupPool = [ ] string {
` time.nist.gov ` ,
` 0.pool.ntp.org ` ,
` 1.pool.ntp.org ` ,
` 2.pool.ntp.org ` ,
` 3.pool.ntp.org ` }
2015-05-22 04:51:04 +03:00
func ( r * run ) ValidateParameters ( ) ( err error ) {
2015-03-14 20:28:14 +03:00
if r . Parameters . Drift != "" {
_ , err = time . ParseDuration ( r . Parameters . Drift )
}
return err
}
2016-12-16 18:55:21 +03:00
func ( r * run ) Run ( in modules . ModuleReader ) ( out string ) {
2015-03-14 20:28:14 +03:00
var (
2015-10-19 13:56:53 +03:00
stats statistics
el elements
drift time . Duration
ntpFile * os . File
ntpScan * bufio . Scanner
ntpPool [ ] string
2015-03-14 20:28:14 +03:00
)
2015-04-29 20:10:30 +03:00
defer func ( ) {
if e := recover ( ) ; e != nil {
r . Results . Errors = append ( r . Results . Errors , fmt . Sprintf ( "%v" , e ) )
r . Results . Success = false
buf , _ := json . Marshal ( r . Results )
out = string ( buf [ : ] )
}
} ( )
el . LocalTime = time . Now ( ) . Format ( time . RFC3339Nano )
2015-03-14 20:28:14 +03:00
t1 := time . Now ( )
2015-05-08 00:34:00 +03:00
err := modules . ReadInputParameters ( in , & r . Parameters )
2015-03-14 20:28:14 +03:00
if err != nil {
2015-04-29 20:10:30 +03:00
panic ( err )
2015-03-14 20:28:14 +03:00
}
err = r . ValidateParameters ( )
if err != nil {
2015-04-29 20:10:30 +03:00
panic ( err )
2015-03-14 20:28:14 +03:00
}
// if drift is not set, skip the ntp test
if r . Parameters . Drift == "" {
r . Results . FoundAnything = true
goto done
}
drift , err = time . ParseDuration ( r . Parameters . Drift )
if err != nil {
2015-04-29 20:10:30 +03:00
panic ( err )
2015-03-14 20:28:14 +03:00
}
// assume host has synched time and set to false if not true
2015-04-29 20:10:30 +03:00
el . IsWithinDrift = true
2015-10-19 13:56:53 +03:00
//Load ntp servers from /etc/ntp.conf
ntpFile , err = os . Open ( "/etc/ntp.conf" )
if err != nil {
r . Results . Errors = append ( r . Results . Errors ,
fmt . Sprintf ( "Using backup NTP hosts. Failed to read /etc/ntp.conf with error '%v'" , err ) )
} else {
defer ntpFile . Close ( )
ntpScan = bufio . NewScanner ( ntpFile )
for ntpScan . Scan ( ) {
ntpFields := strings . Fields ( ntpScan . Text ( ) )
if len ( ntpFields ) < 2 {
continue
}
if ntpFields [ 0 ] == "server" {
ntpPool = append ( ntpPool , ntpFields [ 1 ] )
}
}
}
//Add our hardcoded online servers to the end of our ntpPool as fallbacks
ntpPool = append ( ntpPool , NtpBackupPool ... )
2015-03-30 15:53:41 +03:00
// attempt to get network time from each of the NTP servers, and exit
// as soon as we get a valid result from one of them
2015-10-19 13:56:53 +03:00
for _ , ntpsrv := range ntpPool {
2015-03-30 15:53:41 +03:00
t , lat , err := GetNetworkTime ( ntpsrv )
if err != nil {
// failed to get network time, log a failure and try another one
stats . NtpStats = append ( stats . NtpStats , ntpstats {
Host : ntpsrv ,
Reachable : false ,
} )
continue
}
// compare network time to local time
2015-03-14 20:28:14 +03:00
localtime := time . Now ( )
if err != nil {
r . Results . Errors = append ( r . Results . Errors , fmt . Sprintf ( "%v" , err ) )
continue
}
if localtime . Before ( t . Add ( - drift ) ) {
2015-04-29 20:10:30 +03:00
el . IsWithinDrift = false
el . Drifts = append ( el . Drifts , fmt . Sprintf ( "Local time is behind ntp host %s by %s" , ntpsrv , t . Sub ( localtime ) . String ( ) ) )
2015-03-14 20:28:14 +03:00
} else if localtime . After ( t . Add ( drift ) ) {
2015-04-29 20:10:30 +03:00
el . IsWithinDrift = false
el . Drifts = append ( el . Drifts , fmt . Sprintf ( "Local time is ahead of ntp host %s by %s" , ntpsrv , localtime . Sub ( t ) . String ( ) ) )
2015-03-14 20:28:14 +03:00
}
stats . NtpStats = append ( stats . NtpStats , ntpstats {
2015-03-30 15:53:41 +03:00
Host : ntpsrv ,
2015-03-14 20:28:14 +03:00
Time : t ,
Latency : lat ,
Drift : localtime . Sub ( t ) . String ( ) ,
Reachable : true ,
} )
2015-04-29 20:10:30 +03:00
el . HasCheckedDrift = true
2015-03-30 15:53:41 +03:00
// comparison succeeded, exit the loop
break
2015-03-14 20:28:14 +03:00
}
2015-04-29 20:10:30 +03:00
if ! el . IsWithinDrift {
2015-03-15 19:35:46 +03:00
r . Results . FoundAnything = true
}
2015-03-14 20:28:14 +03:00
done :
stats . ExecTime = time . Now ( ) . Sub ( t1 ) . String ( )
2015-04-29 20:10:30 +03:00
out = r . buildResults ( el , stats )
return
2015-03-14 20:28:14 +03:00
}
2015-03-30 15:53:41 +03:00
// GetNetworkTime queries a given NTP server to obtain the network time
func GetNetworkTime ( host string ) ( t time . Time , latency string , err error ) {
2015-03-14 20:28:14 +03:00
raddr , err := net . ResolveUDPAddr ( "udp" , host + ":123" )
if err != nil {
return
}
// NTP request is 48 bytes long, we only set the first byte
data := make ( [ ] byte , 48 )
// Flags: 0x1b (27)
// 00...... leap indicator (0)
// ..011... version number (3)
// .....011 mode: client (3)
data [ 0 ] = 3 << 3 | 3
t1 := time . Now ( )
con , err := net . DialUDP ( "udp" , nil , raddr )
if err != nil {
return
}
defer con . Close ( )
// send the request
_ , err = con . Write ( data )
if err != nil {
return
}
// wait up to 5 seconds for the response
con . SetDeadline ( time . Now ( ) . Add ( 5 * time . Second ) )
// read up to 48 bytes from the response
_ , err = con . Read ( data )
if err != nil {
return
}
2015-03-30 15:53:41 +03:00
latency = time . Now ( ) . Sub ( t1 ) . String ( )
2015-03-14 20:28:14 +03:00
// Response format (from the RFC)
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |LI | VN |Mode | Stratum | Poll | Precision |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Root Delay |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Root Dispersion |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Reference ID |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | |
// + Reference Timestamp (64) +
// | |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | |
// + Origin Timestamp (64) +
// | |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | |
// + Receive Timestamp (64) +
// | |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | |
// + Transmit Timestamp (64) +
// | |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
var sec , frac uint64
sec = uint64 ( data [ 43 ] ) | uint64 ( data [ 42 ] ) << 8 | uint64 ( data [ 41 ] ) << 16 | uint64 ( data [ 40 ] ) << 24
frac = uint64 ( data [ 47 ] ) | uint64 ( data [ 46 ] ) << 8 | uint64 ( data [ 45 ] ) << 16 | uint64 ( data [ 44 ] ) << 24
2015-03-30 15:53:41 +03:00
if sec == 0 || frac == 0 {
err = fmt . Errorf ( "null response received from NTP host" )
return
}
2015-03-14 20:28:14 +03:00
nsec := sec * 1e9
nsec += ( frac * 1e9 ) >> 32
t = time . Date ( 1900 , 1 , 1 , 0 , 0 , 0 , 0 , time . UTC ) . Add ( time . Duration ( nsec ) ) . Local ( )
return
}
// buildResults marshals the results
2015-05-22 04:51:04 +03:00
func ( r * run ) buildResults ( el elements , stats statistics ) string {
2015-04-29 20:10:30 +03:00
r . Results . Elements = el
r . Results . Statistics = stats
2015-03-14 20:28:14 +03:00
if len ( r . Results . Errors ) == 0 {
r . Results . Success = true
}
2015-03-30 16:51:52 +03:00
// if was supposed to check drift but hasn't, set success to false
2015-04-29 20:10:30 +03:00
if r . Parameters . Drift != "" && ! el . HasCheckedDrift {
2015-03-30 16:51:52 +03:00
r . Results . Success = false
}
2015-03-14 20:28:14 +03:00
jsonOutput , err := json . Marshal ( r . Results )
if err != nil {
panic ( err )
}
return string ( jsonOutput [ : ] )
}
2015-05-22 04:51:04 +03:00
func ( r * run ) PrintResults ( result modules . Result , foundOnly bool ) ( prints [ ] string , err error ) {
2015-04-29 20:10:30 +03:00
var (
el elements
stats statistics
)
err = result . GetElements ( & el )
2015-03-14 20:28:14 +03:00
if err != nil {
return
}
2015-04-29 20:10:30 +03:00
prints = append ( prints , "local time is " + el . LocalTime )
if el . HasCheckedDrift {
if el . IsWithinDrift {
2015-03-14 20:28:14 +03:00
prints = append ( prints , "local time is within acceptable drift from NTP servers" )
} else {
prints = append ( prints , "local time is out of sync from NTP servers" )
2015-04-29 20:10:30 +03:00
for _ , drift := range el . Drifts {
2015-03-14 20:28:14 +03:00
prints = append ( prints , drift )
}
}
}
// stop here if foundOnly is set, we don't want to see errors and stats
if foundOnly {
return
}
2015-04-29 20:10:30 +03:00
for _ , e := range result . Errors {
2015-03-14 20:28:14 +03:00
prints = append ( prints , "error:" , e )
}
2015-04-29 20:10:30 +03:00
err = result . GetStatistics ( & stats )
if err != nil {
panic ( err )
}
prints = append ( prints , "stat: execution time was " + stats . ExecTime )
for _ , ntpstat := range stats . NtpStats {
2015-03-14 20:28:14 +03:00
if ntpstat . Reachable {
prints = append ( prints , "stat: " + ntpstat . Host + " responded in " + ntpstat . Latency + " with time " + ntpstat . Time . UTC ( ) . String ( ) + ". local time drifts by " + ntpstat . Drift )
} else {
prints = append ( prints , "stat: " + ntpstat . Host + " was unreachable" )
}
}
2015-04-29 20:10:30 +03:00
if result . Success {
2015-03-30 16:51:52 +03:00
prints = append ( prints , fmt . Sprintf ( "timedrift module has succeeded" ) )
} else {
prints = append ( prints , fmt . Sprintf ( "timedrift module has failed" ) )
}
2015-03-14 20:28:14 +03:00
return
}
func printHelp ( isCmd bool ) {
dash := ""
if isCmd {
dash = "-"
}
2015-03-15 19:36:12 +03:00
fmt . Printf ( ` timedrift returns the local time of a system and , when % sdrift is set ,
verifies that local time is within acceptable range of network time by querying NTP servers
2015-03-14 20:28:14 +03:00
% sdrift < duration > allowed time drift window . a value of "5s" compares local
time with ntp hosts and returns a drift failure if local
time is too far out of sync .
If no drift is set , the module only returns local time .
` , dash , dash )
}
2015-05-22 04:51:04 +03:00
func ( r * run ) ParamsCreator ( ) ( interface { } , error ) {
2015-04-19 19:07:13 +03:00
fmt . Println ( "initializing timedrift parameters creation" )
2015-03-14 20:28:14 +03:00
var err error
var p params
printHelp ( false )
scanner := bufio . NewScanner ( os . Stdin )
for {
fmt . Printf ( "drift> " )
scanner . Scan ( )
if err := scanner . Err ( ) ; err != nil {
fmt . Println ( "Invalid input. Try again" )
continue
}
input := scanner . Text ( )
if input == "help" {
printHelp ( false )
continue
}
if input != "" {
_ , err = time . ParseDuration ( input )
if err != nil {
fmt . Println ( "invalid drift duration. try again. ex: drift> 5s" )
continue
}
}
p . Drift = input
break
}
2015-04-19 19:07:13 +03:00
r . Parameters = p
return r . Parameters , r . ValidateParameters ( )
2015-03-14 20:28:14 +03:00
}
2015-05-22 04:51:04 +03:00
func ( r * run ) ParamsParser ( args [ ] string ) ( interface { } , error ) {
2015-03-14 20:28:14 +03:00
var (
err error
drift string
fs flag . FlagSet
)
2015-03-15 19:36:12 +03:00
if len ( args ) >= 1 && args [ 0 ] == "help" {
2015-03-14 20:28:14 +03:00
printHelp ( true )
2015-03-15 19:36:12 +03:00
return nil , fmt . Errorf ( "help printed" )
}
if len ( args ) == 0 {
return r . Parameters , nil
2015-03-14 20:28:14 +03:00
}
fs . Init ( "time" , flag . ContinueOnError )
fs . StringVar ( & drift , "drift" , "" , "see help" )
err = fs . Parse ( args )
if err != nil {
return nil , err
}
_ , err = time . ParseDuration ( drift )
if err != nil {
return nil , fmt . Errorf ( "invalid drift duration. try help." )
}
2015-03-15 19:36:12 +03:00
r . Parameters . Drift = drift
return r . Parameters , r . ValidateParameters ( )
2015-03-14 20:28:14 +03:00
}