diff --git a/protocol/chrony/README.md b/protocol/chrony/README.md new file mode 100644 index 00000000..15ac1bba --- /dev/null +++ b/protocol/chrony/README.md @@ -0,0 +1,5 @@ +# Chrony network protocol used for command and monitoring of the timeserver + +Native Go implementation of Chrony communication protocol v6. + +As of now, only monitoring part of protocol that is used to communicate between `chronyc` and `chronyd` is implemented. diff --git a/protocol/chrony/client.go b/protocol/chrony/client.go new file mode 100644 index 00000000..3fe1810b --- /dev/null +++ b/protocol/chrony/client.go @@ -0,0 +1,112 @@ +/* +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" + "io" + + log "github.com/sirupsen/logrus" +) + +// Client talks to chronyd +type Client struct { + Connection io.ReadWriter + Sequence uint32 +} + +// Communicate sends the packet to chronyd, parse response into something usable +func (n *Client) Communicate(packet RequestPacket) (ResponsePacket, error) { + n.Sequence++ + var err error + packet.SetSequence(n.Sequence) + err = binary.Write(n.Connection, binary.BigEndian, packet) + if err != nil { + return nil, err + } + response := make([]uint8, 1024) + read, err := n.Connection.Read(response) + if err != nil { + return nil, err + } + log.Debugf("Read %d bytes", read) + 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 == sttUnauth { + return nil, ErrNotAuthorized + } + if head.Status != sttSuccess { + return nil, fmt.Errorf("got status %s", StatusDesc[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 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 + default: + return nil, fmt.Errorf("not implemented reply type %d from %+v", head.Reply, head) + } +} diff --git a/protocol/chrony/client_test.go b/protocol/chrony/client_test.go new file mode 100644 index 00000000..7aa7f54f --- /dev/null +++ b/protocol/chrony/client_test.go @@ -0,0 +1,197 @@ +/* +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" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// fakeConn gives us fake io.ReadWriter interace implementation for which we can provide fake outputs +type fakeConn struct { + readCount int + outputs []*bytes.Buffer +} + +func newConn(outputs []*bytes.Buffer) *fakeConn { + return &fakeConn{ + readCount: 0, + outputs: outputs, + } +} + +func (c *fakeConn) Read(p []byte) (n int, err error) { + pos := c.readCount + if c.readCount < len(c.outputs) { + c.readCount++ + return c.outputs[pos].Read(p) + } + return 0, fmt.Errorf("EOF") +} + +func (c *fakeConn) Write(p []byte) (n int, err error) { + // here we may assert writes + return 0, nil +} + +// Test if we have errors when there is nothing on the line to read +func TestCommunicateEOF(t *testing.T) { + require := require.New(t) + conn := newConn([]*bytes.Buffer{ + bytes.NewBuffer([]byte{}), + }) + client := Client{Sequence: 1, Connection: conn} + _, err := client.Communicate(NewTrackingPacket()) + require.NotNil(err) +} + +func TestCommunicateError(t *testing.T) { + var err error + require := require.New(t) + buf := &bytes.Buffer{} + packetHead := replyHead{ + Version: protoVersionNumber, + PKTType: pktTypeCmdReply, + Res1: 0, + Res2: 0, + Command: reqTracking, + Reply: rpyTracking, + Status: sttNoSuchSource, + Pad1: 0, + Pad2: 0, + Pad3: 0, + Sequence: 2, + Pad4: 0, + Pad5: 0, + } + packetBody := replyTrackingContent{} + err = binary.Write(buf, binary.BigEndian, packetHead) + require.Nil(err) + err = binary.Write(buf, binary.BigEndian, packetBody) + require.Nil(err) + conn := newConn([]*bytes.Buffer{ + buf, + }) + client := Client{Sequence: 1, Connection: conn} + _, err = client.Communicate(NewTrackingPacket()) + require.NotNil(err) +} + +func TestCommunicateAuthError(t *testing.T) { + var err error + assert := assert.New(t) + require := require.New(t) + buf := &bytes.Buffer{} + packetHead := replyHead{ + Version: protoVersionNumber, + PKTType: pktTypeCmdReply, + Res1: 0, + Res2: 0, + Command: reqTracking, + Reply: rpyTracking, + Status: sttUnauth, + Pad1: 0, + Pad2: 0, + Pad3: 0, + Sequence: 2, + Pad4: 0, + Pad5: 0, + } + packetBody := replyTrackingContent{} + err = binary.Write(buf, binary.BigEndian, packetHead) + require.Nil(err) + err = binary.Write(buf, binary.BigEndian, packetBody) + require.Nil(err) + conn := newConn([]*bytes.Buffer{ + buf, + }) + client := Client{Sequence: 1, Connection: conn} + _, err = client.Communicate(NewTrackingPacket()) + assert.Equal(ErrNotAuthorized, err) +} + +// Test if we can read reply properly +func TestCommunicateOK(t *testing.T) { + var err error + assert := assert.New(t) + require := require.New(t) + buf := &bytes.Buffer{} + packetHead := replyHead{ + Version: protoVersionNumber, + PKTType: pktTypeCmdReply, + Res1: 0, + Res2: 0, + Command: reqTracking, + Reply: rpyTracking, + Status: sttSuccess, + Pad1: 0, + Pad2: 0, + Pad3: 0, + Sequence: 2, + Pad4: 0, + Pad5: 0, + } + packetBody := replyTrackingContent{ + RefID: 1, + IPAddr: *newIPAddr(net.IP([]byte{192, 168, 0, 10})), + Stratum: 3, + LeapStatus: 0, + RefTime: timeSpec{}, + CurrentCorrection: 0, + LastOffset: 12345, + RMSOffset: 0, + FreqPPM: 0, + ResidFreqPPM: 0, + SkewPPM: 0, + RootDelay: 0, + RootDispersion: 0, + LastUpdateInterval: 0, + } + err = binary.Write(buf, binary.BigEndian, packetHead) + require.Nil(err) + err = binary.Write(buf, binary.BigEndian, packetBody) + require.Nil(err) + conn := newConn([]*bytes.Buffer{ + buf, + }) + client := Client{Sequence: 1, Connection: conn} + p, err := client.Communicate(NewTrackingPacket()) + require.Nil(err) + expected := &ReplyTracking{ + replyHead: packetHead, + tracking: tracking{ + RefID: packetBody.RefID, + IPAddr: net.IP([]byte{192, 168, 0, 10}), + Stratum: packetBody.Stratum, + LeapStatus: packetBody.LeapStatus, + RefTime: packetBody.RefTime.ToTime(), + CurrentCorrection: packetBody.CurrentCorrection.ToFloat(), + LastOffset: packetBody.LastOffset.ToFloat(), + RMSOffset: packetBody.RMSOffset.ToFloat(), + FreqPPM: packetBody.FreqPPM.ToFloat(), + ResidFreqPPM: packetBody.ResidFreqPPM.ToFloat(), + SkewPPM: packetBody.SkewPPM.ToFloat(), + RootDelay: packetBody.RootDelay.ToFloat(), + RootDispersion: packetBody.RootDispersion.ToFloat(), + LastUpdateInterval: packetBody.LastUpdateInterval.ToFloat(), + }, + } + assert.Equal(expected, p) +} diff --git a/protocol/chrony/helpers.go b/protocol/chrony/helpers.go new file mode 100644 index 00000000..972e518f --- /dev/null +++ b/protocol/chrony/helpers.go @@ -0,0 +1,194 @@ +/* +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 ( + "errors" + "fmt" + "math" + "net" + "time" + "unicode" +) + +// ErrNotAuthorized identifies failure to get data from Chronyd when we are not authorized to do so +// (like asking for NTP data over UDP instead of unix socket) +var ErrNotAuthorized = errors.New("Not authorized") + +// ChronySocketPath is the default path to chronyd socket +const ChronySocketPath = "/var/run/chrony/chronyd.sock" + +// ChronyPortV6Regexp is a regexp to find anything that listens on port 323 +// hex(323) = '0x143' +const ChronyPortV6Regexp = "[0-9]+: [0-9A-Z]+:0143 .*" + +// This is used in timeSpec.SecHigh for 32-bit timestamps +const noHighSec uint32 = 0x7fffffff + +// ip stuff +const ( + ipAddrInet4 uint16 = 1 + ipAddrInet6 uint16 = 2 +) + +// magic numbers to convert chronyFloat to normal float +const ( + floatExpBits = 7 + floatCoefBits = (4*8 - floatExpBits) +) + +type ipAddr struct { + IP [16]uint8 + Family uint16 + Pad uint16 +} + +func (ip *ipAddr) ToNetIP() net.IP { + if ip.Family == ipAddrInet4 { + return net.IP(ip.IP[:4]) + } + return net.IP(ip.IP[:]) +} + +func newIPAddr(ip net.IP) *ipAddr { + family := ipAddrInet6 + if ip.To4() != nil { + family = ipAddrInet4 + } + var nIP [16]byte + copy(nIP[:], ip) + return &ipAddr{ + IP: nIP, + Family: family, + } +} + +type timeSpec struct { + SecHigh uint32 + SecLow uint32 + Nsec uint32 +} + +func (t *timeSpec) ToTime() time.Time { + highU64 := uint64(t.SecHigh) + if t.SecHigh == noHighSec { + highU64 = 0 + } + lowU64 := uint64(t.SecLow) + return time.Unix(int64(highU64<<32|lowU64), int64(t.Nsec)) +} + +/* 32-bit floating-point format consisting of 7-bit signed exponent + and 25-bit signed coefficient without hidden bit. + The result is calculated as: 2^(exp - 25) * coef */ +type chronyFloat int32 + +// ToFloat does magic to decode float from int32. +// Code is copied and translated to Go from original C sources. +func (f chronyFloat) ToFloat() float64 { + var exp, coef int32 + + x := uint32(f) + + exp = int32(x >> floatCoefBits) + if exp >= 1<<(floatExpBits-1) { + exp -= 1 << floatExpBits + } + exp -= floatCoefBits + + coef = int32(x % (1 << floatCoefBits)) + if coef >= 1<<(floatCoefBits-1) { + coef -= 1 << floatCoefBits + } + + return float64(coef) * math.Pow(2.0, float64(exp)) +} + +// RefidAsHEX prints ref id as hex +func RefidAsHEX(refID uint32) string { + return fmt.Sprintf("%08X", refID) +} + +// RefidToString decodes ASCII string encoded as uint32 +func RefidToString(refID uint32) string { + result := []rune{} + + for i := 0; i < 4 && i < 64-1; i++ { + c := rune((refID >> (24 - uint(i)*8)) & 0xff) + if unicode.IsPrint(c) { + result = append(result, c) + } + } + + return string(result) +} + +/* NTP tests from RFC 5905: + +--------------------------+----------------------------------------+ + | Packet Type | Description | + +--------------------------+----------------------------------------+ + | 1 duplicate packet | The packet is at best an old duplicate | + | | or at worst a replay by a hacker. | + | | This can happen in symmetric modes if | + | | the poll intervals are uneven. | + | 2 bogus packet | | + | 3 invalid | One or more timestamp fields are | + | | invalid. This normally happens in | + | | symmetric modes when one peer sends | + | | the first packet to the other and | + | | before the other has received its | + | | first reply. | + | 4 access denied | The access controls have blacklisted | + | | the source. | + | 5 authentication failure | The cryptographic message digest does | + | | not match the MAC. | + | 6 unsynchronized | The server is not synchronized to a | + | | valid source. | + | 7 bad header data | One or more header fields are invalid. | + +--------------------------+----------------------------------------+ + +chrony doesn't do test #4, but adds four extra tests: +* maximum delay +* delay ratio +* delay dev ratio +* synchronisation loop. + +Those tests are roughly equivalent to ntpd 'flashers' +*/ + +// NTPTestDescMap maps bit mask with corresponding flash status +var NTPTestDescMap = map[uint16]string{ + 0x0001: "pkt_dup", + 0x0002: "pkt_bogus", + 0x0004: "pkt_invalid", + 0x0008: "pkt_auth", + 0x0010: "pkt_stratum", + 0x0020: "pkt_header", + 0x0040: "tst_max_delay", + 0x0080: "tst_delay_ratio", + 0x0100: "tst_delay_dev_ration", + 0x0200: "tst_sync_loop", +} + +// ReadNTPTestFlags returns list of failed ntp test flags (as strings) +func ReadNTPTestFlags(flags uint16) []string { + testFlags := flags & NTPFlagsTests + results := []string{} + for mask, message := range NTPTestDescMap { + if testFlags&mask == 0 { + results = append(results, message) + } + } + return results +} diff --git a/protocol/chrony/helpers_test.go b/protocol/chrony/helpers_test.go new file mode 100644 index 00000000..ba374f8d --- /dev/null +++ b/protocol/chrony/helpers_test.go @@ -0,0 +1,102 @@ +/* +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 ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFloat(t *testing.T) { + testCases := []struct { + in chronyFloat + out float64 + }{ + { + in: chronyFloat(0), + out: 0.0, + }, + { + in: chronyFloat(17091950), + out: -0.490620, + }, + { + in: chronyFloat(-90077357), + out: 0.039435696, + }, + } + + for _, testCase := range testCases { + // can't really compare big floats, thus measure delta + assert.InDelta( + t, + testCase.out, + testCase.in.ToFloat(), + 0.000001, + ) + } +} + +func TestRefidToString(t *testing.T) { + testCases := []struct { + in uint32 + out string + }{ + { + in: 0, + out: "", + }, + { + in: 1196446464, + out: "GPS", + }, + { + in: 2139029761, + out: "", + }, + } + + for _, testCase := range testCases { + assert.Equal( + t, + testCase.out, + RefidToString(testCase.in), + ) + } +} + +func TestNTPTestsFlagsString(t *testing.T) { + testCases := []struct { + in uint16 + out []string + }{ + { + in: 255, + out: []string{"tst_delay_dev_ration", "tst_sync_loop"}, + }, + { + in: 65535, + out: []string{}, + }, + } + + for _, testCase := range testCases { + assert.ElementsMatch( + t, + testCase.out, + ReadNTPTestFlags(testCase.in), + ) + } +} diff --git a/protocol/chrony/packet.go b/protocol/chrony/packet.go new file mode 100644 index 00000000..ba16b14c --- /dev/null +++ b/protocol/chrony/packet.go @@ -0,0 +1,555 @@ +/* +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 ( + "net" + "time" +) + +// 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 +) + +// request types. Only those we suppor, there are more +const ( + reqNSources CommandType = 14 + reqSourceData CommandType = 15 + reqTracking CommandType = 33 + reqServerStats CommandType = 54 + reqNtpData CommandType = 57 +) + +// reply types +const ( + rpyNSources ReplyType = 2 + rpySourceData ReplyType = 3 + rpyTracking ReplyType = 5 + rpyServerStats ReplyType = 14 + rpyNTPData ReplyType = 16 +) + +// 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", +} + +// SourceStateDesc provides mapping from SourceStateType to string +var SourceStateDesc = [6]string{ + "sync", + "unreach", + "falseticket", + "jittery", + "candidate", + "outlier", +} + +// 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 +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 +} + +// 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 + EOR int32 +} + +// 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 + EOR int32 +} + +// 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 + EOR int32 +} + +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 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 + EOR int32 +} + +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 usable version of 'ntp data' response +type ReplyNTPData struct { + replyHead + ntpData +} + +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 +} + +// 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, + }, + } +} + +// 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, + }, + } +}