cmd/ntpcheck/checker/peer.go (189 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 checker import ( "fmt" "strconv" "github.com/facebook/time/ntp/chrony" "github.com/facebook/time/ntp/control" "github.com/pkg/errors" ) // Peer contains parsed information from Peer Variables and peer status word, as described in http://doc.ntp.org/current-stable/ntpq.html type Peer struct { // from PeerStatusWord Configured bool AuthPossible bool Authentic bool Reachable bool Broadcast bool Selection uint8 Condition string // from variables SRCAdr string SRCPort int DSTAdr string DSTPort int Leap int Stratum int Precision int RootDelay float64 RootDisp float64 RefID string RefTime string Reach uint8 Unreach int HMode int PMode int HPoll int PPoll int Headway int Flash uint16 Flashers []string Offset float64 Delay float64 Dispersion float64 Jitter float64 Xleave float64 Rec string FiltDelay string FiltOffset string FiltDisp string } // sanityCheckPeerVars checks if we parsed enough info from NTPD response func sanityCheckPeerVars(p *Peer) error { if p == nil { return errors.New("No peer") } if p.Stratum == 0 { return errors.New("Incomplete data, stratum 0 in peer variables") } if p.PPoll == 0 || p.HPoll == 0 { return errors.New("Incomplete data, poll 0 in peer variables") } return nil } // NewPeerFromNTP constructs Peer from NTPControlMsg packet func NewPeerFromNTP(p *control.NTPControlMsg) (*Peer, error) { psWord, err := p.GetPeerStatus() if err != nil { return nil, err } m, err := p.GetAssociationInfo() if err != nil { return nil, err } var reach, flash uint16 // data comes as k=v pairs in packet, and those kv pairs are parsed by GetAssociationInfo. // If data is severely corrupted GetAssociationInfo will return error. // It's ok to have some fields missing, thus we don't check for errors below. leap, _ := strconv.Atoi(m["leap"]) unreach, _ := strconv.Atoi(m["unreach"]) pmode, _ := strconv.Atoi(m["pmode"]) hpoll, _ := strconv.Atoi(m["hpoll"]) offset, _ := strconv.ParseFloat(m["offset"], 64) dispersion, _ := strconv.ParseFloat(m["dispersion"], 64) dstport, _ := strconv.Atoi(m["dstport"]) fmt.Sscan(m["reach"], &reach) ppoll, _ := strconv.Atoi(m["ppoll"]) headway, _ := strconv.Atoi(m["headway"]) fmt.Sscan(m["flash"], &flash) jitter, _ := strconv.ParseFloat(m["jitter"], 64) rootdelay, _ := strconv.ParseFloat(m["rootdelay"], 64) precision, _ := strconv.Atoi(m["precision"]) delay, _ := strconv.ParseFloat(m["delay"], 64) stratum, _ := strconv.Atoi(m["stratum"]) hmode, _ := strconv.Atoi(m["hmode"]) srcport, _ := strconv.Atoi(m["srcport"]) xleave, _ := strconv.ParseFloat(m["xleave"], 64) rootdisp, _ := strconv.ParseFloat(m["rootdisp"], 64) peer := Peer{ // from PeerStatusWord Configured: psWord.PeerStatus.Configured, AuthPossible: psWord.PeerStatus.AuthEnabled, Authentic: psWord.PeerStatus.AuthOK, Reachable: psWord.PeerStatus.Reachable, Broadcast: psWord.PeerStatus.Broadcast, Selection: psWord.PeerSelection, Condition: control.PeerSelect[psWord.PeerSelection], // from variables Leap: leap, Unreach: unreach, PMode: pmode, HPoll: hpoll, DSTAdr: m["dstadr"], Rec: m["rec"], FiltOffset: m["filtoffset"], RefTime: m["reftime"], Offset: offset, FiltDisp: m["filtdisp"], SRCAdr: m["srcadr"], Dispersion: dispersion, DSTPort: dstport, RefID: m["refid"], Reach: uint8(reach), PPoll: ppoll, Headway: headway, Flash: uint16(flash), Flashers: control.ReadFlashStatusWord(uint16(flash)), Jitter: jitter, RootDelay: rootdelay, Precision: precision, Delay: delay, Stratum: stratum, HMode: hmode, FiltDelay: m["filtdelay"], SRCPort: srcport, Xleave: xleave, RootDisp: rootdisp, } if err := sanityCheckPeerVars(&peer); err != nil { return nil, err } return &peer, nil } // this mapping is not 100% correct, but fits the purpose of the tool var chronyToPeerSelection = map[chrony.SourceStateType]uint8{ chrony.SourceStateSync: control.SelSYSPeer, chrony.SourceStateUnreach: control.SelReject, // not a direct mapping chrony.SourceStateFalseTicket: control.SelFalseTick, chrony.SourceStateJittery: control.SelReject, // ditto chrony.SourceStateCandidate: control.SelCandidate, chrony.SourceStateOutlier: control.SelOutlier, } // NewPeerFromChrony constructs Peer from two chrony packets func NewPeerFromChrony(s *chrony.ReplySourceData, p *chrony.ReplyNTPData) (*Peer, error) { if s == nil { return nil, fmt.Errorf("no ReplySourceData to create Peer") } // clear auth and interlieved flag flash := s.Flags & chrony.NTPFlagsTests // don't report all flashers if peer is unreachable if flash > 0 { flash ^= chrony.NTPFlagsTests // negate bits as original NTPD flashers have opposite meaning } // all NTP measures are in ms, and chrony reports all in seconds, thus secToMS everywhere peer := Peer{ Configured: true, AuthPossible: false, Authentic: s.Flags&chrony.NTPFlagAuthenticated != 0, Reachable: s.Reachability == 255, // all 8 attempts Broadcast: false, Selection: chronyToPeerSelection[s.State], Condition: chrony.SourceStateDesc[s.State], Flash: flash, Flashers: chrony.ReadNTPTestFlags(s.Flags), Offset: -1 * secToMS(s.OrigLatestMeas), // sourceData offset and NTPData offset sign has opposite meaning PPoll: int(s.Poll), HPoll: int(s.Poll), Stratum: int(s.Stratum), SRCAdr: s.IPAddr.String(), Reach: uint8(s.Reachability), } // populate data from ntpdata struct if p != nil { refID := chrony.RefidAsHEX(p.RefID) // Only stratum 1 servers can have GPS or something else as string refID if p.Stratum == 1 { refIDStr := chrony.RefidToString(p.RefID) if len(refIDStr) > 0 { refID = refIDStr } } peer.Leap = int(p.Leap) peer.HPoll = int(p.Poll) peer.DSTAdr = p.LocalAddr.String() peer.RefTime = p.RefTime.String() peer.Offset = secToMS(p.Offset) peer.Dispersion = secToMS(p.PeerDispersion) peer.DSTPort = int(p.RemotePort) peer.RefID = refID peer.PPoll = int(p.Poll) peer.Jitter = secToMS(p.PeerDispersion) // best approx we have peer.RootDelay = secToMS(p.RootDelay) peer.Precision = int(p.Precision) peer.Delay = secToMS(p.PeerDelay) peer.RootDisp = secToMS(p.RootDispersion) } // no need for sanity check as we are not parsing k=v pairs in case of chrony proto return &peer, nil }