Songmin Li d61e7e5e1f fix(dhcp): can not renew an ip address
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>
2024-10-14 17:42:30 +02:00

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}
}