Skip to content

Commit

Permalink
Add Response.ReferenceString function
Browse files Browse the repository at this point in the history
ReferenceString returns a string representation of the response's
ReferenceID value that is based on the response's Stratum value.
  • Loading branch information
beevik committed Jul 22, 2023
1 parent c227df9 commit 8dcc21b
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 37 deletions.
45 changes: 40 additions & 5 deletions ntp.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"net"
"strconv"
"strings"
Expand Down Expand Up @@ -267,13 +268,13 @@ type Response struct {
// 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
// ReferenceID is a 32-bit integer identifying the server or reference
// clock. For stratum 1 servers, this is typically a meaningful
// zero-padded ASCII-encoded string assigned to the clock. For stratum 2+
// servers, this is a reference identifier for the server and is either
// the server's IPv4 address or a hash of its IPv6 address. For
// kiss-of-death responses (stratum=0), this field contains the
// ASCII-encoded "kiss code".
// kiss-of-death responses (stratum 0), this is the ASCII-encoded "kiss
// code".
ReferenceID uint32

// ReferenceTime is the time when the server's system clock was last
Expand Down Expand Up @@ -323,6 +324,40 @@ func (r *Response) IsKissOfDeath() bool {
return r.Stratum == 0
}

// ReferenceString returns the response's ReferenceID value formatted as a
// string. If the response's stratum is zero, then the "kiss o' death" string
// is returned. If stratum is one, then the server is a reference clock and
// the reference clock's name is returned. If stratum is two or greater, then
// the ID is either an IPv4 address or an MD5 hash of the IPv6 address; in
// either case the reference string is reported as 4 dot-separated
// decimal-based integers.
func (r *Response) ReferenceString() string {
if r.Stratum == 0 {
return kissCode(r.ReferenceID)
}

var b [4]byte
binary.BigEndian.PutUint32(b[:], r.ReferenceID)

if r.Stratum == 1 {
const dot = rune(0x22c5)
var r []rune
for i := range b {
if b[i] == 0 {
break
}
if b[i] >= 32 && b[i] <= 126 {
r = append(r, rune(b[i]))
} else {
r = append(r, dot)
}
}
return fmt.Sprintf(".%s.", string(r))
}

return fmt.Sprintf("%d.%d.%d.%d", b[0], b[1], b[2], b[3])
}

// Validate checks if the response is valid for the purposes of time
// synchronization.
func (r *Response) Validate() error {
Expand Down Expand Up @@ -725,7 +760,7 @@ func toInterval(t int8) time.Duration {
func kissCode(id uint32) string {
isPrintable := func(ch byte) bool { return ch >= 32 && ch <= 126 }

b := []byte{
b := [4]byte{
byte(id >> 24),
byte(id >> 16),
byte(id >> 8),
Expand All @@ -736,5 +771,5 @@ func kissCode(id uint32) string {
return ""
}
}
return string(b)
return string(b[:])
}
76 changes: 44 additions & 32 deletions ntp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
package ntp

import (
"encoding/binary"
"errors"
"fmt"
"net"
"strings"
"testing"
Expand Down Expand Up @@ -60,7 +58,7 @@ func logResponse(t *testing.T, r *Response) {
t.Logf("[%s] ~TrueTime: %s", host, now.Add(r.ClockOffset).Format(timeFormat))
t.Logf("[%s] XmitTime: %s", host, r.Time.Format(timeFormat))
t.Logf("[%s] Stratum: %d", host, r.Stratum)
t.Logf("[%s] RefID: %s (0x%08x)", host, formatRefID(r.ReferenceID, r.Stratum), r.ReferenceID)
t.Logf("[%s] RefID: %s (0x%08x)", host, r.ReferenceString(), r.ReferenceID)
t.Logf("[%s] RefTime: %s", host, r.ReferenceTime.Format(timeFormat))
t.Logf("[%s] RTT: %s", host, r.RTT)
t.Logf("[%s] Poll: %s", host, r.Poll)
Expand All @@ -73,35 +71,6 @@ func logResponse(t *testing.T, r *Response) {
t.Logf("[%s] KissCode: %s", host, stringOrEmpty(r.KissCode))
}

func formatRefID(id uint32, stratum uint8) string {
if stratum == 0 {
return "<kiss>"
}

b := make([]byte, 4)
binary.BigEndian.PutUint32(b, id)

// Stratum 1 ref IDs typically contain ASCII-encoded string identifiers.
if stratum == 1 {
const dot = rune(0x22c5)
var r []rune
for i := range b {
if b[i] == 0 {
break
}
if b[i] >= 32 && b[i] <= 126 {
r = append(r, rune(b[i]))
} else {
r = append(r, dot)
}
}
return fmt.Sprintf(".%s.", string(r))
}

// Stratum 2+ ref IDs typically contain IPv4 addresses.
return fmt.Sprintf("%d.%d.%d.%d", b[0], b[1], b[2], b[3])
}

func stringOrEmpty(s string) string {
if s == "" {
return "<empty>"
Expand Down Expand Up @@ -360,6 +329,49 @@ func TestOfflineOffsetCalculationNegative(t *testing.T) {
assert.Equal(t, expectedOffset, offset)
}

func TestOfflineReferenceString(t *testing.T) {
cases := []struct {
Stratum byte
RefID uint32
Str string
}{
{0, 0x41435354, "ACST"},
{0, 0x41555448, "AUTH"},
{0, 0x4155544f, "AUTO"},
{0, 0x42435354, "BCST"},
{0, 0x43525950, "CRYP"},
{0, 0x44454e59, "DENY"},
{0, 0x44524f50, "DROP"},
{0, 0x52535452, "RSTR"},
{0, 0x494e4954, "INIT"},
{0, 0x4d435354, "MCST"},
{0, 0x4e4b4559, "NKEY"},
{0, 0x4e54534e, "NTSN"},
{0, 0x52415445, "RATE"},
{0, 0x524d4f54, "RMOT"},
{0, 0x53544550, "STEP"},
{0, 0x01010101, ""},
{0, 0xfefefefe, ""},
{0, 0x01544450, ""},
{0, 0x41544401, ""},
{1, 0x47505300, ".GPS."},
{1, 0x474f4553, ".GOES."},
{2, 0x0a0a1401, "10.10.20.1"},
{3, 0xc0a80001, "192.168.0.1"},
{4, 0xc0a80001, "192.168.0.1"},
{5, 0xc0a80001, "192.168.0.1"},
{6, 0xc0a80001, "192.168.0.1"},
{7, 0xc0a80001, "192.168.0.1"},
{8, 0xc0a80001, "192.168.0.1"},
{9, 0xc0a80001, "192.168.0.1"},
{10, 0xc0a80001, "192.168.0.1"},
}
for _, c := range cases {
r := Response{Stratum: c.Stratum, ReferenceID: c.RefID}
assert.Equal(t, c.Str, r.ReferenceString())
}
}

func TestOfflineTimeConversions(t *testing.T) {
nowNtp := toNtpTime(time.Now())
now := nowNtp.Time()
Expand Down

0 comments on commit 8dcc21b

Please sign in to comment.