node_exporter/vendor/github.com/beevik/ntp/ntp.go
Chris Batey 387c64424b Use the offset calculation that includes round trip time in the ntp collector
Previously the raw time difference was used which includes the network trip time
between the node and the ntp server. This makes setting alerts off the value
troublesome as it depends on the latency as well as the clock offset.
2016-06-01 08:56:20 +01:00

207 lines
5.1 KiB
Go

// Copyright 2015 Brett Vickers.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package ntp provides a simple mechanism for querying the current time from
// a remote NTP server. This package only supports NTP client mode behavior
// and version 4 of the NTP protocol. See RFC 5905. Approach inspired by go-
// nuts post by Michael Hofmann:
//
// https://groups.google.com/forum/?fromgroups#!topic/golang-nuts/FlcdMU5fkLQ
package ntp
import (
"encoding/binary"
"net"
"time"
)
type mode byte
const (
reserved mode = 0 + iota
symmetricActive
symmetricPassive
client
server
broadcast
controlMessage
reservedPrivate
)
const (
maxStratum = 16
nanoPerSec = 1000000000
)
var (
timeout = 5 * time.Second
ntpEpoch = time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC)
)
type ntpTime struct {
Seconds uint32
Fraction uint32
}
func (t ntpTime) Time() time.Time {
return ntpEpoch.Add(t.sinceEpoch())
}
// sinceEpoch converts the ntpTime record t into a duration since the NTP
// epoch time (Jan 1, 1900).
func (t ntpTime) sinceEpoch() time.Duration {
sec := time.Duration(t.Seconds) * time.Second
frac := time.Duration(uint64(t.Fraction) * nanoPerSec >> 32)
return sec + frac
}
// toNtpTime converts the time value t into an ntpTime representation.
func toNtpTime(t time.Time) ntpTime {
nsec := uint64(t.Sub(ntpEpoch))
return ntpTime{
Seconds: uint32(nsec / nanoPerSec),
Fraction: uint32((nsec % nanoPerSec) << 32 / nanoPerSec),
}
}
// msg is an internal representation of an NTP packet.
type msg struct {
LiVnMode byte // Leap Indicator (2) + Version (3) + Mode (3)
Stratum byte
Poll byte
Precision byte
RootDelay uint32
RootDispersion uint32
ReferenceID uint32
ReferenceTime ntpTime
OriginTime ntpTime
ReceiveTime ntpTime
TransmitTime ntpTime
}
// setVersion sets the NTP protocol version on the message.
func (m *msg) setVersion(v int) {
m.LiVnMode = (m.LiVnMode & 0xc7) | uint8(v)<<3
}
// setMode sets the NTP protocol mode on the message.
func (m *msg) setMode(md mode) {
m.LiVnMode = (m.LiVnMode & 0xf8) | byte(md)
}
// A Response contains time data, some of which is returned by the NTP server
// and some of which is calculated by the client.
type Response struct {
Time time.Time // receive time reported by the server
RTT time.Duration // round-trip time between client and server
ClockOffset time.Duration // local clock offset relative to server
Stratum uint8 // stratum level of NTP server's clock
}
// Query returns information from the remote NTP server specifed as host. NTP
// client mode is used.
func Query(host string, version int) (*Response, error) {
m, err := getTime(host, version)
now := toNtpTime(time.Now())
if err != nil {
return nil, err
}
r := &Response{
Time: m.ReceiveTime.Time(),
RTT: rtt(m.OriginTime, m.ReceiveTime, m.TransmitTime, now),
ClockOffset: offset(m.OriginTime, m.ReceiveTime, m.TransmitTime, now),
Stratum: m.Stratum,
}
// https://tools.ietf.org/html/rfc5905#section-7.3
if r.Stratum == 0 {
r.Stratum = maxStratum
}
return r, nil
}
// Time returns the "receive time" from the remote NTP server specifed as
// host. NTP client mode is used.
func getTime(host string, version int) (*msg, error) {
if version < 2 || version > 4 {
panic("ntp: invalid version number")
}
raddr, err := net.ResolveUDPAddr("udp", host+":123")
if err != nil {
return nil, err
}
con, err := net.DialUDP("udp", nil, raddr)
if err != nil {
return nil, err
}
defer con.Close()
con.SetDeadline(time.Now().Add(timeout))
m := new(msg)
m.setMode(client)
m.setVersion(version)
m.TransmitTime = toNtpTime(time.Now())
err = binary.Write(con, binary.BigEndian, m)
if err != nil {
return nil, err
}
err = binary.Read(con, binary.BigEndian, m)
if err != nil {
return nil, err
}
return m, nil
}
// TimeV returns the "receive time" from the remote NTP server specifed as
// host. Use the NTP client mode with the requested version number (2, 3, or
// 4).
func TimeV(host string, version int) (time.Time, error) {
m, err := getTime(host, version)
if err != nil {
return time.Now(), err
}
return m.ReceiveTime.Time().Local(), nil
}
// Time returns the "receive time" from the remote NTP server specifed as
// host. NTP client mode version 4 is used.
func Time(host string) (time.Time, error) {
return TimeV(host, 4)
}
func rtt(t1, t2, t3, t4 ntpTime) time.Duration {
// round trip delay time (https://tools.ietf.org/html/rfc5905#section-8)
// T1 = client send time
// T2 = server receive time
// T3 = server reply time
// T4 = client receive time
//
// RTT d:
// d = (T4-T1) - (T3-T2)
a := t4.Time().Sub(t1.Time())
b := t3.Time().Sub(t2.Time())
return a - b
}
func offset(t1, t2, t3, t4 ntpTime) time.Duration {
// local offset equation (https://tools.ietf.org/html/rfc5905#section-8)
// T1 = client send time
// T2 = server receive time
// T3 = server reply time
// T4 = client receive time
//
// Local clock offset t:
// t = ((T2-T1) + (T3-T4)) / 2
a := t2.Time().Sub(t1.Time())
b := t3.Time().Sub(t4.Time())
return (a + b) / time.Duration(2)
}