add ntp.Client and begin adding dhcp.Client

This commit is contained in:
soypat
2024-12-25 23:45:30 -03:00
parent 3e656491b9
commit c99076cd0b
8 changed files with 334 additions and 9 deletions
+56
View File
@@ -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 }
+24 -1
View File
@@ -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
+12 -2
View File
@@ -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")
+26 -1
View File
@@ -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]]
}
+7
View File
@@ -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
+42
View File
@@ -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
}
}
+135
View File
@@ -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
}
+32 -5
View File
@@ -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()))
}