ntp/chrony/packet.go (628 lines of code) (raw):

/* Copyright (c) Facebook, Inc. and its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package chrony import ( "bytes" "encoding/binary" "fmt" "net" "time" log "github.com/sirupsen/logrus" ) // original C++ versions of those consts/structs // are in https://github.com/mlichvar/chrony/blob/master/candm.h // ReplyType identifies reply packet type type ReplyType uint16 // CommandType identifies command type in both request and repy type CommandType uint16 // ModeType identifies source (peer) mode type ModeType uint16 // SourceStateType identifies source (peer) state type SourceStateType uint16 // ResponseStatusType identifies response status type ResponseStatusType uint16 // PacketType - request or reply type PacketType uint8 // we implement latest (at the moment) protocol version const protoVersionNumber uint8 = 6 const maxDataLen = 396 // packet types const ( pktTypeCmdRequest PacketType = 1 pktTypeCmdReply PacketType = 2 ) func (t PacketType) String() string { switch t { case pktTypeCmdRequest: return "request" case pktTypeCmdReply: return "reply" default: return fmt.Sprintf("unknown (%d)", t) } } // request types. Only those we suppor, there are more const ( reqNSources CommandType = 14 reqSourceData CommandType = 15 reqTracking CommandType = 33 reqSourceStats CommandType = 34 reqServerStats CommandType = 54 reqNTPData CommandType = 57 ) // reply types const ( rpyNSources ReplyType = 2 rpySourceData ReplyType = 3 rpyTracking ReplyType = 5 rpySourceStats ReplyType = 6 rpyServerStats ReplyType = 14 rpyNTPData ReplyType = 16 rpyServerStats2 ReplyType = 22 ) // source modes const ( SourceModeClient ModeType = 0 SourceModePeer ModeType = 1 SourceModeRef ModeType = 2 ) // source state const ( SourceStateSync SourceStateType = 0 SourceStateUnreach SourceStateType = 1 SourceStateFalseTicket SourceStateType = 2 SourceStateJittery SourceStateType = 3 SourceStateCandidate SourceStateType = 4 SourceStateOutlier SourceStateType = 5 ) // source data flags const ( FlagNoselect uint16 = 0x1 FlagPrefer uint16 = 0x2 FlagTrust uint16 = 0x4 FlagRequire uint16 = 0x8 ) // ntpdata flags const ( NTPFlagsTests uint16 = 0x3ff NTPFlagInterleaved uint16 = 0x4000 NTPFlagAuthenticated uint16 = 0x8000 ) // response status codes //nolint:varcheck,deadcode,unused const ( sttSuccess ResponseStatusType = 0 sttFailed ResponseStatusType = 1 sttUnauth ResponseStatusType = 2 sttInvalid ResponseStatusType = 3 sttNoSuchSource ResponseStatusType = 4 sttInvalidTS ResponseStatusType = 5 sttNotEnabled ResponseStatusType = 6 sttBadSubnet ResponseStatusType = 7 sttAccessAllowed ResponseStatusType = 8 sttAccessDenied ResponseStatusType = 9 sttNoHostAccess ResponseStatusType = 10 sttSourceAlreadyKnown ResponseStatusType = 11 sttTooManySources ResponseStatusType = 12 sttNoRTC ResponseStatusType = 13 sttBadRTCFile ResponseStatusType = 14 sttInactive ResponseStatusType = 15 sttBadSample ResponseStatusType = 16 sttInvalidAF ResponseStatusType = 17 sttBadPktVersion ResponseStatusType = 18 sttBadPktLength ResponseStatusType = 19 ) // StatusDesc provides mapping from ResponseStatusType to string var StatusDesc = [20]string{ "SUCCESS", "FAILED", "UNAUTH", "INVALID", "NOSUCHSOURCE", "INVALIDTS", "NOTENABLED", "BADSUBNET", "ACCESSALLOWED", "ACCESSDENIED", "NOHOSTACCESS", "SOURCEALREADYKNOWN", "TOOMANYSOURCES", "NORTC", "BADRTCFILE", "INACTIVE", "BADSAMPLE", "INVALIDAF", "BADPKTVERSION", "BADPKTLENGTH", } func (r ResponseStatusType) String() string { if int(r) >= len(StatusDesc) { return fmt.Sprintf("UNKNOWN (%d)", r) } return StatusDesc[r] } // SourceStateDesc provides mapping from SourceStateType to string var SourceStateDesc = [6]string{ "sync", "unreach", "falseticket", "jittery", "candidate", "outlier", } func (s SourceStateType) String() string { if int(s) >= len(SourceStateDesc) { return fmt.Sprintf("unknown (%d)", s) } return SourceStateDesc[s] } // ModeTypeDesc provides mapping from ModeType to string var ModeTypeDesc = [3]string{ "client", "peer", "reference clock", } func (m ModeType) String() string { if int(m) >= len(ModeTypeDesc) { return fmt.Sprintf("unknown (%d)", m) } return ModeTypeDesc[m] } // RequestHead is the first (common) part of the request, // in a format that can be directly passed to binary.Write type RequestHead struct { Version uint8 PKTType PacketType Res1 uint8 Res2 uint8 Command CommandType Attempt uint16 Sequence uint32 Pad1 uint32 Pad2 uint32 } // GetCommand returns request packet command func (r *RequestHead) GetCommand() CommandType { return r.Command } // SetSequence sets request packet sequence number func (r *RequestHead) SetSequence(n uint32) { r.Sequence = n } // RequestPacket is an iterface to abstract all different outgoing packets type RequestPacket interface { GetCommand() CommandType SetSequence(n uint32) } // ResponsePacket is an interface to abstract all different incoming packets type ResponsePacket interface { GetCommand() CommandType GetType() PacketType GetStatus() ResponseStatusType } // RequestSources - packet to request number of sources (peers) type RequestSources struct { RequestHead // we actually need this to send proper packet data [maxDataLen]uint8 //nolint:unused,structcheck } // RequestSourceData - packet to request source data for source id type RequestSourceData struct { RequestHead Index int32 EOR int32 // we pass i32 - 4 bytes data [maxDataLen - 4]uint8 //nolint:unused,structcheck } // RequestNTPData - packet to request NTP data for peer IP. // As of now, it's only allowed by Chrony over unix socket connection. type RequestNTPData struct { RequestHead IPAddr ipAddr EOR int32 // we pass at max ipv6 addr - 16 bytes data [maxDataLen - 16]uint8 //nolint:unused,structcheck } // RequestServerStats - packet to request server stats type RequestServerStats struct { RequestHead // we actually need this to send proper packet data [maxDataLen]uint8 //nolint:unused,structcheck } // RequestTracking - packet to request 'tracking' data type RequestTracking struct { RequestHead // we actually need this to send proper packet data [maxDataLen]uint8 //nolint:unused,structcheck } // RequestSourceStats - packet to request 'sourcestats' data for source id type RequestSourceStats struct { RequestHead Index int32 EOR int32 // we pass i32 - 4 bytes data [maxDataLen - 4]uint8 //nolint:unused,structcheck } // ReplyHead is the first (common) part of the reply packet, // in a format that can be directly passed to binary.Read type ReplyHead struct { Version uint8 PKTType PacketType Res1 uint8 Res2 uint8 Command CommandType Reply ReplyType Status ResponseStatusType Pad1 uint16 Pad2 uint16 Pad3 uint16 Sequence uint32 Pad4 uint32 Pad5 uint32 } // GetCommand returns reply packet command func (r *ReplyHead) GetCommand() CommandType { return r.Command } // GetType returns reply packet type func (r *ReplyHead) GetType() PacketType { return r.PKTType } // GetStatus returns reply packet status func (r *ReplyHead) GetStatus() ResponseStatusType { return r.Status } type replySourcesContent struct { NSources uint32 } // ReplySources is a usable version of a reply to 'sources' command type ReplySources struct { ReplyHead NSources int } type replySourceDataContent struct { IPAddr ipAddr Poll int16 Stratum uint16 State SourceStateType Mode ModeType Flags uint16 Reachability uint16 SinceSample uint32 OrigLatestMeas chronyFloat LatestMeas chronyFloat LatestMeasErr chronyFloat } // SourceData contains parsed version of 'source data' reply type SourceData struct { IPAddr net.IP Poll int16 Stratum uint16 State SourceStateType Mode ModeType Flags uint16 Reachability uint16 SinceSample uint32 OrigLatestMeas float64 LatestMeas float64 LatestMeasErr float64 } func newSourceData(r *replySourceDataContent) *SourceData { return &SourceData{ IPAddr: r.IPAddr.ToNetIP(), Poll: r.Poll, Stratum: r.Stratum, State: r.State, Mode: r.Mode, Flags: r.Flags, Reachability: r.Reachability, SinceSample: r.SinceSample, OrigLatestMeas: r.OrigLatestMeas.ToFloat(), LatestMeas: r.LatestMeas.ToFloat(), LatestMeasErr: r.LatestMeasErr.ToFloat(), } } // ReplySourceData is a usable version of 'source data' reply for given source id type ReplySourceData struct { ReplyHead SourceData } type replyTrackingContent struct { RefID uint32 IPAddr ipAddr // our current sync source Stratum uint16 LeapStatus uint16 RefTime timeSpec CurrentCorrection chronyFloat LastOffset chronyFloat RMSOffset chronyFloat FreqPPM chronyFloat ResidFreqPPM chronyFloat SkewPPM chronyFloat RootDelay chronyFloat RootDispersion chronyFloat LastUpdateInterval chronyFloat } // Tracking contains parsed version of 'tracking' reply type Tracking struct { RefID uint32 IPAddr net.IP Stratum uint16 LeapStatus uint16 RefTime time.Time CurrentCorrection float64 LastOffset float64 RMSOffset float64 FreqPPM float64 ResidFreqPPM float64 SkewPPM float64 RootDelay float64 RootDispersion float64 LastUpdateInterval float64 } func newTracking(r *replyTrackingContent) *Tracking { return &Tracking{ RefID: r.RefID, IPAddr: r.IPAddr.ToNetIP(), Stratum: r.Stratum, LeapStatus: r.LeapStatus, RefTime: r.RefTime.ToTime(), CurrentCorrection: r.CurrentCorrection.ToFloat(), LastOffset: r.LastOffset.ToFloat(), RMSOffset: r.RMSOffset.ToFloat(), FreqPPM: r.FreqPPM.ToFloat(), ResidFreqPPM: r.ResidFreqPPM.ToFloat(), SkewPPM: r.SkewPPM.ToFloat(), RootDelay: r.RootDelay.ToFloat(), RootDispersion: r.RootDispersion.ToFloat(), LastUpdateInterval: r.LastUpdateInterval.ToFloat(), } } // ReplyTracking has usable 'tracking' response type ReplyTracking struct { ReplyHead Tracking } type replySourceStatsContent struct { RefID uint32 IPAddr ipAddr NSamples uint32 NRuns uint32 SpanSeconds uint32 StandardDeviation chronyFloat ResidFreqPPM chronyFloat SkewPPM chronyFloat EstimatedOffset chronyFloat EstimatedOffsetErr chronyFloat } // SourceStats contains stats about the source type SourceStats struct { RefID uint32 IPAddr net.IP NSamples uint32 NRuns uint32 SpanSeconds uint32 StandardDeviation float64 ResidFreqPPM float64 SkewPPM float64 EstimatedOffset float64 EstimatedOffsetErr float64 } func newSourceStats(r *replySourceStatsContent) *SourceStats { return &SourceStats{ RefID: r.RefID, IPAddr: r.IPAddr.ToNetIP(), NSamples: r.NSamples, NRuns: r.NRuns, SpanSeconds: r.SpanSeconds, StandardDeviation: r.StandardDeviation.ToFloat(), ResidFreqPPM: r.ResidFreqPPM.ToFloat(), SkewPPM: r.SkewPPM.ToFloat(), EstimatedOffset: r.EstimatedOffset.ToFloat(), EstimatedOffsetErr: r.EstimatedOffsetErr.ToFloat(), } } // ReplySourceStats has usable 'sourcestats' response type ReplySourceStats struct { ReplyHead SourceStats } type replyNTPDataContent struct { RemoteAddr ipAddr LocalAddr ipAddr RemotePort uint16 Leap uint8 Version uint8 Mode uint8 Stratum uint8 Poll int8 Precision int8 RootDelay chronyFloat RootDispersion chronyFloat RefID uint32 RefTime timeSpec Offset chronyFloat PeerDelay chronyFloat PeerDispersion chronyFloat ResponseTime chronyFloat JitterAsymmetry chronyFloat Flags uint16 TXTssChar uint8 RXTssChar uint8 TotalTXCount uint32 TotalRXCount uint32 TotalValidCount uint32 Reserved [4]uint32 } // NTPData contains parsed version of 'ntpdata' reply type NTPData struct { RemoteAddr net.IP LocalAddr net.IP RemotePort uint16 Leap uint8 Version uint8 Mode uint8 Stratum uint8 Poll int8 Precision int8 RootDelay float64 RootDispersion float64 RefID uint32 RefTime time.Time Offset float64 PeerDelay float64 PeerDispersion float64 ResponseTime float64 JitterAsymmetry float64 Flags uint16 TXTssChar uint8 RXTssChar uint8 TotalTXCount uint32 TotalRXCount uint32 TotalValidCount uint32 } func newNTPData(r *replyNTPDataContent) *NTPData { return &NTPData{ RemoteAddr: r.RemoteAddr.ToNetIP(), LocalAddr: r.LocalAddr.ToNetIP(), RemotePort: r.RemotePort, Leap: r.Leap, Version: r.Version, Mode: r.Mode, Stratum: r.Stratum, Poll: r.Poll, Precision: r.Precision, RootDelay: r.RootDelay.ToFloat(), RootDispersion: r.RootDispersion.ToFloat(), RefID: r.RefID, RefTime: r.RefTime.ToTime(), Offset: r.Offset.ToFloat(), PeerDelay: r.PeerDelay.ToFloat(), PeerDispersion: r.PeerDispersion.ToFloat(), ResponseTime: r.ResponseTime.ToFloat(), JitterAsymmetry: r.JitterAsymmetry.ToFloat(), Flags: r.Flags, TXTssChar: r.TXTssChar, RXTssChar: r.RXTssChar, TotalTXCount: r.TotalTXCount, TotalRXCount: r.TotalRXCount, TotalValidCount: r.TotalValidCount, } } // ReplyNTPData is a what end user will get for of 'ntp data' response type ReplyNTPData struct { ReplyHead NTPData } // ServerStats contains parsed version of 'serverstats' reply type ServerStats struct { NTPHits uint32 CMDHits uint32 NTPDrops uint32 CMDDrops uint32 LogDrops uint32 } // ReplyServerStats is a usable version of 'serverstats' response type ReplyServerStats struct { ReplyHead ServerStats } // ServerStats2 contains parsed version of 'serverstats2' reply type ServerStats2 struct { NTPHits uint32 NKEHits uint32 CMDHits uint32 NTPDrops uint32 NKEDrops uint32 CMDDrops uint32 LogDrops uint32 NTPAuthHits uint32 } // ReplyServerStats2 is a usable version of 'serverstats2' response type ReplyServerStats2 struct { ReplyHead ServerStats2 } // here go request constuctors // NewSourcesPacket creates new packet to request number of sources (peers) func NewSourcesPacket() *RequestSources { return &RequestSources{ RequestHead: RequestHead{ Version: protoVersionNumber, PKTType: pktTypeCmdRequest, Command: reqNSources, }, } } // NewTrackingPacket creates new packet to request 'tracking' information func NewTrackingPacket() *RequestTracking { return &RequestTracking{ RequestHead: RequestHead{ Version: protoVersionNumber, PKTType: pktTypeCmdRequest, Command: reqTracking, }, } } // NewSourceStatsPacket creates a new packet to request 'sourcestats' information func NewSourceStatsPacket(sourceID int32) *RequestSourceStats { return &RequestSourceStats{ RequestHead: RequestHead{ Version: protoVersionNumber, PKTType: pktTypeCmdRequest, Command: reqSourceStats, }, Index: sourceID, } } // NewSourceDataPacket creates new packet to request 'source data' information about source with given ID func NewSourceDataPacket(sourceID int32) *RequestSourceData { return &RequestSourceData{ RequestHead: RequestHead{ Version: protoVersionNumber, PKTType: pktTypeCmdRequest, Command: reqSourceData, }, Index: sourceID, } } // NewNTPDataPacket creates new packet to request 'ntp data' information for given peer IP func NewNTPDataPacket(ip net.IP) *RequestNTPData { return &RequestNTPData{ RequestHead: RequestHead{ Version: protoVersionNumber, PKTType: pktTypeCmdRequest, Command: reqNTPData, }, IPAddr: *newIPAddr(ip), } } // NewServerStatsPacket creates new packet to request 'serverstats' information func NewServerStatsPacket() *RequestServerStats { return &RequestServerStats{ RequestHead: RequestHead{ Version: protoVersionNumber, PKTType: pktTypeCmdRequest, Command: reqServerStats, }, } } // decodePacket decodes bytes to valid response packet func decodePacket(response []byte) (ResponsePacket, error) { var err error r := bytes.NewReader(response) head := new(ReplyHead) if err = binary.Read(r, binary.BigEndian, head); err != nil { return nil, err } log.Debugf("response head: %+v", head) if head.Status != sttSuccess { return nil, fmt.Errorf("got status %s (%d)", head.Status, head.Status) } switch head.Reply { case rpyNSources: data := new(replySourcesContent) if err = binary.Read(r, binary.BigEndian, data); err != nil { return nil, err } log.Debugf("response data: %+v", data) return &ReplySources{ ReplyHead: *head, NSources: int(data.NSources), }, nil case rpySourceData: data := new(replySourceDataContent) if err = binary.Read(r, binary.BigEndian, data); err != nil { return nil, err } log.Debugf("response data: %+v", data) return &ReplySourceData{ ReplyHead: *head, SourceData: *newSourceData(data), }, nil case rpyTracking: data := new(replyTrackingContent) if err = binary.Read(r, binary.BigEndian, data); err != nil { return nil, err } log.Debugf("response data: %+v", data) return &ReplyTracking{ ReplyHead: *head, Tracking: *newTracking(data), }, nil case rpySourceStats: data := new(replySourceStatsContent) if err = binary.Read(r, binary.BigEndian, data); err != nil { return nil, err } log.Debugf("response data: %+v", data) return &ReplySourceStats{ ReplyHead: *head, SourceStats: *newSourceStats(data), }, nil case rpyServerStats: data := new(ServerStats) if err = binary.Read(r, binary.BigEndian, data); err != nil { return nil, err } log.Debugf("response data: %+v", data) return &ReplyServerStats{ ReplyHead: *head, ServerStats: *data, }, nil case rpyNTPData: data := new(replyNTPDataContent) if err = binary.Read(r, binary.BigEndian, data); err != nil { return nil, err } log.Debugf("response data: %+v", data) return &ReplyNTPData{ ReplyHead: *head, NTPData: *newNTPData(data), }, nil case rpyServerStats2: data := new(ServerStats2) if err = binary.Read(r, binary.BigEndian, data); err != nil { return nil, err } log.Debugf("response data: %+v", data) return &ReplyServerStats2{ ReplyHead: *head, ServerStats2: *data, }, nil default: return nil, fmt.Errorf("not implemented reply type %d from %+v", head.Reply, head) } }