diff --git a/lneto/dhcp/client.go b/lneto/dhcp/client.go new file mode 100644 index 0000000..ee5bc24 --- /dev/null +++ b/lneto/dhcp/client.go @@ -0,0 +1,56 @@ +package dhcp + +import ( + "io" +) + +type ClientV4 struct { + reqHostname []byte + hostname []byte + offer [4]byte + svip [4]byte + reqIP [4]byte + dns [4]byte + router [4]byte + subnet [4]byte + broadcast [4]byte + gateway [4]byte + optbuf [10]Option + currentXID uint32 + tRenew uint32 + tRebind uint32 + tIPLease uint32 + state ClientState +} + +type RequestConfig struct { + RequestedAddr [4]byte + // Optional hostname to request. + Hostname string +} + +func (c *ClientV4) BeginRequest(xid uint32, cfg RequestConfig) error { + c.currentXID = xid + c.reqHostname = append(c.reqHostname[:0], cfg.Hostname...) + c.reqIP = cfg.RequestedAddr + return nil +} + +func (c *ClientV4) Write(dst []byte) (int, error) { + if c.isClosed() { + return 0, io.EOF + } + frm, err := NewFrameV4(dst) + if err != nil { + return 0, err + } + // var options []Option + // var nextState ClientState + switch c.state { + case StateInit: + frm.MagicCookie() + } + return 0, nil +} + +func (c *ClientV4) isClosed() bool { return c.state == 0 } diff --git a/lneto/dhcp/definitions.go b/lneto/dhcp/definitions.go index 044d69e..dcd3dd7 100644 --- a/lneto/dhcp/definitions.go +++ b/lneto/dhcp/definitions.go @@ -5,7 +5,30 @@ import ( "fmt" ) -//go:generate stringer -type=OptNum,Op,MessageType -linecomment -output stringers.go +//go:generate stringer -type=OptNum,Op,MessageType,ClientState -linecomment -output stringers.go + +type ClientState uint8 + +// State transition table: +// +// StateInit -> | Send out Discover | -> StateSelecting +// StateSelecting -> |Accept Offer+Request| -> StateRequesting +// StateRequesting-> | Receive Ack | -> StateBound +const ( + _ ClientState = iota + // On clean slate boot, abort, NAK or decline enter the INIT state. + StateInit // init + // After sending out a Discover enter SELECTING. + StateSelecting // selecting + // After receiving a worthy offer and sending out request for offer enter REQUESTING. + StateRequesting // requesting + // On ACK to Request enter BOUND. + StateBound // bound + StateRenewing // renewing + StateRebinding // rebinding + StateInitReboot // init-reboot + StateRebooting // rebooting +) type Option struct { Num OptNum diff --git a/lneto/dhcp/frame.go b/lneto/dhcp/frame.go index 337bb88..688f70a 100644 --- a/lneto/dhcp/frame.go +++ b/lneto/dhcp/frame.go @@ -20,8 +20,11 @@ const ( DefaultServerPort = 67 ) -func NewFrameV4(buf []byte) FrameV4 { - return FrameV4{buf: buf} +func NewFrameV4(buf []byte) (FrameV4, error) { + if len(buf) < sizeHeader { + return FrameV4{}, errors.New("DHCP short frame") + } + return FrameV4{buf: buf}, nil } // Frame encapsulates the raw data of a DHCP packet @@ -94,6 +97,13 @@ func (frm FrameV4) MagicCookie() uint32 { return binary.BigEndian.Uint32(frm.buf[magicCookieOffset:]) } +// ClearHeader zeros out the header contents. +func (frm FrameV4) ClearHeader() { + for i := range frm.buf[:sizeHeader] { + frm.buf[i] = 0 + } +} + func (frm FrameV4) ForEachOption(fn func(opt Option) error) error { if fn == nil { return errors.New("nil function to parse DHCP") diff --git a/lneto/dhcp/stringers.go b/lneto/dhcp/stringers.go index 618b740..77e0e9c 100644 --- a/lneto/dhcp/stringers.go +++ b/lneto/dhcp/stringers.go @@ -1,4 +1,4 @@ -// Code generated by "stringer -type=OptNum,Op,MessageType -linecomment -output stringers.go"; DO NOT EDIT. +// Code generated by "stringer -type=OptNum,Op,MessageType,ClientState -linecomment -output stringers.go"; DO NOT EDIT. package dhcp @@ -126,3 +126,28 @@ func (i MessageType) String() string { } return _MessageType_name[_MessageType_index[i]:_MessageType_index[i+1]] } +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[StateInit-1] + _ = x[StateSelecting-2] + _ = x[StateRequesting-3] + _ = x[StateBound-4] + _ = x[StateRenewing-5] + _ = x[StateRebinding-6] + _ = x[StateInitReboot-7] + _ = x[StateRebooting-8] +} + +const _ClientState_name = "initselectingrequestingboundrenewingrebindinginit-rebootrebooting" + +var _ClientState_index = [...]uint8{0, 4, 13, 23, 28, 36, 45, 56, 65} + +func (i ClientState) String() string { + i -= 1 + if i >= ClientState(len(_ClientState_index)-1) { + return "ClientState(" + strconv.FormatInt(int64(i+1), 10) + ")" + } + return _ClientState_name[_ClientState_index[i]:_ClientState_index[i+1]] +} diff --git a/lneto/dns/definitions.go b/lneto/dns/definitions.go index 6c8b5ce..2ca830a 100644 --- a/lneto/dns/definitions.go +++ b/lneto/dns/definitions.go @@ -97,6 +97,13 @@ func (frm Frame) SetARCount(arCount uint16) { binary.BigEndian.PutUint16(frm.buf[10:12], arCount) } +// ClearHeader zeros out the fixed(non-variable) header contents. +func (frm Frame) ClearHeader() { + for i := range frm.buf[:SizeHeader] { + frm.buf[i] = 0 + } +} + // HeaderFlags gathers the flags in bits 16..31 of the header. type HeaderFlags uint16 diff --git a/lneto/frames.go b/lneto/frames.go index dce25a5..0482a25 100644 --- a/lneto/frames.go +++ b/lneto/frames.go @@ -125,6 +125,13 @@ func (efrm EthFrame) IsVLAN() bool { return efrm.EtherTypeOrSize() == EtherTypeVLAN } +// ClearHeader zeros out the fixed(non-variable) header contents. +func (frm EthFrame) ClearHeader() { + for i := range frm.buf[:sizeHeaderEthNoVLAN] { + frm.buf[i] = 0 + } +} + // ARPFrame encapsulates the raw data of an ARP packet // and provides methods for manipulating, validating and // retrieving fields and payload data. See [RFC826]. @@ -208,6 +215,13 @@ func (afrm ARPFrame) Target16() (hardwareAddr *[6]byte, proto *[16]byte) { return (*[6]byte)(afrm.buf[30:36]), (*[16]byte)(afrm.buf[36:52]) } +// ClearHeader zeros out the fixed(non-variable) header contents. +func (frm ARPFrame) ClearHeader() { + for i := range frm.buf[:8] { + frm.buf[i] = 0 + } +} + // IPv4Frame encapsulates the raw data of an IPv4 packet // and provides methods for manipulating, validating and // retreiving fields and payload data. See [RFC791]. @@ -356,6 +370,13 @@ func (ifrm IPv4Frame) Payload() []byte { return ifrm.buf[off:l] } +// ClearHeader zeros out the fixed(non-variable) header contents. +func (frm IPv4Frame) ClearHeader() { + for i := range frm.buf[:sizeHeaderIPv4] { + frm.buf[i] = 0 + } +} + // IPv6Frame encapsulates the raw data of an IPv6 packet // and provides methods for manipulating, validating and // retrieving fields and payload data. See [RFC8200]. @@ -443,6 +464,13 @@ func (ifrm IPv6Frame) crcWritePseudo(crc *CRC791) { crc.AddUint32(uint32(ifrm.NextHeader())) } +// ClearHeader zeros out the header contents. +func (frm IPv6Frame) ClearHeader() { + for i := range frm.buf[:sizeHeaderIPv6] { + frm.buf[i] = 0 + } +} + // TCPFrame encapsulates the raw data of a TCP segment // and provides methods for manipulating, validating and // retrieving fields and payload data. See [RFC9293]. @@ -581,6 +609,13 @@ func (tfrm TCPFrame) Options() []byte { return tfrm.buf[sizeHeaderTCP:tfrm.HeaderLength()] } +// ClearHeader zeros out the fixed(non-variable) header contents. +func (frm TCPFrame) ClearHeader() { + for i := range frm.buf[:sizeHeaderTCP] { + frm.buf[i] = 0 + } +} + // UDPFrame encapsulates the raw data of a UDP datagram // and provides methods for manipulating, validating and // retrieving fields and payload data. See [RFC768]. @@ -662,3 +697,10 @@ func (ufrm UDPFrame) CalculateIPv6Checksum(ifrm IPv6Frame) uint16 { crc.Write(ufrm.Payload()) return crc.Sum16() } + +// ClearHeader zeros out the header contents. +func (frm UDPFrame) ClearHeader() { + for i := range frm.buf[:sizeHeaderUDP] { + frm.buf[i] = 0 + } +} diff --git a/lneto/ntp/client.go b/lneto/ntp/client.go new file mode 100644 index 0000000..a35f036 --- /dev/null +++ b/lneto/ntp/client.go @@ -0,0 +1,135 @@ +package ntp + +import ( + "errors" + "io" + "time" +) + +type state uint8 + +const ( + stateClosed state = iota + stateSend1 + stateAwait1 + stateSend2 + stateAwait2 + stateDone +) + +const sysprecRecalcNeeded int8 = 127 + +func NewClient(now func() time.Time) *Client { + return &Client{ + _now: now, + _sysprec: sysprecRecalcNeeded, + } +} + +type Client struct { + start time.Time + _now func() time.Time + t [4]Timestamp + // org Timestamp + // rec Timestamp + xmt Timestamp + state state + _sysprec int8 +} + +func (c *Client) Write(payload []byte) (int, error) { + if c.isDone() { + return 0, io.EOF + } + frm, err := NewFrame(payload) + if err != nil { + return 0, err + } + + switch c.state { + case stateSend1: + c.start = c.now() + c.xmt = TimestampFromUint64(0) + c.state = stateAwait1 + case stateSend2: + c.xmt = c.unsyncTimestamp(c.now()) + c.state = stateDone + default: + return 0, nil // Nothing to handle. + } + + for i := range payload[:SizeHeader] { + payload[i] = 0 + } + sysprec := c.sysprec() + frm.ClearHeader() + frm.SetStratum(StratumUnsync) + frm.SetPoll(6) + frm.SetPrecision(sysprec) + frm.SetOriginTime(c.xmt) + frm.SetFlags(ModeClient, Version4, LeapNoWarning) + return SizeHeader, nil +} + +func (c *Client) read(payload []byte) error { + if c.isDone() { + return io.EOF + } + frm, err := NewFrame(payload) + if err != nil { + return err + } + t := &c.t + switch c.state { + case stateAwait1: + tstx := frm.TransmitTime() + tsorig := frm.OriginTime() + if tstx == tsorig || tsorig == c.xmt { + return errors.New("bogus NTP packet") + } + t[0] = tsorig + t[1] = frm.ReceiveTime() + t[2] = tstx + t[3] = c.unsyncTimestamp(c.now()) + c.state = stateDone + case stateAwait2: + c.state = stateAwait2 + } + return nil +} + +func (c *Client) isDone() bool { + return c.state == stateDone +} + +func (c *Client) now() time.Time { + if c._now == nil { + return time.Now() + } + return c._now() +} + +func (c *Client) unsyncTimestamp(now time.Time) Timestamp { + return TimestampFromUint64(0).Add(now.Sub(c.start)) +} + +func (c *Client) sysprec() int8 { + if c._sysprec == sysprecRecalcNeeded { + c._sysprec = CalculateSystemPrecision(c._now) + } + return c._sysprec +} + +// Now returns the current time as corrected by NTP protocol. +func (c *Client) Now() time.Time { + return c.now().Add(c.Offset()) +} + +// Offset returns the +func (c *Client) Offset() time.Duration { + if c.isDone() { + t := &c.t + return t[1].Sub(t[0])/2 + t[2].Sub(t[3])/2 + } + return 0 +} diff --git a/lneto/ntp/ntp.go b/lneto/ntp/ntp.go index 09f8942..a3babbb 100644 --- a/lneto/ntp/ntp.go +++ b/lneto/ntp/ntp.go @@ -24,10 +24,18 @@ const ( MinDispDiv = 200 // Minimum dispersion divisor 1/(200) == 0.005 ) -func NewFrame(buf []byte) Frame { - return Frame{buf: buf} +func NewFrame(buf []byte) (Frame, error) { + if len(buf) < SizeHeader { + return Frame{buf: nil}, errors.New("NTP frame too short") + } + return Frame{buf: buf}, nil } +// Frame encapsulates the raw data of an NTP packet +// and provides methods for manipulating, validating and +// retrieving fields and payload data. See [RFC5905]. +// +// [RFC5905]: https://tools.ietf.org/html/rfc5905 type Frame struct { buf []byte } @@ -123,6 +131,13 @@ func (frm Frame) SetTransmitTime(rt Timestamp) { rt.Put(frm.buf[40:48]) } +// ClearHeader zeros out the header contents. +func (frm Frame) ClearHeader() { + for i := range frm.buf[:SizeHeader] { + frm.buf[i] = 0 + } +} + type Short uint32 var baseTime = time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC) @@ -262,11 +277,23 @@ func SystemPrecision() int8 { } func recalculateSystemPrecision() { + sysPrec = CalculateSystemPrecision(nil) +} + +// CalculateSystemPrecision calculates the NTP system precision for a time source. +// If the time source is nil the default static call to [time.Now] is used. +func CalculateSystemPrecision(now func() time.Time) int8 { const maxIter = 16 var times [maxIter]time.Time - for i := 0; i < maxIter; i++ { - times[i] = time.Now() + if now == nil { + for i := 0; i < maxIter; i++ { + times[i] = time.Now() + } + } else { + for i := 0; i < maxIter; i++ { + times[i] = now() + } } avg := times[maxIter-1].Sub(times[0]) / maxIter - sysPrec = int8(math.Log2(avg.Seconds())) + return int8(math.Log2(avg.Seconds())) }