diff --git a/ntp/chrony/packet.go b/ntp/chrony/packet.go index ad83aa85..51da62d2 100644 --- a/ntp/chrony/packet.go +++ b/ntp/chrony/packet.go @@ -77,6 +77,8 @@ const ( reqNTPData CommandType = 57 reqNTPSourceName CommandType = 65 reqSelectData CommandType = 69 + + reqClientAccessesByIndex3 CommandType = 68 ) // reply types @@ -94,8 +96,15 @@ const ( RpyServerStats3 ReplyType = 24 RpyServerStats4 ReplyType = 25 RpyNTPData2 ReplyType = 26 + + RpyClientAccessesByIndex3 ReplyType = 21 ) +// MaxClientAccessesByIndex is the fixed number of client slots returned per +// REQ_CLIENT_ACCESSES_BY_INDEX reply (MAX_CLIENT_ACCESSES in chrony's candm.h). +// Use it to size pagination loops over chronyd's client table. +const MaxClientAccessesByIndex = 8 + // source modes const ( SourceModeClient ModeType = 0 @@ -333,6 +342,25 @@ type RequestSelectData struct { data [maxDataLen - 4]uint8 } +// RequestClientAccessesByIndex - packet to request 'clients' data: a page of +// up to MaxClientAccessesByIndex per-client entries starting at FirstIndex. +// Mirrors REQ_ClientAccessesByIndex in chrony's candm.h. +// +// The trailing padding brings the total request size to 520 bytes, which is +// the minimum chronyd accepts for this command: chrony's PKL_CommandLength +// (pktlength.c) adds anti-amplification padding so the request is at least +// as large as the reply, and the v3 'clients' reply is 520 bytes. Smaller +// requests are rejected with BADPKTLENGTH. +type RequestClientAccessesByIndex struct { + RequestHead + FirstIndex uint32 + NClients uint32 + MinHits uint32 + Reset uint32 + EOR int32 + data [480]uint8 +} + // ReplyHead is the first (common) part of the reply packet, // in a format that can be directly passed to binary.Read type ReplyHead struct { @@ -886,6 +914,95 @@ func newSelectData(r *replySelectData) *SelectData { } } +// replyClientAccessesByIndexClient is the wire-format per-client entry, +// matching RPY_ClientAccesses_Client in chrony's candm.h. 60 bytes. +type replyClientAccessesByIndexClient struct { + IPAddr IPAddr + NTPHits uint32 + NKEHits uint32 + CmdHits uint32 + NTPDrops uint32 + NKEDrops uint32 + CmdDrops uint32 + NTPInterval int8 + NKEInterval int8 + CmdInterval int8 + NTPTimeoutInterval int8 + LastNTPHitAgo uint32 + LastNKEHitAgo uint32 + LastCmdHitAgo uint32 +} + +// replyClientAccessesByIndexContent is the wire-format reply, matching +// RPY_ClientAccessesByIndex in chrony's candm.h. The clients array is fixed +// at MaxClientAccessesByIndex slots; NClients tells you how many are valid. +type replyClientAccessesByIndexContent struct { + NIndices uint32 + NextIndex uint32 + NClients uint32 + Clients [MaxClientAccessesByIndex]replyClientAccessesByIndexClient +} + +// ClientAccess contains parsed per-client statistics from chronyd's +// 'clients' command. IPAddr is the raw chrony IPAddr; use ToNetIP for a +// resolved net.IP or String for chronyc-style output (which also handles +// unresolved entries as "ID#XXXXXXXXXX"). +type ClientAccess struct { + IPAddr *IPAddr + NTPHits uint32 + NKEHits uint32 + CmdHits uint32 + NTPDrops uint32 + NKEDrops uint32 + CmdDrops uint32 + NTPInterval int8 + NKEInterval int8 + CmdInterval int8 + NTPTimeoutInterval int8 + LastNTPHitAgo uint32 + LastNKEHitAgo uint32 + LastCmdHitAgo uint32 +} + +// ClientAccessesByIndex contains parsed 'clients' reply: chronyd's known +// index count, the next index to resume pagination from, and up to +// MaxClientAccessesByIndex per-client entries. +// +// Pagination is not atomic: chronyd's internal client table can change +// between consecutive page requests, so clients may be missed or seen +// twice when traversing the full table on a busy server. +type ClientAccessesByIndex struct { + NIndices uint32 + NextIndex uint32 + NClients uint32 + Clients []ClientAccess +} + +// ReplyClientAccessesByIndex is a usable version of the 'clients' reply. +type ReplyClientAccessesByIndex struct { + ReplyHead + ClientAccessesByIndex +} + +func newClientAccess(r *replyClientAccessesByIndexClient) *ClientAccess { + return &ClientAccess{ + IPAddr: &r.IPAddr, + NTPHits: r.NTPHits, + NKEHits: r.NKEHits, + CmdHits: r.CmdHits, + NTPDrops: r.NTPDrops, + NKEDrops: r.NKEDrops, + CmdDrops: r.CmdDrops, + NTPInterval: r.NTPInterval, + NKEInterval: r.NKEInterval, + CmdInterval: r.CmdInterval, + NTPTimeoutInterval: r.NTPTimeoutInterval, + LastNTPHitAgo: r.LastNTPHitAgo, + LastNKEHitAgo: r.LastNKEHitAgo, + LastCmdHitAgo: r.LastCmdHitAgo, + } +} + // here go request constructors // NewSourcesPacket creates new packet to request number of sources (peers) @@ -1001,6 +1118,48 @@ func NewSelectDataPacket(sourceID int32) *RequestSelectData { } } +// NewClientAccessesByIndexPacket creates new packet to request 'clients' +// information: a page of up to MaxClientAccessesByIndex per-client entries +// starting at firstIndex. minHits filters out clients with fewer hits. +// +// This sends REQ_CLIENT_ACCESSES_BY_INDEX3 and expects an +// RPY_CLIENT_ACCESSES_BY_INDEX3 reply. chronyd versions older than 4.0 do +// not understand this command and reply with BADPKTVERSION. +// +// chronyd restricts this command to the Unix socket by default (see +// ChronySocketPath); access over UDP port 323 requires an explicit +// 'cmdallow' rule in chrony.conf, otherwise the reply is ACCESSDENIED. +// +// Use NewClientAccessesByIndexResetPacket if you also want chronyd to +// clear its accounting table after the reply. +func NewClientAccessesByIndexPacket(firstIndex, nClients, minHits uint32) *RequestClientAccessesByIndex { + return newClientAccessesByIndexPacket(firstIndex, nClients, minHits, 0) +} + +// NewClientAccessesByIndexResetPacket is like NewClientAccessesByIndexPacket +// but additionally instructs chronyd to clear its accounting table after +// the reply (equivalent to chronyc's `-r` flag). Most polling consumers +// want the non-resetting form, since clearing the table invalidates rate +// calculations across scrape intervals. +func NewClientAccessesByIndexResetPacket(firstIndex, nClients, minHits uint32) *RequestClientAccessesByIndex { + return newClientAccessesByIndexPacket(firstIndex, nClients, minHits, 1) +} + +func newClientAccessesByIndexPacket(firstIndex, nClients, minHits, reset uint32) *RequestClientAccessesByIndex { + return &RequestClientAccessesByIndex{ + RequestHead: RequestHead{ + Version: protoVersionNumber, + PKTType: pktTypeCmdRequest, + Command: reqClientAccessesByIndex3, + }, + FirstIndex: firstIndex, + NClients: nClients, + MinHits: minHits, + Reset: reset, + data: [480]uint8{}, + } +} + // possible clock sources const ( ClockSourceUnspec = "unspec" @@ -1173,6 +1332,26 @@ func decodePacket(response []byte) (ResponsePacket, error) { ReplyHead: *head, SelectData: *newSelectData(data), }, nil + case RpyClientAccessesByIndex3: + data := new(replyClientAccessesByIndexContent) + if err = binary.Read(r, binary.BigEndian, data); err != nil { + return nil, err + } + Logger.Printf("response data: %+v", data) + n := min(data.NClients, MaxClientAccessesByIndex) + clients := make([]ClientAccess, n) + for i := range n { + clients[i] = *newClientAccess(&data.Clients[i]) + } + return &ReplyClientAccessesByIndex{ + ReplyHead: *head, + ClientAccessesByIndex: ClientAccessesByIndex{ + NIndices: data.NIndices, + NextIndex: data.NextIndex, + NClients: data.NClients, + Clients: clients, + }, + }, nil default: return nil, fmt.Errorf("not implemented reply type %d from %+v", head.Reply, head) } diff --git a/ntp/chrony/packet_test.go b/ntp/chrony/packet_test.go index f57d4b1e..61a8c2b3 100644 --- a/ntp/chrony/packet_test.go +++ b/ntp/chrony/packet_test.go @@ -654,6 +654,305 @@ func TestDecodeSelectData(t *testing.T) { require.Equal(t, want, packet) } +func TestDecodeClientAccessesByIndex3(t *testing.T) { + // Captured via `strace -xx -e trace=recvfrom chronyc clients` against a + // chronyd serving real clients. Client IPs have been substituted with + // RFC 5737 (192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24) and + // RFC 3849 (2001:db8::/32) documentation ranges; all other fields + // (hit counters, intervals, last-hit-ago sentinels) are unchanged. + raw := []uint8{ + 0x06, 0x02, 0x00, 0x00, 0x00, 0x44, 0x00, 0x15, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa6, 0x18, 0xf1, 0xf7, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, + 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, + 0x01, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xc0, 0x00, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, + 0x02, 0x91, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xc0, 0x00, 0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, + 0x00, 0xc9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xc6, 0x33, 0x64, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x06, 0x7f, 0x7f, 0x7f, 0x00, 0x00, + 0x01, 0x89, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xc6, 0x33, 0x64, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, + 0x02, 0x5d, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xcb, 0x00, 0x71, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x7f, 0x7f, 0x7f, 0x00, 0x00, + 0x00, 0x22, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xcb, 0x00, 0x71, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfc, 0x7f, 0x7f, 0x7f, 0x00, 0x00, + 0x00, 0x1a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xcb, 0x00, 0x71, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, + 0x01, 0xb1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + } + packet, err := decodePacket(raw) + require.Nil(t, err) + // chronyd uses 0x7f as an "interval unset" sentinel and 0xffffffff as + // "never hit" for the *_hit_ago fields when the protocol wasn't used. + want := &ReplyClientAccessesByIndex{ + ReplyHead: ReplyHead{ + Version: protoVersionNumber, + PKTType: pktTypeCmdReply, + Command: reqClientAccessesByIndex3, + Reply: RpyClientAccessesByIndex3, + Status: sttSuccess, + Sequence: 0xa618f1f7, + }, + ClientAccessesByIndex: ClientAccessesByIndex{ + NIndices: 4096, + NextIndex: 8, + NClients: 8, + Clients: []ClientAccess{ + { + IPAddr: &IPAddr{IP: IPToBytes(net.ParseIP("2001:db8::1")), Family: IPAddrInet6}, + NTPHits: 1, + NTPInterval: 127, + NKEInterval: 127, + CmdInterval: 127, + NTPTimeoutInterval: 127, + LastNTPHitAgo: 447, + LastNKEHitAgo: 0xffffffff, + LastCmdHitAgo: 0xffffffff, + }, + { + IPAddr: &IPAddr{IP: IPToBytes(net.ParseIP("192.0.2.1")), Family: IPAddrInet4}, + NTPHits: 1, + NTPInterval: 127, + NKEInterval: 127, + CmdInterval: 127, + NTPTimeoutInterval: 127, + LastNTPHitAgo: 657, + LastNKEHitAgo: 0xffffffff, + LastCmdHitAgo: 0xffffffff, + }, + { + IPAddr: &IPAddr{IP: IPToBytes(net.ParseIP("192.0.2.2")), Family: IPAddrInet4}, + NTPHits: 1, + NTPInterval: 127, + NKEInterval: 127, + CmdInterval: 127, + NTPTimeoutInterval: 127, + LastNTPHitAgo: 201, + LastNKEHitAgo: 0xffffffff, + LastCmdHitAgo: 0xffffffff, + }, + { + IPAddr: &IPAddr{IP: IPToBytes(net.ParseIP("198.51.100.3")), Family: IPAddrInet4}, + NTPHits: 2, + NTPInterval: 6, + NKEInterval: 127, + CmdInterval: 127, + NTPTimeoutInterval: 127, + LastNTPHitAgo: 393, + LastNKEHitAgo: 0xffffffff, + LastCmdHitAgo: 0xffffffff, + }, + { + IPAddr: &IPAddr{IP: IPToBytes(net.ParseIP("198.51.100.4")), Family: IPAddrInet4}, + NTPHits: 1, + NTPInterval: 127, + NKEInterval: 127, + CmdInterval: 127, + NTPTimeoutInterval: 127, + LastNTPHitAgo: 605, + LastNKEHitAgo: 0xffffffff, + LastCmdHitAgo: 0xffffffff, + }, + { + IPAddr: &IPAddr{IP: IPToBytes(net.ParseIP("203.0.113.5")), Family: IPAddrInet4}, + NTPHits: 7, + NTPInterval: 8, + NKEInterval: 127, + CmdInterval: 127, + NTPTimeoutInterval: 127, + LastNTPHitAgo: 34, + LastNKEHitAgo: 0xffffffff, + LastCmdHitAgo: 0xffffffff, + }, + { + IPAddr: &IPAddr{IP: IPToBytes(net.ParseIP("203.0.113.6")), Family: IPAddrInet4}, + NTPHits: 3, + NTPInterval: -4, + NKEInterval: 127, + CmdInterval: 127, + NTPTimeoutInterval: 127, + LastNTPHitAgo: 26, + LastNKEHitAgo: 0xffffffff, + LastCmdHitAgo: 0xffffffff, + }, + { + IPAddr: &IPAddr{IP: IPToBytes(net.ParseIP("203.0.113.7")), Family: IPAddrInet4}, + NTPHits: 1, + NTPInterval: 127, + NKEInterval: 127, + CmdInterval: 127, + NTPTimeoutInterval: 127, + LastNTPHitAgo: 433, + LastNKEHitAgo: 0xffffffff, + LastCmdHitAgo: 0xffffffff, + }, + }, + }, + } + require.Equal(t, want, packet) +} + +func TestDecodeClientAccessesByIndex3Empty(t *testing.T) { + raw := []uint8{ + // Reply head + 0x06, 0x02, 0x00, 0x00, 0x00, 0x44, 0x00, 0x15, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xde, 0xad, 0xbe, 0xef, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // NIndices=0, NextIndex=0, NClients=0 + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + } + raw = append(raw, make([]uint8, 8*60)...) + + packet, err := decodePacket(raw) + require.Nil(t, err) + rep, ok := packet.(*ReplyClientAccessesByIndex) + require.True(t, ok) + require.Equal(t, uint32(0), rep.NIndices) + require.Equal(t, uint32(0), rep.NextIndex) + require.Equal(t, uint32(0), rep.NClients) + require.Empty(t, rep.Clients) +} + +func TestEncodeClientAccessesByIndexPacket(t *testing.T) { + p := NewClientAccessesByIndexPacket(0, MaxClientAccessesByIndex, 0) + var buf bytes.Buffer + require.NoError(t, binary.Write(&buf, binary.BigEndian, p)) + // chronyd's PKL_CommandLength requires 520 bytes for + // REQ_CLIENT_ACCESSES_BY_INDEX3; smaller requests are rejected with + // BADPKTLENGTH. + require.Equal(t, 520, buf.Len()) + require.Equal(t, uint32(0), p.Reset) + require.Equal(t, reqClientAccessesByIndex3, p.Command) + + p2 := NewClientAccessesByIndexPacket(8, MaxClientAccessesByIndex, 5) + require.Equal(t, uint32(8), p2.FirstIndex) + require.Equal(t, uint32(MaxClientAccessesByIndex), p2.NClients) + require.Equal(t, uint32(5), p2.MinHits) + require.Equal(t, uint32(0), p2.Reset) +} + +func TestClientCommunicateClientAccessesByIndex(t *testing.T) { + // Sanitized real chronyd reply (same bytes as TestDecodeClientAccessesByIndex3, + // re-used here to exercise the full Client.Communicate read path including + // the response buffer sizing. + mockConn := &MockConnection{ + readData: []byte{ + 0x06, 0x02, 0x00, 0x00, 0x00, 0x44, 0x00, 0x15, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa6, 0x18, 0xf1, 0xf7, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, + 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, + 0x01, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xc0, 0x00, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, + 0x02, 0x91, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xc0, 0x00, 0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, + 0x00, 0xc9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xc6, 0x33, 0x64, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x06, 0x7f, 0x7f, 0x7f, 0x00, 0x00, + 0x01, 0x89, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xc6, 0x33, 0x64, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, + 0x02, 0x5d, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xcb, 0x00, 0x71, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x7f, 0x7f, 0x7f, 0x00, 0x00, + 0x00, 0x22, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xcb, 0x00, 0x71, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfc, 0x7f, 0x7f, 0x7f, 0x00, 0x00, + 0x00, 0x1a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xcb, 0x00, 0x71, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, + 0x01, 0xb1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + }, + } + client := &Client{Connection: mockConn} + + rpy, err := client.Communicate(NewClientAccessesByIndexPacket(0, MaxClientAccessesByIndex, 0)) + require.NoError(t, err) + r, ok := rpy.(*ReplyClientAccessesByIndex) + require.True(t, ok) + require.Equal(t, uint32(4096), r.NIndices) + require.Equal(t, uint32(8), r.NextIndex) + require.Equal(t, uint32(8), r.NClients) + require.Len(t, r.Clients, 8) + // Spot-check first and last entries + require.Equal(t, IPAddrInet6, r.Clients[0].IPAddr.Family) + require.Equal(t, uint32(1), r.Clients[0].NTPHits) + require.Equal(t, IPAddrInet4, r.Clients[7].IPAddr.Family) + require.Equal(t, "203.0.113.7", r.Clients[7].IPAddr.String()) +} + +func TestEncodeClientAccessesByIndexResetPacket(t *testing.T) { + p := NewClientAccessesByIndexResetPacket(0, MaxClientAccessesByIndex, 0) + var buf bytes.Buffer + require.NoError(t, binary.Write(&buf, binary.BigEndian, p)) + require.Equal(t, 520, buf.Len()) + require.Equal(t, uint32(1), p.Reset) + require.Equal(t, reqClientAccessesByIndex3, p.Command) +} + func TestSourceStateTypeToString(t *testing.T) { v := SourceStateUnreach got := v.String() @@ -756,6 +1055,10 @@ func TestPacketEncodingNoPanic(t *testing.T) { require.Less(t, buf.Len(), 500, "encoded packet should not exceed reasonable protocol limits") }) } + // RequestClientAccessesByIndex has its own dedicated test + // (TestEncodeClientAccessesByIndexPacket) because it exceeds the 500-byte + // limit above: chronyd's anti-amplification padding pushes the request to + // exactly 520 bytes. } func TestClientCommunicate(t *testing.T) { @@ -786,6 +1089,9 @@ func TestClientCommunicate(t *testing.T) { {"NTPSourceName", NewNTPSourceNamePacket(&IPAddr{IP: IPToBytes(net.ParseIP("127.0.0.1")), Family: IPAddrInet4})}, {"NTPData", NewNTPDataPacket(&IPAddr{IP: IPToBytes(net.ParseIP("127.0.0.1")), Family: IPAddrInet4})}, {"SelectData", NewSelectDataPacket(1)}, + // RequestClientAccessesByIndex is covered by the dedicated + // TestClientCommunicateClientAccessesByIndex test because its 520-byte + // wire size exceeds the 500-byte sanity limit asserted below. } for _, tc := range testCases { @@ -819,6 +1125,7 @@ func TestEORFieldInitialization(t *testing.T) { {"RequestNTPSourceName", NewNTPSourceNamePacket(&IPAddr{IP: IPToBytes(net.ParseIP("127.0.0.1")), Family: IPAddrInet4})}, {"RequestNTPData", NewNTPDataPacket(&IPAddr{IP: IPToBytes(net.ParseIP("127.0.0.1")), Family: IPAddrInet4})}, {"RequestSelectData", NewSelectDataPacket(42)}, + {"RequestClientAccessesByIndex", NewClientAccessesByIndexPacket(0, MaxClientAccessesByIndex, 0)}, } for _, tt := range packetsWithEOR { @@ -835,6 +1142,8 @@ func TestEORFieldInitialization(t *testing.T) { require.Equal(t, int32(0), p.EOR, "EOR should be initialized to 0") case *RequestSelectData: require.Equal(t, int32(0), p.EOR, "EOR should be initialized to 0") + case *RequestClientAccessesByIndex: + require.Equal(t, int32(0), p.EOR, "EOR should be initialized to 0") default: t.Errorf("Unexpected packet type: %T", p) }