2016-01-21 04:19:15 +01:00
|
|
|
// Copyright 2015 Brett Vickers.
|
|
|
|
// Use of this source code is governed by a BSD-style
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
2016-06-01 09:56:20 +02:00
|
|
|
// 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:
|
|
|
|
//
|
2016-01-21 04:19:15 +01:00
|
|
|
// 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
|
|
|
|
)
|
|
|
|
|
2016-06-01 09:56:20 +02:00
|
|
|
const (
|
|
|
|
maxStratum = 16
|
|
|
|
nanoPerSec = 1000000000
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
timeout = 5 * time.Second
|
|
|
|
ntpEpoch = time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
|
|
)
|
|
|
|
|
2016-01-21 04:19:15 +01:00
|
|
|
type ntpTime struct {
|
|
|
|
Seconds uint32
|
|
|
|
Fraction uint32
|
|
|
|
}
|
|
|
|
|
2016-06-01 09:56:20 +02:00
|
|
|
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),
|
|
|
|
}
|
2016-01-21 04:19:15 +01:00
|
|
|
}
|
|
|
|
|
2016-06-01 09:56:20 +02:00
|
|
|
// msg is an internal representation of an NTP packet.
|
2016-01-21 04:19:15 +01:00
|
|
|
type msg struct {
|
|
|
|
LiVnMode byte // Leap Indicator (2) + Version (3) + Mode (3)
|
|
|
|
Stratum byte
|
|
|
|
Poll byte
|
|
|
|
Precision byte
|
|
|
|
RootDelay uint32
|
|
|
|
RootDispersion uint32
|
2016-06-01 09:56:20 +02:00
|
|
|
ReferenceID uint32
|
2016-01-21 04:19:15 +01:00
|
|
|
ReferenceTime ntpTime
|
|
|
|
OriginTime ntpTime
|
|
|
|
ReceiveTime ntpTime
|
|
|
|
TransmitTime ntpTime
|
|
|
|
}
|
|
|
|
|
2016-06-01 09:56:20 +02:00
|
|
|
// setVersion sets the NTP protocol version on the message.
|
|
|
|
func (m *msg) setVersion(v int) {
|
|
|
|
m.LiVnMode = (m.LiVnMode & 0xc7) | uint8(v)<<3
|
2016-01-21 04:19:15 +01:00
|
|
|
}
|
|
|
|
|
2016-06-01 09:56:20 +02:00
|
|
|
// setMode sets the NTP protocol mode on the message.
|
|
|
|
func (m *msg) setMode(md mode) {
|
2016-01-21 04:19:15 +01:00
|
|
|
m.LiVnMode = (m.LiVnMode & 0xf8) | byte(md)
|
|
|
|
}
|
|
|
|
|
2016-06-01 09:56:20 +02:00
|
|
|
// 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) {
|
2016-01-21 04:19:15 +01:00
|
|
|
if version < 2 || version > 4 {
|
|
|
|
panic("ntp: invalid version number")
|
|
|
|
}
|
|
|
|
|
|
|
|
raddr, err := net.ResolveUDPAddr("udp", host+":123")
|
|
|
|
if err != nil {
|
2016-06-01 09:56:20 +02:00
|
|
|
return nil, err
|
2016-01-21 04:19:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
con, err := net.DialUDP("udp", nil, raddr)
|
|
|
|
if err != nil {
|
2016-06-01 09:56:20 +02:00
|
|
|
return nil, err
|
2016-01-21 04:19:15 +01:00
|
|
|
}
|
|
|
|
defer con.Close()
|
2016-06-01 09:56:20 +02:00
|
|
|
con.SetDeadline(time.Now().Add(timeout))
|
2016-01-21 04:19:15 +01:00
|
|
|
|
|
|
|
m := new(msg)
|
2016-06-01 09:56:20 +02:00
|
|
|
m.setMode(client)
|
|
|
|
m.setVersion(version)
|
|
|
|
m.TransmitTime = toNtpTime(time.Now())
|
2016-01-21 04:19:15 +01:00
|
|
|
|
|
|
|
err = binary.Write(con, binary.BigEndian, m)
|
|
|
|
if err != nil {
|
2016-06-01 09:56:20 +02:00
|
|
|
return nil, err
|
2016-01-21 04:19:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
err = binary.Read(con, binary.BigEndian, m)
|
|
|
|
if err != nil {
|
2016-06-01 09:56:20 +02:00
|
|
|
return nil, err
|
2016-01-21 04:19:15 +01:00
|
|
|
}
|
|
|
|
|
2016-06-01 09:56:20 +02:00
|
|
|
return m, nil
|
2016-01-21 04:19:15 +01:00
|
|
|
}
|
|
|
|
|
2016-06-01 09:56:20 +02:00
|
|
|
// 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
|
2016-01-21 04:19:15 +01:00
|
|
|
}
|
|
|
|
|
2016-06-01 09:56:20 +02:00
|
|
|
// Time returns the "receive time" from the remote NTP server specifed as
|
|
|
|
// host. NTP client mode version 4 is used.
|
2016-01-21 04:19:15 +01:00
|
|
|
func Time(host string) (time.Time, error) {
|
2016-06-01 09:56:20 +02:00
|
|
|
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)
|
2016-01-21 04:19:15 +01:00
|
|
|
}
|