diff --git a/lneto/dhcp/client.go b/lneto/dhcp/client.go deleted file mode 100644 index ee5bc24..0000000 --- a/lneto/dhcp/client.go +++ /dev/null @@ -1,56 +0,0 @@ -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/dhcpv4/client.go b/lneto/dhcpv4/client.go new file mode 100644 index 0000000..01cc811 --- /dev/null +++ b/lneto/dhcpv4/client.go @@ -0,0 +1,260 @@ +package dhcpv4 + +import ( + "encoding/binary" + "errors" + "fmt" + "io" + "math" + "math/bits" +) + +type Client struct { + reqHostname string + hostname []byte + dns [][4]byte + + tRenew uint32 + tRebind uint32 + tIPLease uint32 + currentXID uint32 + state ClientState + offer [4]byte + svip [4]byte + reqIP [4]byte + router [4]byte + subnet [4]byte + broadcast [4]byte + gateway [4]byte + clientMAC [6]byte + + auxbuf [64]byte +} + +type RequestConfig struct { + RequestedAddr [4]byte + ClientHardwareAddr [6]byte + // Optional hostname to request. + Hostname string +} + +func (c *Client) BeginRequest(xid uint32, cfg RequestConfig) error { + if len(cfg.Hostname) > 36 { + return errors.New("requested hostname too long") + } + c.reset(xid) + c.currentXID = xid + c.reqHostname = cfg.Hostname + c.reqIP = cfg.RequestedAddr + c.clientMAC = cfg.ClientHardwareAddr + return nil +} + +func (c *Client) Send(dst []byte) (int, error) { + if c.isClosed() { + return 0, io.EOF + } else if c.state == StateSelecting && c.offer == [4]byte{} { + return 0, nil // No offer received yet. + } else if c.state == StateBound { + return 0, nil // Done! + } + frm, err := NewFrame(dst) + if err != nil { + return 0, err + } + + // var options []Option + // var nextState ClientState + optBuf := c.auxbuf[:0] + var nextState ClientState + switch c.state { + case StateInit: + // Send out discover. + optBuf = AppendOption(optBuf, OptMessageType, byte(MsgDiscover)) + optBuf = AppendOption(optBuf, OptParameterRequestList, defaultParamReqList...) + optBuf = AppendOption(optBuf, OptClientIdentifier, c.clientMAC[:]...) + maxlen := len(dst) + if maxlen > math.MaxUint16 { + maxlen = math.MaxUint16 + } + optBuf = AppendOption(optBuf, OptMaximumMessageSize, byte(maxlen>>8), byte(maxlen)) + if c.reqIP != [4]byte{} { + optBuf = AppendOption(optBuf, OptRequestedIPaddress, c.reqIP[:]...) + } + nextState = StateSelecting + + case StateSelecting: + // Send out request, we know we've received an offer by now. + optBuf = AppendOption(optBuf, OptMessageType, byte(MsgRequest)) + optBuf = AppendOption(optBuf, OptRequestedIPaddress, c.offer[:]...) + optBuf = AppendOption(optBuf, OptServerIdentification, c.svip[:]...) + nextState = StateRequesting + + default: + return 0, errors.New("unhandled state") + } + if len(c.reqHostname) > 0 { + optBuf = append(optBuf, byte(OptHostName), byte(len(c.hostname))) + optBuf = append(optBuf, c.hostname...) + } + optBuf = append(optBuf, 0xff) // End mark. + options := frm.OptionsPayload() + if len(optBuf) > len(options) { + return 0, errors.New("DHCPv4 short buffer for options") + } + c.setHeader(frm) + n := copy(options, optBuf) + c.state = nextState + return optionsOffset + n, nil +} + +func (c *Client) Recv(pkt []byte) error { + if c.isClosed() { + return io.EOF + } + frm, err := NewFrame(pkt) + if err != nil { + return err + } else if frm.XID() != c.currentXID { + return errors.New("dhcpv4 unexpected transaction ID") + } else if frm.MagicCookie() != MagicCookie { + return errors.New("dhcpv4 bad magic cookie") + } + msgType := c.getMessageType(frm) + if msgType == MsgNack { + return errors.New("dhcp nack received") + } + + msgOK := msgType == MsgOffer || msgType == MsgAck + if !msgOK { + return fmt.Errorf("invalid DHCP message received or none got=%d", msgType) + } + err = c.setOptions(frm) + if err != nil { + return err + } + + switch c.state { + case StateSelecting: + if msgType == MsgOffer && c.offer == [4]byte{} { + // Lock in on this offer. + c.gateway = *frm.GIAddr() + c.offer = *frm.YIAddr() + } + + case StateRequesting: + if msgType == MsgAck { + c.state = StateBound + } + default: + return fmt.Errorf("dcpv4 unexpected state in recv %s", c.state.String()) + } + return nil +} + +func (c *Client) getMessageType(frm Frame) MessageType { + c.auxbuf[0] = 255 + ptrMsgType := &c.auxbuf[0] + frm.ForEachOption(func(opt OptNum, data []byte) error { + if len(data) == 1 { + *ptrMsgType = data[0] + return io.EOF + } + return nil + }) + return MessageType(*ptrMsgType) +} + +func (c *Client) setOptions(frm Frame) error { + return frm.ForEachOption(func(opt OptNum, data []byte) error { + switch opt { + case OptRenewTimeValue: + c.tRenew = maybeU32(data) + case OptIPAddressLeaseTime: + c.tIPLease = maybeU32(data) + case OptRebindingTimeValue: + c.tRebind = maybeU32(data) + + case OptServerIdentification: + c.svip = maybe4byte(data) + case OptRouter: + c.router = maybe4byte(data) + case OptBroadcastAddress: + c.broadcast = maybe4byte(data) + case OptSubnetMask: + c.subnet = maybe4byte(data) + + case OptHostName: + if len(data) < maxHostSize { + c.hostname = append(c.hostname[:0], data...) + } + case OptDNSServers: + if len(c.dns) > 0 || len(data)%4 != 0 { + return nil // No DNS parsing if already got in previous exchange. + } + for i := 0; i < len(data); i += 4 { + c.dns = append(c.dns, [4]byte(data[i:i+4])) + } + } + return nil + }) +} + +func (c *Client) isClosed() bool { return c.state == 0 || c.currentXID == 0 } + +func (c *Client) setHeader(frm Frame) { + frm.ClearHeader() + frm.SetOp(OpRequest) + frm.SetXID(c.currentXID) + frm.SetHardware(1, 6, 0) + frm.SetSecs(1) + // copy(frm.CIAddr()[:], c.offer[:]) + copy(frm.SIAddr()[:], c.svip[:]) + copy(frm.YIAddr()[:], c.offer[:]) + copy(frm.CHAddrAs6()[:], c.clientMAC[:]) + frm.SetMagicCookie(MagicCookie) +} + +func (c *Client) reset(xid uint32) { + *c = Client{ + reqHostname: c.reqHostname, + currentXID: xid, + reqIP: c.reqIP, + clientMAC: c.clientMAC, + } +} + +func (d *Client) State() ClientState { return d.state } + +func (d *Client) CIDRBits() uint8 { + if d.subnet == [4]byte{} { + return 0 + } + v := binary.BigEndian.Uint32(d.subnet[:]) + return 32 - uint8(bits.TrailingZeros32(v)) +} + +var defaultParamReqList = []byte{ + byte(OptSubnetMask), + byte(OptTimeOffset), + byte(OptRouter), + byte(OptInterfaceMTUSize), + byte(OptBroadcastAddress), + byte(OptDNSServers), + byte(OptDomainName), + byte(OptNTPServersAddresses), +} + +func maybeU32(b []byte) uint32 { + if len(b) < 4 { + return 0 + } + return binary.BigEndian.Uint32(b) +} + +func maybe4byte(b []byte) [4]byte { + if len(b) != 4 { + return [4]byte{} + } + return [4]byte(b) +} diff --git a/lneto/dhcp/definitions.go b/lneto/dhcpv4/definitions.go similarity index 90% rename from lneto/dhcp/definitions.go rename to lneto/dhcpv4/definitions.go index dcd3dd7..17bdd71 100644 --- a/lneto/dhcp/definitions.go +++ b/lneto/dhcpv4/definitions.go @@ -1,19 +1,18 @@ -package dhcp +package dhcpv4 import ( "errors" - "fmt" ) //go:generate stringer -type=OptNum,Op,MessageType,ClientState -linecomment -output stringers.go -type ClientState uint8 - -// State transition table: +// ClientState transition table during request: // // StateInit -> | Send out Discover | -> StateSelecting // StateSelecting -> |Accept Offer+Request| -> StateRequesting // StateRequesting-> | Receive Ack | -> StateBound +type ClientState uint8 + const ( _ ClientState = iota // On clean slate boot, abort, NAK or decline enter the INIT state. @@ -30,26 +29,26 @@ const ( StateRebooting // rebooting ) -type Option struct { - Num OptNum - Data []byte +func AppendOption(dst []byte, opt OptNum, data ...byte) []byte { + if len(data) > 255 { + panic("option data too long") + } + dst = append(dst, byte(opt), byte(len(data))) + dst = append(dst, data...) + return dst } -func (opt *Option) String() string { - return opt.Num.String() + ":" + fmt.Sprint(opt.Data) -} - -func (opt *Option) Encode(dst []byte) (int, error) { - if len(opt.Data) > 255 { - return 0, errors.New("DHCP option data too long") - } else if len(dst) < 2+len(opt.Data) { +func EncodeOption(dst []byte, opt OptNum, data ...byte) (int, error) { + if len(data) > 255 { + return 0, errors.New("DHCPv4 option data too long (>255)") + } else if len(dst) < 2+len(data) { return 0, errors.New("DHCP option buffer too short") } - _ = dst[2+len(opt.Data)] - dst[0] = byte(opt.Num) - dst[1] = byte(len(opt.Data)) - copy(dst[2:], opt.Data) - return 2 + len(opt.Data), nil + _ = dst[2+len(data)] + dst[0] = byte(opt) + dst[1] = byte(len(data)) + copy(dst[2:], data) + return 2 + len(data), nil } type OptNum uint8 @@ -137,7 +136,9 @@ const ( MsgRequest // request MsgDecline // decline MsgAck // ack - MsgNak // nak + MsgNack // nak MsgRelease // release MsgInform // inform ) + +type Flags uint16 diff --git a/lneto/dhcp/frame.go b/lneto/dhcpv4/frame.go similarity index 52% rename from lneto/dhcp/frame.go rename to lneto/dhcpv4/frame.go index 688f70a..e2f4fd6 100644 --- a/lneto/dhcp/frame.go +++ b/lneto/dhcpv4/frame.go @@ -1,4 +1,4 @@ -package dhcp +package dhcpv4 import ( "encoding/binary" @@ -6,6 +6,7 @@ import ( ) const ( + maxHostSize = 16 // max size for hostname. sizeSName = 64 // Server name, part of BOOTP too. sizeBootFile = 128 // Boot file name, Legacy. sizeHeader = 44 @@ -20,11 +21,13 @@ const ( DefaultServerPort = 67 ) -func NewFrameV4(buf []byte) (FrameV4, error) { - if len(buf) < sizeHeader { - return FrameV4{}, errors.New("DHCP short frame") +// NewFrame returns a new DHCPv4 Frame with data set to buf. +// An error is returned if the buffer size is smaller than 240. +func NewFrame(buf []byte) (Frame, error) { + if len(buf) < optionsOffset { + return Frame{}, errors.New("DHCPv4 short frame") } - return FrameV4{buf: buf}, nil + return Frame{buf: buf}, nil } // Frame encapsulates the raw data of a DHCP packet @@ -32,79 +35,81 @@ func NewFrameV4(buf []byte) (FrameV4, error) { // retrieving fields and payload data. See [RFC2131]. // // [RFC2131]: https://tools.ietf.org/html/rfc2131 -type FrameV4 struct { +type Frame struct { buf []byte } -func (frm FrameV4) Op() Op { - return Op(frm.buf[0]) +// OptionsPayload returns the options portion of the DHCP frame. May be zero lengthed. +func (frm Frame) OptionsPayload() []byte { + return frm.buf[:optionsOffset] } -func (frm FrameV4) Hardware() (Type, Len, Ops uint8) { +func (frm Frame) Op() Op { return Op(frm.buf[0]) } +func (frm Frame) SetOp(op Op) { frm.buf[0] = byte(op) } + +func (frm Frame) Hardware() (Type, Len, Ops uint8) { return frm.buf[1], frm.buf[2], frm.buf[3] } -func (frm FrameV4) SetHardware(Type, Len, Ops uint8) { +func (frm Frame) SetHardware(Type, Len, Ops uint8) { frm.buf[1], frm.buf[2], frm.buf[3] = Type, Len, Ops } -func (frm FrameV4) XID() uint32 { - return binary.BigEndian.Uint32(frm.buf[4:8]) -} +func (frm Frame) XID() uint32 { return binary.BigEndian.Uint32(frm.buf[4:8]) } +func (frm Frame) SetXID(xid uint32) { binary.BigEndian.PutUint32(frm.buf[4:8], xid) } -func (frm FrameV4) Secs() uint16 { - return binary.BigEndian.Uint16(frm.buf[8:10]) -} +func (frm Frame) Secs() uint16 { return binary.BigEndian.Uint16(frm.buf[8:10]) } +func (frm Frame) SetSecs(secs uint16) { binary.BigEndian.PutUint16(frm.buf[8:10], secs) } -func (frm FrameV4) Flags() uint16 { - return binary.BigEndian.Uint16(frm.buf[10:12]) -} +func (frm Frame) Flags() Flags { return Flags(binary.BigEndian.Uint16(frm.buf[10:12])) } +func (frm Frame) SetFlags(flags Flags) { binary.BigEndian.PutUint16(frm.buf[10:12], uint16(flags)) } // CIAddr is the client IP address. If the client has not obtained an IP // address yet, this field is set to 0. -func (frm FrameV4) CIAddr() *[4]byte { +func (frm Frame) CIAddr() *[4]byte { return (*[4]byte)(frm.buf[12:16]) } // YIAddr is the IP address offered by the server to the client. -func (frm FrameV4) YIAddr() *[4]byte { +func (frm Frame) YIAddr() *[4]byte { return (*[4]byte)(frm.buf[16:20]) } // SIAddr is the IP address of the next server to use in bootstrap. This // field is used in DHCPOFFER and DHCPACK messages. -func (frm FrameV4) SIAddr() *[4]byte { +func (frm Frame) SIAddr() *[4]byte { return (*[4]byte)(frm.buf[20:24]) } // GIAddr is the gateway IP address. -func (frm FrameV4) GIAddr() *[4]byte { +func (frm Frame) GIAddr() *[4]byte { return (*[4]byte)(frm.buf[24:28]) } -// CHAddrAs6 returns [FrameV4.CHAddr] but limited to first 6 bytes. -func (frm FrameV4) CHAddrAs6() *[6]byte { +// CHAddrAs6 returns [Frame.CHAddr] but limited to first 6 bytes. +func (frm Frame) CHAddrAs6() *[6]byte { return (*[6]byte)(frm.buf[28 : 28+6]) } // CHAddr is the client hardware address. Can be up to 16 bytes in length but // is usually 6 bytes for Ethernet. -func (frm FrameV4) CHAddr() *[16]byte { +func (frm Frame) CHAddr() *[16]byte { return (*[16]byte)(frm.buf[28:44]) } -func (frm FrameV4) MagicCookie() uint32 { - return binary.BigEndian.Uint32(frm.buf[magicCookieOffset:]) +func (frm Frame) MagicCookie() uint32 { return binary.BigEndian.Uint32(frm.buf[magicCookieOffset:]) } +func (frm Frame) SetMagicCookie(cookie uint32) { + binary.BigEndian.PutUint32(frm.buf[magicCookieOffset:], cookie) } // ClearHeader zeros out the header contents. -func (frm FrameV4) ClearHeader() { - for i := range frm.buf[:sizeHeader] { +func (frm Frame) ClearHeader() { + for i := range frm.buf[:optionsOffset] { frm.buf[i] = 0 } } -func (frm FrameV4) ForEachOption(fn func(opt Option) error) error { +func (frm Frame) ForEachOption(fn func(op OptNum, data []byte) error) error { if fn == nil { return errors.New("nil function to parse DHCP") } @@ -126,7 +131,7 @@ func (frm FrameV4) ForEachOption(fn func(opt Option) error) error { } optlen := frm.buf[ptr+1] optionData := frm.buf[ptr+2 : ptr+2+int(optlen)] - if err := fn(Option{optnum, optionData}); err != nil { + if err := fn(optnum, optionData); err != nil { return err } ptr += int(optlen) + 2 diff --git a/lneto/dhcp/stringers.go b/lneto/dhcpv4/stringers.go similarity index 99% rename from lneto/dhcp/stringers.go rename to lneto/dhcpv4/stringers.go index 77e0e9c..3041b3f 100644 --- a/lneto/dhcp/stringers.go +++ b/lneto/dhcpv4/stringers.go @@ -1,6 +1,6 @@ // Code generated by "stringer -type=OptNum,Op,MessageType,ClientState -linecomment -output stringers.go"; DO NOT EDIT. -package dhcp +package dhcpv4 import "strconv" @@ -111,7 +111,7 @@ func _() { _ = x[MsgRequest-3] _ = x[MsgDecline-4] _ = x[MsgAck-5] - _ = x[MsgNak-6] + _ = x[MsgNack-6] _ = x[MsgRelease-7] _ = x[MsgInform-8] } diff --git a/lneto/frames.go b/lneto/frames.go index 0482a25..76d8e30 100644 --- a/lneto/frames.go +++ b/lneto/frames.go @@ -8,6 +8,7 @@ import ( ) // NewEthFrame returns a EthFrame with data set to buf. +// An error is returned if the buffer size is smaller than 14. // Users should still call [EthFrame.ValidateSize] before working // with payload/options of frames to avoid panics. func NewEthFrame(buf []byte) (EthFrame, error) { @@ -18,6 +19,7 @@ func NewEthFrame(buf []byte) (EthFrame, error) { } // NewARPFrame returns a ARPFrame with data set to buf. +// An error is returned if the buffer size is smaller than 28 (IPv4 min size). // Users should still call [ARPFrame.ValidateSize] before working // with payload/options of frames to avoid panics. func NewARPFrame(buf []byte) (ARPFrame, error) { @@ -28,6 +30,7 @@ func NewARPFrame(buf []byte) (ARPFrame, error) { } // NewIPv4Frame returns a new IPv4Frame with data set to buf. +// An error is returned if the buffer size is smaller than 20. // Users should still call [IPv4Frame.ValidateSize] before working // with payload/options of frames to avoid panics. func NewIPv4Frame(buf []byte) (IPv4Frame, error) { @@ -38,6 +41,7 @@ func NewIPv4Frame(buf []byte) (IPv4Frame, error) { } // NewIPv6Frame returns a new IPv6Frame with data set to buf. +// An error is returned if the buffer size is smaller than 40. // Users should still call [IPv6Frame.ValidateSize] before working // with payload/options of frames to avoid panics. func NewIPv6Frame(buf []byte) (IPv6Frame, error) { @@ -48,6 +52,7 @@ func NewIPv6Frame(buf []byte) (IPv6Frame, error) { } // NewTCPFrame returns a new TCPFrame with data set to buf. +// An error is returned if the buffer size is smaller than 20. // Users should still call [TCPFrame.ValidateSize] before working // with payload/options of frames to avoid panics. func NewTCPFrame(buf []byte) (TCPFrame, error) { @@ -58,6 +63,7 @@ func NewTCPFrame(buf []byte) (TCPFrame, error) { } // NewUDPFrame returns a new UDPFrame with data set to buf. +// An error is returned if the buffer size is smaller than 8. // Users should still call [UDPFrame.ValidateSize] before working // with payload/options of frames to avoid panics. func NewUDPFrame(buf []byte) (UDPFrame, error) { diff --git a/lneto/ntp/client.go b/lneto/ntp/client.go index a35f036..593146a 100644 --- a/lneto/ntp/client.go +++ b/lneto/ntp/client.go @@ -32,12 +32,13 @@ type Client struct { t [4]Timestamp // org Timestamp // rec Timestamp - xmt Timestamp - state state - _sysprec int8 + xmt Timestamp + state state + serverStratum Stratum + _sysprec int8 } -func (c *Client) Write(payload []byte) (int, error) { +func (c *Client) Send(payload []byte) (int, error) { if c.isDone() { return 0, io.EOF } @@ -71,7 +72,7 @@ func (c *Client) Write(payload []byte) (int, error) { return SizeHeader, nil } -func (c *Client) read(payload []byte) error { +func (c *Client) Read(payload []byte) error { if c.isDone() { return io.EOF } @@ -91,6 +92,7 @@ func (c *Client) read(payload []byte) error { t[1] = frm.ReceiveTime() t[2] = tstx t[3] = c.unsyncTimestamp(c.now()) + c.serverStratum = frm.Stratum() c.state = stateDone case stateAwait2: c.state = stateAwait2 @@ -125,6 +127,9 @@ func (c *Client) Now() time.Time { return c.now().Add(c.Offset()) } +// ServerStratum returns the stratum of the server client synchronized with. +func (c *Client) ServerStratum() Stratum { return c.serverStratum } + // Offset returns the func (c *Client) Offset() time.Duration { if c.isDone() { diff --git a/lneto/ntp/definitions.go b/lneto/ntp/definitions.go index 7e1b6bd..1c216a4 100644 --- a/lneto/ntp/definitions.go +++ b/lneto/ntp/definitions.go @@ -1,5 +1,8 @@ package ntp +// LeapIndicator represents the leap second indicator. +// It indicates whether there is no warning, an extra second (61 seconds in the last minute), +// or a missing second (59 seconds in the last minute). type LeapIndicator uint8 const ( @@ -8,20 +11,42 @@ const ( LeapLastMinute59 // last minute 59 ) +// Stratum represents the stratum level of the NTP server. +type Stratum uint8 + const ( // If the Stratum field is 0, which implies unspecified or invalid, the // Reference Identifier field can be used to convey messages useful for // status reporting and access control. These are called Kiss-o'-Death // (KoD) packets and the ASCII messages they convey are called kiss codes. - StratumUnspecified = 0 - StratumPrimary = 1 - StratumUnsync = 16 + StratumUnspecified Stratum = 0 // unspecified + StratumPrimary Stratum = 1 // primary + StratumUnsync Stratum = 16 // unsynchronized ) -func IsStratumSecondary(stratum uint8) bool { - return stratum > 1 && stratum < 16 +// String returns a human readable representation of the Stratum. +func (s Stratum) String() string { + switch s { + case 0: + return "unspecified" + case 1: + return "primary" + case 16: + return "unsynchronized" + } + if s < 16 { + return "secondary" + } + return "invalid" } +func (s Stratum) IsSecondary() bool { + return s > 1 && s < 16 +} + +// Mode represents the mode of the NTP message. +// It can be undefined, symmetric active, symmetric passive, client, server, broadcast, +// NTP control message, or private use. type Mode uint8 const ( diff --git a/lneto/ntp/ntp.go b/lneto/ntp/ntp.go index a3babbb..232af14 100644 --- a/lneto/ntp/ntp.go +++ b/lneto/ntp/ntp.go @@ -53,8 +53,8 @@ func (frm Frame) SetFlags(mode Mode, version uint8, lp LeapIndicator) { frm.buf[0] = b } -func (frm Frame) Stratum() uint8 { return frm.buf[1] } -func (frm Frame) SetStratum(stratum uint8) { frm.buf[1] = stratum } +func (frm Frame) Stratum() Stratum { return Stratum(frm.buf[1]) } +func (frm Frame) SetStratum(stratum Stratum) { frm.buf[1] = byte(stratum) } // Poll is 8-bit signed integer representing the maximum interval between // successive messages, in log2 seconds. Suggested default limits for