kvm/vendor/github.com/beevik/ntp/ntp.go

828 lines
25 KiB
Go

// Copyright © 2015-2023 Brett Vickers.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package ntp provides an implementation of a Simple NTP (SNTP) client
// capable of querying the current time from a remote NTP server. See
// RFC 5905 (https://tools.ietf.org/html/rfc5905) for more details.
//
// This approach grew out of a go-nuts post by Michael Hofmann:
// https://groups.google.com/forum/?fromgroups#!topic/golang-nuts/FlcdMU5fkLQ
package ntp
import (
"bytes"
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"net"
"strconv"
"strings"
"time"
"golang.org/x/net/ipv4"
)
var (
ErrAuthFailed = errors.New("authentication failed")
ErrInvalidAuthKey = errors.New("invalid authentication key")
ErrInvalidDispersion = errors.New("invalid dispersion in response")
ErrInvalidLeapSecond = errors.New("invalid leap second in response")
ErrInvalidMode = errors.New("invalid mode in response")
ErrInvalidProtocolVersion = errors.New("invalid protocol version requested")
ErrInvalidStratum = errors.New("invalid stratum in response")
ErrInvalidTime = errors.New("invalid time reported")
ErrInvalidTransmitTime = errors.New("invalid transmit time in response")
ErrKissOfDeath = errors.New("kiss of death received")
ErrServerClockFreshness = errors.New("server clock not fresh")
ErrServerResponseMismatch = errors.New("server response didn't match request")
ErrServerTickedBackwards = errors.New("server clock ticked backwards")
)
// The LeapIndicator is used to warn if a leap second should be inserted
// or deleted in the last minute of the current month.
type LeapIndicator uint8
const (
// LeapNoWarning indicates no impending leap second.
LeapNoWarning LeapIndicator = 0
// LeapAddSecond indicates the last minute of the day has 61 seconds.
LeapAddSecond = 1
// LeapDelSecond indicates the last minute of the day has 59 seconds.
LeapDelSecond = 2
// LeapNotInSync indicates an unsynchronized leap second.
LeapNotInSync = 3
)
// Internal constants
const (
defaultNtpVersion = 4
defaultNtpPort = 123
nanoPerSec = 1000000000
maxStratum = 16
defaultTimeout = 5 * time.Second
maxPollInterval = (1 << 17) * time.Second
maxDispersion = 16 * time.Second
)
// Internal variables
var (
ntpEpoch = time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC)
)
type mode uint8
// NTP modes. This package uses only client mode.
const (
reserved mode = 0 + iota
symmetricActive
symmetricPassive
client
server
broadcast
controlMessage
reservedPrivate
)
// An ntpTime is a 64-bit fixed-point (Q32.32) representation of the number of
// seconds elapsed.
type ntpTime uint64
// Duration interprets the fixed-point ntpTime as a number of elapsed seconds
// and returns the corresponding time.Duration value.
func (t ntpTime) Duration() time.Duration {
sec := (t >> 32) * nanoPerSec
frac := (t & 0xffffffff) * nanoPerSec
nsec := frac >> 32
if uint32(frac) >= 0x80000000 {
nsec++
}
return time.Duration(sec + nsec)
}
// Time interprets the fixed-point ntpTime as an absolute time and returns
// the corresponding time.Time value.
func (t ntpTime) Time() time.Time {
return ntpEpoch.Add(t.Duration())
}
// toNtpTime converts the time.Time value t into its 64-bit fixed-point
// ntpTime representation.
func toNtpTime(t time.Time) ntpTime {
nsec := uint64(t.Sub(ntpEpoch))
sec := nsec / nanoPerSec
nsec = uint64(nsec-sec*nanoPerSec) << 32
frac := uint64(nsec / nanoPerSec)
if nsec%nanoPerSec >= nanoPerSec/2 {
frac++
}
return ntpTime(sec<<32 | frac)
}
// An ntpTimeShort is a 32-bit fixed-point (Q16.16) representation of the
// number of seconds elapsed.
type ntpTimeShort uint32
// Duration interprets the fixed-point ntpTimeShort as a number of elapsed
// seconds and returns the corresponding time.Duration value.
func (t ntpTimeShort) Duration() time.Duration {
sec := uint64(t>>16) * nanoPerSec
frac := uint64(t&0xffff) * nanoPerSec
nsec := frac >> 16
if uint16(frac) >= 0x8000 {
nsec++
}
return time.Duration(sec + nsec)
}
// header is an internal representation of an NTP packet header.
type header struct {
LiVnMode uint8 // Leap Indicator (2) + Version (3) + Mode (3)
Stratum uint8
Poll int8
Precision int8
RootDelay ntpTimeShort
RootDispersion ntpTimeShort
ReferenceID uint32 // KoD code if Stratum == 0
ReferenceTime ntpTime
OriginTime ntpTime
ReceiveTime ntpTime
TransmitTime ntpTime
}
// setVersion sets the NTP protocol version on the header.
func (h *header) setVersion(v int) {
h.LiVnMode = (h.LiVnMode & 0xc7) | uint8(v)<<3
}
// setMode sets the NTP protocol mode on the header.
func (h *header) setMode(md mode) {
h.LiVnMode = (h.LiVnMode & 0xf8) | uint8(md)
}
// setLeap modifies the leap indicator on the header.
func (h *header) setLeap(li LeapIndicator) {
h.LiVnMode = (h.LiVnMode & 0x3f) | uint8(li)<<6
}
// getVersion returns the version value in the header.
func (h *header) getVersion() int {
return int((h.LiVnMode >> 3) & 0x7)
}
// getMode returns the mode value in the header.
func (h *header) getMode() mode {
return mode(h.LiVnMode & 0x07)
}
// getLeap returns the leap indicator on the header.
func (h *header) getLeap() LeapIndicator {
return LeapIndicator((h.LiVnMode >> 6) & 0x03)
}
// An Extension adds custom behaviors capable of modifying NTP packets before
// being sent to the server and processing packets after being received by the
// server.
type Extension interface {
// ProcessQuery is called when the client is about to send a query to the
// NTP server. The buffer contains the NTP header. It may also contain
// extension fields added by extensions processed prior to this one.
ProcessQuery(buf *bytes.Buffer) error
// ProcessResponse is called after the client has received the server's
// NTP response. The buffer contains the entire message returned by the
// server.
ProcessResponse(buf []byte) error
}
// QueryOptions contains configurable options used by the QueryWithOptions
// function.
type QueryOptions struct {
// Timeout determines how long the client waits for a response from the
// server before failing with a timeout error. Defaults to 5 seconds.
Timeout time.Duration
// Version of the NTP protocol to use. Defaults to 4.
Version int
// LocalAddress contains the local IP address to use when creating a
// connection to the remote NTP server. This may be useful when the local
// system has more than one IP address. This address should not contain
// a port number.
LocalAddress string
// TTL specifies the maximum number of IP hops before the query datagram
// is dropped by the network. Defaults to the local system's default value.
TTL int
// Auth contains the settings used to configure NTP symmetric key
// authentication. See RFC 5905 for further details.
Auth AuthOptions
// Extensions may be added to modify NTP queries before they are
// transmitted and to process NTP responses after they arrive.
Extensions []Extension
// Dialer is a callback used to override the default UDP network dialer.
// The localAddress is directly copied from the LocalAddress field
// specified in QueryOptions. It may be the empty string or a host address
// (without port number). The remoteAddress is the "host:port" string
// derived from the first parameter to QueryWithOptions. The
// remoteAddress is guaranteed to include a port number.
Dialer func(localAddress, remoteAddress string) (net.Conn, error)
// Dial is a callback used to override the default UDP network dialer.
//
// DEPRECATED. Use Dialer instead.
Dial func(laddr string, lport int, raddr string, rport int) (net.Conn, error)
// Port indicates the port used to reach the remote NTP server.
//
// DEPRECATED. Embed the port number in the query address string instead.
Port int
}
// A Response contains time data, some of which is returned by the NTP server
// and some of which is calculated by this client.
type Response struct {
// Time is the transmit time reported by the server just before it
// responded to the client's NTP query. You should not use this value
// for time synchronization purposes. Use the ClockOffset instead.
Time time.Time
// ClockOffset is the estimated offset of the local system clock relative
// to the server's clock. Add this value to subsequent local system time
// measurements in order to obtain a more accurate time.
ClockOffset time.Duration
// RTT is the measured round-trip-time delay estimate between the client
// and the server.
RTT time.Duration
// Precision is the reported precision of the server's clock.
Precision time.Duration
// Version is the NTP protocol version number reported by the server.
Version int
// Stratum is the "stratum level" of the server. The smaller the number,
// the closer the server is to the reference clock. Stratum 1 servers are
// attached directly to the reference clock. A stratum value of 0
// indicates the "kiss of death," which typically occurs when the client
// issues too many requests to the server in a short period of time.
Stratum uint8
// 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 is the ASCII-encoded "kiss
// code".
ReferenceID uint32
// ReferenceTime is the time when the server's system clock was last
// set or corrected.
ReferenceTime time.Time
// RootDelay is the server's estimated aggregate round-trip-time delay to
// the stratum 1 server.
RootDelay time.Duration
// RootDispersion is the server's estimated maximum measurement error
// relative to the stratum 1 server.
RootDispersion time.Duration
// RootDistance is an estimate of the total synchronization distance
// between the client and the stratum 1 server.
RootDistance time.Duration
// Leap indicates whether a leap second should be added or removed from
// the current month's last minute.
Leap LeapIndicator
// MinError is a lower bound on the error between the client and server
// clocks. When the client and server are not synchronized to the same
// clock, the reported timestamps may appear to violate the principle of
// causality. In other words, the NTP server's response may indicate
// that a message was received before it was sent. In such cases, the
// minimum error may be useful.
MinError time.Duration
// KissCode is a 4-character string describing the reason for a
// "kiss of death" response (stratum=0). For a list of standard kiss
// codes, see https://tools.ietf.org/html/rfc5905#section-7.4.
KissCode string
// Poll is the maximum interval between successive NTP query messages to
// the server.
Poll time.Duration
authErr error
}
// IsKissOfDeath returns true if the response is a "kiss of death" from the
// remote server. If this function returns true, you may examine the
// response's KissCode value to determine the reason for the kiss of death.
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 {
// Forward authentication errors.
if r.authErr != nil {
return r.authErr
}
// Handle invalid stratum values.
if r.Stratum == 0 {
return ErrKissOfDeath
}
if r.Stratum >= maxStratum {
return ErrInvalidStratum
}
// Estimate the "freshness" of the time. If it exceeds the maximum
// polling interval (~36 hours), then it cannot be considered "fresh".
freshness := r.Time.Sub(r.ReferenceTime)
if freshness > maxPollInterval {
return ErrServerClockFreshness
}
// Calculate the peer synchronization distance, lambda:
// lambda := RootDelay/2 + RootDispersion
// If this value exceeds MAXDISP (16s), then the time is not suitable
// for synchronization purposes.
// https://tools.ietf.org/html/rfc5905#appendix-A.5.1.1.
lambda := r.RootDelay/2 + r.RootDispersion
if lambda > maxDispersion {
return ErrInvalidDispersion
}
// If the server's transmit time is before its reference time, the
// response is invalid.
if r.Time.Before(r.ReferenceTime) {
return ErrInvalidTime
}
// Handle invalid leap second indicator.
if r.Leap == LeapNotInSync {
return ErrInvalidLeapSecond
}
// nil means the response is valid.
return nil
}
// Query requests time data from a remote NTP server. The response contains
// information from which a more accurate local time can be inferred.
//
// The server address is of the form "host", "host:port", "host%zone:port",
// "[host]:port" or "[host%zone]:port". The host may contain an IPv4, IPv6 or
// domain name address. When specifying both a port and an IPv6 address, one
// of the bracket formats must be used. If no port is included, NTP default
// port 123 is used.
func Query(address string) (*Response, error) {
return QueryWithOptions(address, QueryOptions{})
}
// QueryWithOptions performs the same function as Query but allows for the
// customization of certain query behaviors. See the comments for Query and
// QueryOptions for further details.
func QueryWithOptions(address string, opt QueryOptions) (*Response, error) {
h, now, err := getTime(address, &opt)
if err != nil && err != ErrAuthFailed {
return nil, err
}
return generateResponse(h, now, err), nil
}
// Time returns the current, corrected local time using information returned
// from the remote NTP server. On error, Time returns the uncorrected local
// system time.
//
// The server address is of the form "host", "host:port", "host%zone:port",
// "[host]:port" or "[host%zone]:port". The host may contain an IPv4, IPv6 or
// domain name address. When specifying both a port and an IPv6 address, one
// of the bracket formats must be used. If no port is included, NTP default
// port 123 is used.
func Time(address string) (time.Time, error) {
r, err := Query(address)
if err != nil {
return time.Now(), err
}
err = r.Validate()
if err != nil {
return time.Now(), err
}
// Use the response's clock offset to calculate an accurate time.
return time.Now().Add(r.ClockOffset), nil
}
// getTime performs the NTP server query and returns the response header
// along with the local system time it was received.
func getTime(address string, opt *QueryOptions) (*header, ntpTime, error) {
if opt.Timeout == 0 {
opt.Timeout = defaultTimeout
}
if opt.Version == 0 {
opt.Version = defaultNtpVersion
}
if opt.Version < 2 || opt.Version > 4 {
return nil, 0, ErrInvalidProtocolVersion
}
if opt.Port == 0 {
opt.Port = defaultNtpPort
}
if opt.Dial != nil {
// wrapper for the deprecated Dial callback.
opt.Dialer = func(la, ra string) (net.Conn, error) {
return dialWrapper(la, ra, opt.Dial)
}
}
if opt.Dialer == nil {
opt.Dialer = defaultDialer
}
// Compose a conforming host:port remote address string if the address
// string doesn't already contain a port.
remoteAddress, err := fixHostPort(address, opt.Port)
if err != nil {
return nil, 0, err
}
// Connect to the remote server.
con, err := opt.Dialer(opt.LocalAddress, remoteAddress)
if err != nil {
return nil, 0, err
}
defer con.Close()
// Set a TTL for the packet if requested.
if opt.TTL != 0 {
ipcon := ipv4.NewConn(con)
err = ipcon.SetTTL(opt.TTL)
if err != nil {
return nil, 0, err
}
}
// Set a timeout on the connection.
con.SetDeadline(time.Now().Add(opt.Timeout))
// Allocate a buffer big enough to hold an entire response datagram.
recvBuf := make([]byte, 8192)
recvHdr := new(header)
// Allocate the query message header.
xmitHdr := new(header)
xmitHdr.setMode(client)
xmitHdr.setVersion(opt.Version)
xmitHdr.setLeap(LeapNoWarning)
xmitHdr.Precision = 0x20
// To help prevent spoofing and client fingerprinting, use a
// cryptographically random 64-bit value for the TransmitTime. See:
// https://www.ietf.org/archive/id/draft-ietf-ntp-data-minimization-04.txt
bits := make([]byte, 8)
_, err = rand.Read(bits)
if err != nil {
return nil, 0, err
}
xmitHdr.TransmitTime = ntpTime(binary.BigEndian.Uint64(bits))
// Write the query header to a transmit buffer.
var xmitBuf bytes.Buffer
binary.Write(&xmitBuf, binary.BigEndian, xmitHdr)
// Allow extensions to process the query and add to the transmit buffer.
for _, e := range opt.Extensions {
err = e.ProcessQuery(&xmitBuf)
if err != nil {
return nil, 0, err
}
}
// If using symmetric key authentication, decode and validate the auth key
// string.
authKey, err := decodeAuthKey(opt.Auth)
if err != nil {
return nil, 0, err
}
// Append a MAC if authentication is being used.
appendMAC(&xmitBuf, opt.Auth, authKey)
// Transmit the query and keep track of when it was transmitted.
xmitTime := time.Now()
_, err = con.Write(xmitBuf.Bytes())
if err != nil {
return nil, 0, err
}
// Receive the response.
recvBytes, err := con.Read(recvBuf)
if err != nil {
return nil, 0, err
}
// Keep track of the time the response was received. As of go 1.9, the
// time package uses a monotonic clock, so delta will never be less than
// zero for go version 1.9 or higher.
delta := time.Since(xmitTime)
if delta < 0 {
delta = 0
}
recvTime := xmitTime.Add(delta)
// Parse the response header.
recvBuf = recvBuf[:recvBytes]
recvReader := bytes.NewReader(recvBuf)
err = binary.Read(recvReader, binary.BigEndian, recvHdr)
if err != nil {
return nil, 0, err
}
// Allow extensions to process the response.
for i := len(opt.Extensions) - 1; i >= 0; i-- {
err = opt.Extensions[i].ProcessResponse(recvBuf)
if err != nil {
return nil, 0, err
}
}
// Check for invalid fields.
if recvHdr.getMode() != server {
return nil, 0, ErrInvalidMode
}
if recvHdr.TransmitTime == ntpTime(0) {
return nil, 0, ErrInvalidTransmitTime
}
if recvHdr.OriginTime != xmitHdr.TransmitTime {
return nil, 0, ErrServerResponseMismatch
}
if recvHdr.ReceiveTime > recvHdr.TransmitTime {
return nil, 0, ErrServerTickedBackwards
}
// Correct the received message's origin time using the actual
// transmit time.
recvHdr.OriginTime = toNtpTime(xmitTime)
// Perform authentication of the server response.
authErr := verifyMAC(recvBuf, opt.Auth, authKey)
return recvHdr, toNtpTime(recvTime), authErr
}
// defaultDialer provides a UDP dialer based on Go's built-in net stack.
func defaultDialer(localAddress, remoteAddress string) (net.Conn, error) {
var laddr *net.UDPAddr
if localAddress != "" {
var err error
laddr, err = net.ResolveUDPAddr("udp", net.JoinHostPort(localAddress, "0"))
if err != nil {
return nil, err
}
}
raddr, err := net.ResolveUDPAddr("udp", remoteAddress)
if err != nil {
return nil, err
}
return net.DialUDP("udp", laddr, raddr)
}
// dialWrapper is used to wrap the deprecated Dial callback in QueryOptions.
func dialWrapper(la, ra string,
dial func(la string, lp int, ra string, rp int) (net.Conn, error)) (net.Conn, error) {
rhost, rport, err := net.SplitHostPort(ra)
if err != nil {
return nil, err
}
rportValue, err := strconv.Atoi(rport)
if err != nil {
return nil, err
}
return dial(la, 0, rhost, rportValue)
}
// fixHostPort examines an address in one of the accepted forms and fixes it
// to include a port number if necessary.
func fixHostPort(address string, defaultPort int) (fixed string, err error) {
if len(address) == 0 {
return "", errors.New("address string is empty")
}
// If the address is wrapped in brackets, append a port if necessary.
if address[0] == '[' {
end := strings.IndexByte(address, ']')
switch {
case end < 0:
return "", errors.New("missing ']' in address")
case end+1 == len(address):
return fmt.Sprintf("%s:%d", address, defaultPort), nil
case address[end+1] == ':':
return address, nil
default:
return "", errors.New("unexpected character following ']' in address")
}
}
// No colons? Must be a port-less IPv4 or domain address.
last := strings.LastIndexByte(address, ':')
if last < 0 {
return fmt.Sprintf("%s:%d", address, defaultPort), nil
}
// Exactly one colon? A port have been included along with an IPv4 or
// domain address. (IPv6 addresses are guaranteed to have more than one
// colon.)
prev := strings.LastIndexByte(address[:last], ':')
if prev < 0 {
return address, nil
}
// Two or more colons means we must have an IPv6 address without a port.
return fmt.Sprintf("[%s]:%d", address, defaultPort), nil
}
// generateResponse processes NTP header fields along with the its receive
// time to generate a Response record.
func generateResponse(h *header, recvTime ntpTime, authErr error) *Response {
r := &Response{
Time: h.TransmitTime.Time(),
ClockOffset: offset(h.OriginTime, h.ReceiveTime, h.TransmitTime, recvTime),
RTT: rtt(h.OriginTime, h.ReceiveTime, h.TransmitTime, recvTime),
Precision: toInterval(h.Precision),
Version: h.getVersion(),
Stratum: h.Stratum,
ReferenceID: h.ReferenceID,
ReferenceTime: h.ReferenceTime.Time(),
RootDelay: h.RootDelay.Duration(),
RootDispersion: h.RootDispersion.Duration(),
Leap: h.getLeap(),
MinError: minError(h.OriginTime, h.ReceiveTime, h.TransmitTime, recvTime),
Poll: toInterval(h.Poll),
authErr: authErr,
}
// Calculate values depending on other calculated values
r.RootDistance = rootDistance(r.RTT, r.RootDelay, r.RootDispersion)
// If a kiss of death was received, interpret the reference ID as
// a kiss code.
if r.Stratum == 0 {
r.KissCode = kissCode(r.ReferenceID)
}
return r
}
// The following helper functions calculate additional metadata about the
// timestamps received from an NTP server. The timestamps returned by
// the server are given the following variable names:
//
// org = Origin Timestamp (client send time)
// rec = Receive Timestamp (server receive time)
// xmt = Transmit Timestamp (server reply time)
// dst = Destination Timestamp (client receive time)
func rtt(org, rec, xmt, dst ntpTime) time.Duration {
a := int64(dst - org)
b := int64(xmt - rec)
rtt := a - b
if rtt < 0 {
rtt = 0
}
return ntpTime(rtt).Duration()
}
func offset(org, rec, xmt, dst ntpTime) time.Duration {
// The inputs are 64-bit unsigned integer timestamps. These timestamps can
// "roll over" at the end of an NTP era, which occurs approximately every
// 136 years starting from the year 1900. To ensure an accurate offset
// calculation when an era boundary is crossed, we need to take care that
// the difference between two 64-bit timestamp values is accurately
// calculated even when they are in neighboring eras.
//
// See: https://www.eecis.udel.edu/~mills/y2k.html
a := int64(rec - org)
b := int64(xmt - dst)
offset := a + (b-a)/2
if offset < 0 {
return -ntpTime(-offset).Duration()
}
return ntpTime(offset).Duration()
}
func minError(org, rec, xmt, dst ntpTime) time.Duration {
// Each NTP response contains two pairs of send/receive timestamps.
// When either pair indicates a "causality violation", we calculate the
// error as the difference in time between them. The minimum error is
// the greater of the two causality violations.
var error0, error1 ntpTime
if org >= rec {
error0 = org - rec
}
if xmt >= dst {
error1 = xmt - dst
}
if error0 > error1 {
return error0.Duration()
}
return error1.Duration()
}
func rootDistance(rtt, rootDelay, rootDisp time.Duration) time.Duration {
// The root distance is:
// the maximum error due to all causes of the local clock
// relative to the primary server. It is defined as half the
// total delay plus total dispersion plus peer jitter.
// (https://tools.ietf.org/html/rfc5905#appendix-A.5.5.2)
//
// In the reference implementation, it is calculated as follows:
// rootDist = max(MINDISP, rootDelay + rtt)/2 + rootDisp
// + peerDisp + PHI * (uptime - peerUptime)
// + peerJitter
// For an SNTP client which sends only a single packet, most of these
// terms are irrelevant and become 0.
totalDelay := rtt + rootDelay
return totalDelay/2 + rootDisp
}
func toInterval(t int8) time.Duration {
switch {
case t > 0:
return time.Duration(uint64(time.Second) << uint(t))
case t < 0:
return time.Duration(uint64(time.Second) >> uint(-t))
default:
return time.Second
}
}
func kissCode(id uint32) string {
isPrintable := func(ch byte) bool { return ch >= 32 && ch <= 126 }
b := [4]byte{
byte(id >> 24),
byte(id >> 16),
byte(id >> 8),
byte(id),
}
for _, ch := range b {
if !isPrintable(ch) {
return ""
}
}
return string(b[:])
}