
The dhcp server is systemd-networkd, and the dhcp plugin can request an ip but can not renew it. The systemd-networkd just ignore the renew request. ``` 2024/09/14 21:46:00 no DHCP packet received within 10s 2024/09/14 21:46:00 retrying in 31.529038 seconds 2024/09/14 21:46:42 no DHCP packet received within 10s 2024/09/14 21:46:42 retrying in 63.150490 seconds 2024/09/14 21:47:45 98184616c91f15419f5cacd012697f85afaa2daeb5d3233e28b0ec21589fb45a/iot/eth1: no more tries 2024/09/14 21:47:45 98184616c91f15419f5cacd012697f85afaa2daeb5d3233e28b0ec21589fb45a/iot/eth1: renewal time expired, rebinding 2024/09/14 21:47:45 Link "eth1" down. Attempting to set up 2024/09/14 21:47:45 98184616c91f15419f5cacd012697f85afaa2daeb5d3233e28b0ec21589fb45a/iot/eth1: lease rebound, expiration is 2024-09-14 22:47:45.309270751 +0800 CST m=+11730.048516519 ``` Follow the https://datatracker.ietf.org/doc/html/rfc2131#section-4.3.6, following options must not be sent in renew - Requested IP Address - Server Identifier Since the upstream code has been inactive for 6 years, we should switch to another dhcpv4 library. The new selected one is https://github.com/insomniacslk/dhcp. Signed-off-by: Songmin Li <lisongmin@protonmail.com>
381 lines
9.3 KiB
Go
381 lines
9.3 KiB
Go
package dhcpv4
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/insomniacslk/dhcp/iana"
|
|
"github.com/insomniacslk/dhcp/rfc1035label"
|
|
"github.com/u-root/uio/uio"
|
|
)
|
|
|
|
var (
|
|
// ErrShortByteStream is an error that is thrown any time a short byte stream is
|
|
// detected during option parsing.
|
|
ErrShortByteStream = errors.New("short byte stream")
|
|
|
|
// ErrZeroLengthByteStream is an error that is thrown any time a zero-length
|
|
// byte stream is encountered.
|
|
ErrZeroLengthByteStream = errors.New("zero-length byte stream")
|
|
)
|
|
|
|
// OptionValue is an interface that all DHCP v4 options adhere to.
|
|
type OptionValue interface {
|
|
ToBytes() []byte
|
|
String() string
|
|
}
|
|
|
|
// Option is a DHCPv4 option and consists of a 1-byte option code and a value
|
|
// stream of bytes.
|
|
//
|
|
// The value is to be interpreted based on the option code.
|
|
type Option struct {
|
|
Code OptionCode
|
|
Value OptionValue
|
|
}
|
|
|
|
// String returns a human-readable version of this option.
|
|
func (o Option) String() string {
|
|
v := o.Value.String()
|
|
if strings.Contains(v, "\n") {
|
|
return fmt.Sprintf("%s:\n%s", o.Code, v)
|
|
}
|
|
return fmt.Sprintf("%s: %s", o.Code, v)
|
|
}
|
|
|
|
// Options is a collection of options.
|
|
type Options map[uint8][]byte
|
|
|
|
// OptionsFromList adds all given options to an options map.
|
|
func OptionsFromList(o ...Option) Options {
|
|
opts := make(Options)
|
|
for _, opt := range o {
|
|
opts.Update(opt)
|
|
}
|
|
return opts
|
|
}
|
|
|
|
// Get will attempt to get all options that match a DHCPv4 option
|
|
// from its OptionCode. If the option was not found it will return an
|
|
// empty list.
|
|
//
|
|
// According to RFC 3396, options that are specified more than once are
|
|
// concatenated, and hence this should always just return one option. This
|
|
// currently returns a list to be API compatible.
|
|
func (o Options) Get(code OptionCode) []byte {
|
|
return o[code.Code()]
|
|
}
|
|
|
|
// Has checks whether o has the given opcode.
|
|
func (o Options) Has(opcode OptionCode) bool {
|
|
_, ok := o[opcode.Code()]
|
|
return ok
|
|
}
|
|
|
|
// Del deletes the option matching the option code.
|
|
func (o Options) Del(opcode OptionCode) {
|
|
delete(o, opcode.Code())
|
|
}
|
|
|
|
// Update updates the existing options with the passed option, adding it
|
|
// at the end if not present already
|
|
func (o Options) Update(option Option) {
|
|
o[option.Code.Code()] = option.Value.ToBytes()
|
|
}
|
|
|
|
// ToBytes makes Options usable as an OptionValue as well.
|
|
//
|
|
// Used in the case of vendor-specific and relay agent options.
|
|
func (o Options) ToBytes() []byte {
|
|
return uio.ToBigEndian(o)
|
|
}
|
|
|
|
// FromBytes parses a sequence of bytes until the end and builds a list of
|
|
// options from it.
|
|
//
|
|
// The sequence should not contain the DHCP magic cookie.
|
|
//
|
|
// Returns an error if any invalid option or length is found.
|
|
func (o Options) FromBytes(data []byte) error {
|
|
return o.fromBytesCheckEnd(data, false)
|
|
}
|
|
|
|
const (
|
|
optPad = 0
|
|
optAgentInfo = 82
|
|
optEnd = 255
|
|
)
|
|
|
|
// FromBytesCheckEnd parses Options from byte sequences using the
|
|
// parsing function that is passed in as a paremeter
|
|
func (o Options) fromBytesCheckEnd(data []byte, checkEndOption bool) error {
|
|
if len(data) == 0 {
|
|
return nil
|
|
}
|
|
buf := uio.NewBigEndianBuffer(data)
|
|
|
|
var end bool
|
|
for buf.Len() >= 1 {
|
|
// 1 byte: option code
|
|
// 1 byte: option length n
|
|
// n bytes: data
|
|
code := buf.Read8()
|
|
|
|
if code == optPad {
|
|
continue
|
|
} else if code == optEnd {
|
|
end = true
|
|
break
|
|
}
|
|
length := int(buf.Read8())
|
|
|
|
// N bytes: option data
|
|
data := buf.Consume(length)
|
|
if data == nil {
|
|
return fmt.Errorf("error collecting options: %v", buf.Error())
|
|
}
|
|
data = data[:length:length]
|
|
|
|
// RFC 2131, Section 4.1 "Options may appear only once, [...].
|
|
// The client concatenates the values of multiple instances of
|
|
// the same option into a single parameter list for
|
|
// configuration."
|
|
//
|
|
// See also RFC 3396 for concatenation order and options longer
|
|
// than 255 bytes.
|
|
o[code] = append(o[code], data...)
|
|
}
|
|
|
|
// If we never read the End option, the sender of this packet screwed
|
|
// up.
|
|
if !end && checkEndOption {
|
|
return io.ErrUnexpectedEOF
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// sortedKeys returns an ordered slice of option keys from the Options map, for
|
|
// use in serializing options to binary.
|
|
func (o Options) sortedKeys() []int {
|
|
// Send all values for a given key
|
|
var codes []int
|
|
var hasOptAgentInfo, hasOptEnd bool
|
|
for k := range o {
|
|
// RFC 3046 section 2.1 states that option 82 SHALL come last (ignoring End).
|
|
if k == optAgentInfo {
|
|
hasOptAgentInfo = true
|
|
continue
|
|
}
|
|
if k == optEnd {
|
|
hasOptEnd = true
|
|
continue
|
|
}
|
|
codes = append(codes, int(k))
|
|
}
|
|
|
|
sort.Ints(codes)
|
|
|
|
if hasOptAgentInfo {
|
|
codes = append(codes, optAgentInfo)
|
|
}
|
|
if hasOptEnd {
|
|
codes = append(codes, optEnd)
|
|
}
|
|
return codes
|
|
}
|
|
|
|
// Marshal writes options binary representations to b.
|
|
func (o Options) Marshal(b *uio.Lexer) {
|
|
for _, c := range o.sortedKeys() {
|
|
code := uint8(c)
|
|
// Even if the End option is in there, don't marshal it until
|
|
// the end.
|
|
// Don't write padding either, since the options are sorted
|
|
// it would always be written first which isn't useful
|
|
if code == optEnd || code == optPad {
|
|
continue
|
|
}
|
|
|
|
data := o[code]
|
|
|
|
// Ensure even 0-length options are written out
|
|
if len(data) == 0 {
|
|
b.Write8(code)
|
|
b.Write8(0)
|
|
continue
|
|
}
|
|
// RFC 3396: If more than 256 bytes of data are given, the
|
|
// option is simply listed multiple times.
|
|
for len(data) > 0 {
|
|
// 1 byte: option code
|
|
b.Write8(code)
|
|
|
|
n := len(data)
|
|
if n > math.MaxUint8 {
|
|
n = math.MaxUint8
|
|
}
|
|
|
|
// 1 byte: option length
|
|
b.Write8(uint8(n))
|
|
|
|
// N bytes: option data
|
|
b.WriteBytes(data[:n])
|
|
data = data[n:]
|
|
}
|
|
}
|
|
}
|
|
|
|
// String prints options using DHCP-specified option codes.
|
|
func (o Options) String() string {
|
|
return o.ToString(dhcpHumanizer)
|
|
}
|
|
|
|
// Summary prints options in human-readable values.
|
|
//
|
|
// Summary uses vendorParser to interpret the OptionVendorSpecificInformation option.
|
|
func (o Options) Summary(vendorDecoder OptionDecoder) string {
|
|
return o.ToString(OptionHumanizer{
|
|
ValueHumanizer: parserFor(vendorDecoder),
|
|
CodeHumanizer: func(c uint8) OptionCode {
|
|
return optionCode(c)
|
|
},
|
|
})
|
|
}
|
|
|
|
// OptionParser gives a human-legible interpretation of data for the given option code.
|
|
type OptionParser func(code OptionCode, data []byte) fmt.Stringer
|
|
|
|
// OptionHumanizer is used to interpret a set of Options for their option code
|
|
// name and values.
|
|
//
|
|
// There should be separate OptionHumanizers for each Option "space": DHCP,
|
|
// BSDP, Relay Agent Info, and others.
|
|
type OptionHumanizer struct {
|
|
ValueHumanizer OptionParser
|
|
CodeHumanizer func(code uint8) OptionCode
|
|
}
|
|
|
|
// Stringify returns a human-readable interpretation of the option code and its
|
|
// associated data.
|
|
func (oh OptionHumanizer) Stringify(code uint8, data []byte) string {
|
|
c := oh.CodeHumanizer(code)
|
|
val := oh.ValueHumanizer(c, data)
|
|
return fmt.Sprintf("%s: %s", c, val)
|
|
}
|
|
|
|
// dhcpHumanizer humanizes the set of DHCP option codes.
|
|
var dhcpHumanizer = OptionHumanizer{
|
|
ValueHumanizer: parseOption,
|
|
CodeHumanizer: func(c uint8) OptionCode {
|
|
return optionCode(c)
|
|
},
|
|
}
|
|
|
|
// ToString uses parse to parse options into human-readable values.
|
|
func (o Options) ToString(humanizer OptionHumanizer) string {
|
|
var ret string
|
|
for _, c := range o.sortedKeys() {
|
|
code := uint8(c)
|
|
v := o[code]
|
|
optString := humanizer.Stringify(code, v)
|
|
// If this option has sub structures, offset them accordingly.
|
|
if strings.Contains(optString, "\n") {
|
|
optString = strings.Replace(optString, "\n ", "\n ", -1)
|
|
}
|
|
ret += fmt.Sprintf(" %v\n", optString)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func parseOption(code OptionCode, data []byte) fmt.Stringer {
|
|
return parserFor(nil)(code, data)
|
|
}
|
|
|
|
func parserFor(vendorParser OptionDecoder) OptionParser {
|
|
return func(code OptionCode, data []byte) fmt.Stringer {
|
|
return getOption(code, data, vendorParser)
|
|
}
|
|
}
|
|
|
|
// OptionDecoder can decode a byte stream into a human-readable option.
|
|
type OptionDecoder interface {
|
|
fmt.Stringer
|
|
FromBytes([]byte) error
|
|
}
|
|
|
|
func getOption(code OptionCode, data []byte, vendorDecoder OptionDecoder) fmt.Stringer {
|
|
var d OptionDecoder
|
|
switch code {
|
|
case OptionRouter, OptionDomainNameServer, OptionNTPServers, OptionServerIdentifier:
|
|
d = &IPs{}
|
|
|
|
case OptionBroadcastAddress, OptionRequestedIPAddress:
|
|
d = &IP{}
|
|
|
|
case OptionClientSystemArchitectureType:
|
|
d = &iana.Archs{}
|
|
|
|
case OptionSubnetMask:
|
|
d = &IPMask{}
|
|
|
|
case OptionDHCPMessageType:
|
|
var mt MessageType
|
|
d = &mt
|
|
|
|
case OptionParameterRequestList:
|
|
d = &OptionCodeList{}
|
|
|
|
case OptionHostName, OptionDomainName, OptionRootPath,
|
|
OptionClassIdentifier, OptionTFTPServerName, OptionBootfileName,
|
|
OptionMessage, OptionReferenceToTZDatabase:
|
|
var s String
|
|
d = &s
|
|
|
|
case OptionRelayAgentInformation:
|
|
d = &RelayOptions{}
|
|
|
|
case OptionDNSDomainSearchList:
|
|
d = &rfc1035label.Labels{}
|
|
|
|
case OptionIPAddressLeaseTime, OptionRenewTimeValue,
|
|
OptionRebindingTimeValue, OptionIPv6OnlyPreferred, OptionArpCacheTimeout,
|
|
OptionTimeOffset:
|
|
var dur Duration
|
|
d = &dur
|
|
|
|
case OptionMaximumDHCPMessageSize:
|
|
var u Uint16
|
|
d = &u
|
|
|
|
case OptionUserClassInformation:
|
|
var s Strings
|
|
d = &s
|
|
if s.FromBytes(data) != nil {
|
|
var s String
|
|
d = &s
|
|
}
|
|
|
|
case OptionAutoConfigure:
|
|
var a AutoConfiguration
|
|
d = &a
|
|
|
|
case OptionVendorIdentifyingVendorClass:
|
|
d = &VIVCIdentifiers{}
|
|
|
|
case OptionVendorSpecificInformation:
|
|
d = vendorDecoder
|
|
|
|
case OptionClasslessStaticRoute:
|
|
d = &Routes{}
|
|
}
|
|
if d != nil && d.FromBytes(data) == nil {
|
|
return d
|
|
}
|
|
return OptionGeneric{data}
|
|
}
|