diff --git a/config/README.md b/config/README.md index 1eba9a24..2fe3b510 100644 --- a/config/README.md +++ b/config/README.md @@ -211,6 +211,23 @@ comm method and add in your contact list if available. }, ``` + +## Configure Network Time Server + ++ To configure and enable a NTP server you need to set the "enabled" field to one of 3 values -1 is disabled 0 is enabled and alert at start up 1 is enabled and warn at start up +servers are configured by the pool array and attempted first to last allowedDifference and allowedNegativeDifference are how far ahead and behind is acceptable for the time to be out in nanoseconds + +```js + "ntpclient": { + "enabled": 0, + "pool": [ + "pool.ntp.org:123" + ], + "allowedDifference": 0, + "allowedNegativeDifference": 0 + }, + ``` + ### Please click GoDocs chevron above to view current GoDoc information for this package ## Contribution diff --git a/config/config.go b/config/config.go index c32291d1..50df97ba 100644 --- a/config/config.go +++ b/config/config.go @@ -1,14 +1,17 @@ package config import ( + "bufio" "encoding/json" "errors" "flag" "fmt" + "io" "os" "path" "runtime" "strconv" + "strings" "sync" "time" @@ -32,6 +35,8 @@ const ( configPairsLastUpdatedWarningThreshold = 30 // 30 days configDefaultHTTPTimeout = time.Second * 15 configMaxAuthFailres = 3 + defaultNTPAllowedDifference = 50000000 + defaultNTPAllowedNegativeDifference = 50000000 ) // Constants here hold some messages @@ -104,6 +109,7 @@ type Config struct { GlobalHTTPTimeout time.Duration `json:"globalHTTPTimeout"` Logging log.Logging `json:"logging"` Profiler ProfilerConfig `json:"profiler"` + NTPClient NTPClientConfig `json:"ntpclient"` Currency CurrencyConfig `json:"currencyConfig"` Communications CommunicationsConfig `json:"communications"` Portfolio portfolio.Base `json:"portfolioAddresses"` @@ -122,6 +128,13 @@ type ProfilerConfig struct { Enabled bool `json:"enabled"` } +type NTPClientConfig struct { + Level int `json:"enabled"` + Pool []string `json:"pool"` + AllowedDifference *time.Duration `json:"allowedDifference"` + AllowedNegativeDifference *time.Duration `json:"allowedNegativeDifference"` +} + // ExchangeConfig holds all the information needed for each enabled Exchange. type ExchangeConfig struct { Name string `json:"name"` @@ -1086,6 +1099,62 @@ func (c *Config) CheckLoggerConfig() error { return nil } +// CheckNTPConfig() checks for missing or incorrectly configured NTPClient and recreates with known safe defaults +func (c *Config) CheckNTPConfig() { + m.Lock() + defer m.Unlock() + + if c.NTPClient.AllowedDifference == nil || *c.NTPClient.AllowedDifference == 0 { + c.NTPClient.AllowedDifference = new(time.Duration) + *c.NTPClient.AllowedDifference = defaultNTPAllowedDifference + } + + if c.NTPClient.AllowedNegativeDifference == nil || *c.NTPClient.AllowedNegativeDifference <= 0 { + c.NTPClient.AllowedNegativeDifference = new(time.Duration) + *c.NTPClient.AllowedNegativeDifference = defaultNTPAllowedNegativeDifference + } + + if len(c.NTPClient.Pool) < 1 { + log.Warn("NTPClient enabled with no servers configured enabling default pool") + c.NTPClient.Pool = []string{"pool.ntp.org:123"} + } +} + +// DisableNTPCheck() allows the user to change how they are prompted for timesync alerts +func (c *Config) DisableNTPCheck(input io.Reader) (string, error) { + m.Lock() + defer m.Unlock() + + reader := bufio.NewReader(input) + log.Warn("Your system time is out of sync this may cause issues with trading") + log.Warn("How would you like to show future notifications? (a)lert / (w)arn / (d)isable \n") + + var answered = false + for ok := true; ok; ok = (!answered) { + answer, err := reader.ReadString('\n') + if err != nil { + return "", err + } + + answer = strings.TrimRight(answer, "\r\n") + switch answer { + case "a": + c.NTPClient.Level = 0 + answered = true + return "Time sync has been set to alert", nil + case "w": + c.NTPClient.Level = 1 + answered = true + return "Time sync has been set to warn only", nil + case "d": + c.NTPClient.Level = -1 + answered = true + return "Future notications for out time sync have been disabled", nil + } + } + return "", errors.New("something went wrong NTPCheck should never make it this far") +} + // GetFilePath returns the desired config file or the default config file name // based on if the application is being run under test or normal mode. func GetFilePath(file string) (string, error) { diff --git a/config/config_test.go b/config/config_test.go index 46a526ba..73a0507a 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1,11 +1,13 @@ package config import ( + "strings" "testing" "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/currency" log "github.com/thrasher-/gocryptotrader/logger" + "github.com/thrasher-/gocryptotrader/ntpclient" ) const ( @@ -967,3 +969,61 @@ func TestCheckLoggerConfig(t *testing.T) { t.Errorf("Failed to create logger with user settings: reason: %v", err) } } + +func TestDisableNTPCheck(t *testing.T) { + c := GetConfig() + err := c.LoadConfig(ConfigTestFile) + if err != nil { + t.Fatal(err) + } + + warn, err := c.DisableNTPCheck(strings.NewReader("w\n")) + if err != nil { + t.Fatalf("test failed to create ntpclient failed reason: %v", err) + } + + if warn != "Time sync has been set to warn only" { + t.Errorf("failed expected %v got %v", "Time sync has been set to warn only", warn) + } + alert, _ := c.DisableNTPCheck(strings.NewReader("a\n")) + if alert != "Time sync has been set to alert" { + t.Errorf("failed expected %v got %v", "Time sync has been set to alert", alert) + } + + disable, _ := c.DisableNTPCheck(strings.NewReader("d\n")) + if disable != "Future notications for out time sync have been disabled" { + t.Errorf("failed expected %v got %v", "Future notications for out time sync have been disabled", disable) + } + + _, err = c.DisableNTPCheck(strings.NewReader(" ")) + if err.Error() != "EOF" { + t.Errorf("failed expected EOF got: %v", err) + } +} + +func TestCheckNTPConfig(t *testing.T) { + c := GetConfig() + + c.NTPClient.Level = 0 + c.NTPClient.Pool = nil + c.NTPClient.AllowedNegativeDifference = nil + c.NTPClient.AllowedDifference = nil + + c.CheckNTPConfig() + _, err := ntpclient.NTPClient(c.NTPClient.Pool) + if err != nil { + t.Fatalf("test failed to create ntpclient failed reason: %v", err) + } + + if c.NTPClient.Pool[0] != "pool.ntp.org:123" { + t.Error("ntpclient with no valid pool should default to pool.ntp.org ") + } + + if c.NTPClient.AllowedDifference == nil { + t.Error("ntpclient with nil alloweddifference should default to sane value") + } + + if c.NTPClient.AllowedNegativeDifference == nil { + t.Error("ntpclient with nil allowednegativedifference should default to sane value") + } +} diff --git a/config_example.json b/config_example.json index ce660842..3f68b35e 100644 --- a/config_example.json +++ b/config_example.json @@ -12,6 +12,14 @@ "profiler": { "enabled": false }, + "ntpclient": { + "enabled": 0, + "pool": [ + "pool.ntp.org:123" + ], + "allowedDifference": 50000000, + "allowedNegativeDifference": 50000000 + }, "currencyConfig": { "forexProviders": [ { diff --git a/main.go b/main.go index d08c60fb..45325633 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "runtime" "strconv" "syscall" + "time" "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/communications" @@ -17,6 +18,7 @@ import ( "github.com/thrasher-/gocryptotrader/currency/coinmarketcap" exchange "github.com/thrasher-/gocryptotrader/exchanges" log "github.com/thrasher-/gocryptotrader/logger" + "github.com/thrasher-/gocryptotrader/ntpclient" "github.com/thrasher-/gocryptotrader/portfolio" ) @@ -103,6 +105,30 @@ func main() { log.Errorf("Failed to setup logger reason: %s", err) } + if bot.config.NTPClient.Level != -1 { + bot.config.CheckNTPConfig() + NTPTime, errNTP := ntpclient.NTPClient(bot.config.NTPClient.Pool) + currentTime := time.Now() + if errNTP != nil { + log.Warnf("NTPClient failed to create: %v", errNTP) + } else { + NTPcurrentTimeDifference := NTPTime.Sub(currentTime) + configNTPTime := *bot.config.NTPClient.AllowedDifference + configNTPNegativeTime := (*bot.config.NTPClient.AllowedNegativeDifference - (*bot.config.NTPClient.AllowedNegativeDifference * 2)) + if NTPcurrentTimeDifference > configNTPTime || NTPcurrentTimeDifference < configNTPNegativeTime { + log.Warnf("Time out of sync (NTP): %v | (time.Now()): %v | (Difference): %v | (Allowed): +%v / %v", NTPTime, currentTime, NTPcurrentTimeDifference, configNTPTime, configNTPNegativeTime) + if bot.config.NTPClient.Level == 0 { + disable, errNTP := bot.config.DisableNTPCheck(os.Stdin) + if errNTP != nil { + log.Errorf("failed to disable ntp time check reason: %v", err) + } else { + log.Info(disable) + } + } + } + } + } + AdjustGoMaxProcs() log.Debugf("Bot '%s' started.\n", bot.config.Name) log.Debugf("Bot dry run mode: %v.\n", common.IsEnabled(bot.dryRun)) diff --git a/ntpclient/ntpclient.go b/ntpclient/ntpclient.go new file mode 100644 index 00000000..99015cbd --- /dev/null +++ b/ntpclient/ntpclient.go @@ -0,0 +1,59 @@ +package ntpclient + +import ( + "encoding/binary" + "errors" + "net" + "time" + + log "github.com/thrasher-/gocryptotrader/logger" +) + +type ntppacket struct { + Settings uint8 // leap yr indicator, ver number, and mode + Stratum uint8 // stratum of local clock + Poll int8 // poll exponent + Precision int8 // precision exponent + RootDelay uint32 // root delay + RootDispersion uint32 // root dispersion + ReferenceID uint32 // reference id + RefTimeSec uint32 // reference timestamp sec + RefTimeFrac uint32 // reference timestamp fractional + OrigTimeSec uint32 // origin time secs + OrigTimeFrac uint32 // origin time fractional + RxTimeSec uint32 // receive time secs + RxTimeFrac uint32 // receive time frac + TxTimeSec uint32 // transmit time secs + TxTimeFrac uint32 // transmit time frac +} + +// NTPClient create's a new NTPClient and returns local based on ntp servers provided timestamp +func NTPClient(pool []string) (time.Time, error) { + for i := range pool { + con, err := net.Dial("udp", pool[i]) + if err != nil { + log.Warnf("Unable to connect to hosts %v attempting next", pool[i]) + continue + } + + defer con.Close() + + con.SetDeadline(time.Now().Add(5 * time.Second)) + + req := &ntppacket{Settings: 0x1B} + if err := binary.Write(con, binary.BigEndian, req); err != nil { + continue + } + + rsp := &ntppacket{} + if err := binary.Read(con, binary.BigEndian, rsp); err != nil { + continue + } + + secs := float64(rsp.TxTimeSec) - 2208988800 + nanos := (int64(rsp.TxTimeFrac) * 1e9) >> 32 + + return time.Unix(int64(secs), nanos), nil + } + return time.Unix(0, 0), errors.New("no valid time servers") +} diff --git a/ntpclient/ntpclient_test.go b/ntpclient/ntpclient_test.go new file mode 100644 index 00000000..0668409e --- /dev/null +++ b/ntpclient/ntpclient_test.go @@ -0,0 +1,25 @@ +package ntpclient + +import ( + "testing" +) + +func TestNTPClient(t *testing.T) { + pool := []string{"pool.ntp.org:123", "0.pool.ntp.org:123"} + _, err := NTPClient(pool) + if err != nil { + t.Fatalf("failed to get time %v", err) + } + + invalidpool := []string{"pool.thisisinvalid.org"} + _, err = NTPClient(invalidpool) + if err == nil { + t.Errorf("failed to get time %v", err) + } + + firstInvalid := []string{"pool.thisisinvalid.org", "pool.ntp.org:123", "0.pool.ntp.org:123"} + _, err = NTPClient(firstInvalid) + if err != nil { + t.Errorf("failed to get time %v", err) + } +} diff --git a/testdata/configtest.json b/testdata/configtest.json index 67a737bf..f9feba93 100644 --- a/testdata/configtest.json +++ b/testdata/configtest.json @@ -12,6 +12,15 @@ "profiler": { "enabled": false }, + "ntpclient": { + "enabled": 0, + "pool": [ + "0.pool.ntp.org:123", + "pool.ntp.org:123" + ], + "allowedDifference": 50000000, + "allowedNegativeDifference": 50000000 + }, "currencyConfig": { "forexProviders": [ {