Add NTP client

This commit is contained in:
世界 2023-02-21 13:30:51 +08:00
parent 989b59665f
commit 3401d21038
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
2 changed files with 444 additions and 0 deletions

51
common/ntp/client.go Normal file
View file

@ -0,0 +1,51 @@
package ntp
import (
"context"
"crypto/rand"
"encoding/binary"
"time"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
func Exchange(ctx context.Context, dialer N.Dialer, serverAddress M.Socksaddr) (*Response, error) {
conn, err := dialer.DialContext(ctx, N.NetworkUDP, serverAddress)
if err != nil {
return nil, err
}
defer conn.Close()
conn.SetDeadline(time.Now().Add(defaultTimeout))
var request msg
request.setMode(client)
request.setVersion(defaultNtpVersion)
request.setLeap(LeapNotInSync)
bits := make([]byte, 8)
_, err = rand.Read(bits)
var xmitTime time.Time
if err == nil {
request.TransmitTime = ntpTime(binary.BigEndian.Uint64(bits))
xmitTime = time.Now()
} else {
xmitTime = time.Now()
request.TransmitTime = toNtpTime(xmitTime)
}
err = binary.Write(conn, binary.BigEndian, request)
if err != nil {
return nil, err
}
var response msg
err = binary.Read(conn, binary.BigEndian, &response)
if err != nil {
return nil, err
}
recvTime := toNtpTime(xmitTime.Add(time.Since(xmitTime)))
response.OriginTime = toNtpTime(xmitTime)
return parseTime(&response, recvTime), nil
}

393
common/ntp/message.go Normal file
View file

@ -0,0 +1,393 @@
// Copyright 2015-2017 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 an implementation of a Simple NTP (SNTP) client
// capable of querying the current time from a remote NTP server. See
// RFC5905 (https://tools.ietf.org/html/rfc5905) for more details.
//
// This approach grew out of a go-nuts post by Michael Hofmann:
// https://groups.google.com/forum/?fromgroups#!topic/golang-nuts/FlcdMU5fkLQ
package ntp
import (
"errors"
"fmt"
"time"
)
// The LeapIndicator is used to warn if a leap second should be inserted
// or deleted in the last minute of the current month.
type LeapIndicator uint8
const (
// LeapNoWarning indicates no impending leap second.
LeapNoWarning LeapIndicator = 0
// LeapAddSecond indicates the last minute of the day has 61 seconds.
LeapAddSecond = 1
// LeapDelSecond indicates the last minute of the day has 59 seconds.
LeapDelSecond = 2
// LeapNotInSync indicates an unsynchronized leap second.
LeapNotInSync = 3
)
// Internal constants
const (
defaultNtpVersion = 4
nanoPerSec = 1000000000
maxStratum = 16
defaultTimeout = 5 * time.Second
maxPollInterval = (1 << 17) * time.Second
maxDispersion = 16 * time.Second
)
// Internal variables
var (
ntpEpoch = time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC)
)
type mode uint8
// NTP modes. This package uses only client mode.
const (
reserved mode = 0 + iota
symmetricActive
symmetricPassive
client
server
broadcast
controlMessage
reservedPrivate
)
// An ntpTime is a 64-bit fixed-point (Q32.32) representation of the number of
// seconds elapsed.
type ntpTime uint64
// Duration interprets the fixed-point ntpTime as a number of elapsed seconds
// and returns the corresponding time.Duration value.
func (t ntpTime) Duration() time.Duration {
sec := (t >> 32) * nanoPerSec
frac := (t & 0xffffffff) * nanoPerSec
nsec := frac >> 32
if uint32(frac) >= 0x80000000 {
nsec++
}
return time.Duration(sec + nsec)
}
// Time interprets the fixed-point ntpTime as an absolute time and returns
// the corresponding time.Time value.
func (t ntpTime) Time() time.Time {
return ntpEpoch.Add(t.Duration())
}
// toNtpTime converts the time.Time value t into its 64-bit fixed-point
// ntpTime representation.
func toNtpTime(t time.Time) ntpTime {
nsec := uint64(t.Sub(ntpEpoch))
sec := nsec / nanoPerSec
nsec = uint64(nsec-sec*nanoPerSec) << 32
frac := uint64(nsec / nanoPerSec)
if nsec%nanoPerSec >= nanoPerSec/2 {
frac++
}
return ntpTime(sec<<32 | frac)
}
// An ntpTimeShort is a 32-bit fixed-point (Q16.16) representation of the
// number of seconds elapsed.
type ntpTimeShort uint32
// Duration interprets the fixed-point ntpTimeShort as a number of elapsed
// seconds and returns the corresponding time.Duration value.
func (t ntpTimeShort) Duration() time.Duration {
sec := uint64(t>>16) * nanoPerSec
frac := uint64(t&0xffff) * nanoPerSec
nsec := frac >> 16
if uint16(frac) >= 0x8000 {
nsec++
}
return time.Duration(sec + nsec)
}
// msg is an internal representation of an NTP packet.
type msg struct {
LiVnMode uint8 // Leap Indicator (2) + Version (3) + Mode (3)
Stratum uint8
Poll int8
Precision int8
RootDelay ntpTimeShort
RootDispersion ntpTimeShort
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) | uint8(md)
}
// setLeap modifies the leap indicator on the message.
func (m *msg) setLeap(li LeapIndicator) {
m.LiVnMode = (m.LiVnMode & 0x3f) | uint8(li)<<6
}
// getVersion returns the version value in the message.
func (m *msg) getVersion() int {
return int((m.LiVnMode >> 3) & 0x07)
}
// getMode returns the mode value in the message.
func (m *msg) getMode() mode {
return mode(m.LiVnMode & 0x07)
}
// getLeap returns the leap indicator on the message.
func (m *msg) getLeap() LeapIndicator {
return LeapIndicator((m.LiVnMode >> 6) & 0x03)
}
// 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 is the transmit time reported by the server just before it
// responded to the client's NTP query.
Time time.Time
// ClockOffset is the estimated offset of the client clock relative to
// the server. Add this to the client's system clock time to obtain a
// more accurate time.
ClockOffset time.Duration
// RTT is the measured round-trip-time delay estimate between the client
// and the server.
RTT time.Duration
// Precision is the reported precision of the server's clock.
Precision time.Duration
// Stratum is the "stratum level" of the server. The smaller the number,
// the closer the server is to the reference clock. Stratum 1 servers are
// attached directly to the reference clock. A stratum value of 0
// indicates the "kiss of death," which typically occurs when the client
// issues too many requests to the server in a short period of time.
Stratum uint8
// ReferenceID is a 32-bit identifier identifying the server or
// reference clock.
ReferenceID uint32
// ReferenceTime is the time when the server's system clock was last
// set or corrected.
ReferenceTime time.Time
// RootDelay is the server's estimated aggregate round-trip-time delay to
// the stratum 1 server.
RootDelay time.Duration
// RootDispersion is the server's estimated maximum measurement error
// relative to the stratum 1 server.
RootDispersion time.Duration
// RootDistance is an estimate of the total synchronization distance
// between the client and the stratum 1 server.
RootDistance time.Duration
// Leap indicates whether a leap second should be added or removed from
// the current month's last minute.
Leap LeapIndicator
// MinError is a lower bound on the error between the client and server
// clocks. When the client and server are not synchronized to the same
// clock, the reported timestamps may appear to violate the principle of
// causality. In other words, the NTP server's response may indicate
// that a message was received before it was sent. In such cases, the
// minimum error may be useful.
MinError time.Duration
// KissCode is a 4-character string describing the reason for a
// "kiss of death" response (stratum = 0). For a list of standard kiss
// codes, see https://tools.ietf.org/html/rfc5905#section-7.4.
KissCode string
// Poll is the maximum interval between successive NTP polling messages.
// It is not relevant for simple NTP clients like this one.
Poll time.Duration
}
// Validate checks if the response is valid for the purposes of time
// synchronization.
func (r *Response) Validate() error {
// Handle invalid stratum values.
if r.Stratum == 0 {
return fmt.Errorf("kiss of death received: %s", r.KissCode)
}
if r.Stratum >= maxStratum {
return errors.New("invalid stratum in response")
}
// Handle invalid leap second indicator.
if r.Leap == LeapNotInSync {
return errors.New("invalid leap second")
}
// Estimate the "freshness" of the time. If it exceeds the maximum
// polling interval (~36 hours), then it cannot be considered "fresh".
freshness := r.Time.Sub(r.ReferenceTime)
if freshness > maxPollInterval {
return errors.New("server clock not fresh")
}
// Calculate the peer synchronization distance, lambda:
// lambda := RootDelay/2 + RootDispersion
// If this value exceeds MAXDISP (16s), then the time is not suitable
// for synchronization purposes.
// https://tools.ietf.org/html/rfc5905#appendix-A.5.1.1.
lambda := r.RootDelay/2 + r.RootDispersion
if lambda > maxDispersion {
return errors.New("invalid dispersion")
}
// If the server's transmit time is before its reference time, the
// response is invalid.
if r.Time.Before(r.ReferenceTime) {
return errors.New("invalid time reported")
}
// nil means the response is valid.
return nil
}
// parseTime parses the NTP packet along with the packet receive time to
// generate a Response record.
func parseTime(m *msg, recvTime ntpTime) *Response {
r := &Response{
Time: m.TransmitTime.Time(),
ClockOffset: offset(m.OriginTime, m.ReceiveTime, m.TransmitTime, recvTime),
RTT: rtt(m.OriginTime, m.ReceiveTime, m.TransmitTime, recvTime),
Precision: toInterval(m.Precision),
Stratum: m.Stratum,
ReferenceID: m.ReferenceID,
ReferenceTime: m.ReferenceTime.Time(),
RootDelay: m.RootDelay.Duration(),
RootDispersion: m.RootDispersion.Duration(),
Leap: m.getLeap(),
MinError: minError(m.OriginTime, m.ReceiveTime, m.TransmitTime, recvTime),
Poll: toInterval(m.Poll),
}
// Calculate values depending on other calculated values
r.RootDistance = rootDistance(r.RTT, r.RootDelay, r.RootDispersion)
// If a kiss of death was received, interpret the reference ID as
// a kiss code.
if r.Stratum == 0 {
r.KissCode = kissCode(r.ReferenceID)
}
return r
}
// The following helper functions calculate additional metadata about the
// timestamps received from an NTP server. The timestamps returned by
// the server are given the following variable names:
//
// org = Origin Timestamp (client send time)
// rec = Receive Timestamp (server receive time)
// xmt = Transmit Timestamp (server reply time)
// dst = Destination Timestamp (client receive time)
func rtt(org, rec, xmt, dst ntpTime) time.Duration {
// round trip delay time
// rtt = (dst-org) - (xmt-rec)
a := dst.Time().Sub(org.Time())
b := xmt.Time().Sub(rec.Time())
rtt := a - b
if rtt < 0 {
rtt = 0
}
return rtt
}
func offset(org, rec, xmt, dst ntpTime) time.Duration {
// local clock offset
// offset = ((rec-org) + (xmt-dst)) / 2
a := rec.Time().Sub(org.Time())
b := xmt.Time().Sub(dst.Time())
return (a + b) / time.Duration(2)
}
func minError(org, rec, xmt, dst ntpTime) time.Duration {
// Each NTP response contains two pairs of send/receive timestamps.
// When either pair indicates a "causality violation", we calculate the
// error as the difference in time between them. The minimum error is
// the greater of the two causality violations.
var error0, error1 ntpTime
if org >= rec {
error0 = org - rec
}
if xmt >= dst {
error1 = xmt - dst
}
if error0 > error1 {
return error0.Duration()
}
return error1.Duration()
}
func rootDistance(rtt, rootDelay, rootDisp time.Duration) time.Duration {
// The root distance is:
// the maximum error due to all causes of the local clock
// relative to the primary server. It is defined as half the
// total delay plus total dispersion plus peer jitter.
// (https://tools.ietf.org/html/rfc5905#appendix-A.5.5.2)
//
// In the reference implementation, it is calculated as follows:
// rootDist = max(MINDISP, rootDelay + rtt)/2 + rootDisp
// + peerDisp + PHI * (uptime - peerUptime)
// + peerJitter
// For an SNTP client which sends only a single packet, most of these
// terms are irrelevant and become 0.
totalDelay := rtt + rootDelay
return totalDelay/2 + rootDisp
}
func toInterval(t int8) time.Duration {
switch {
case t > 0:
return time.Duration(uint64(time.Second) << uint(t))
case t < 0:
return time.Duration(uint64(time.Second) >> uint(-t))
default:
return time.Second
}
}
func kissCode(id uint32) string {
isPrintable := func(ch byte) bool { return ch >= 32 && ch <= 126 }
b := []byte{
byte(id >> 24),
byte(id >> 16),
byte(id >> 8),
byte(id),
}
for _, ch := range b {
if !isPrintable(ch) {
return ""
}
}
return string(b)
}