all: sync with master

This commit is contained in:
Ainar Garipov
2022-09-07 18:03:18 +03:00
parent d23da1b757
commit faf2b32389
108 changed files with 2925 additions and 2390 deletions

View File

@@ -83,7 +83,7 @@ func (s *v4Server) newDHCPConn(iface *net.Interface) (c net.PacketConn, err erro
// wrapErrs is a helper to wrap the errors from two independent underlying
// connections.
func (c *dhcpConn) wrapErrs(action string, udpConnErr, rawConnErr error) (err error) {
func (*dhcpConn) wrapErrs(action string, udpConnErr, rawConnErr error) (err error) {
switch {
case udpConnErr != nil && rawConnErr != nil:
return errors.List(fmt.Sprintf("%s both connections", action), udpConnErr, rawConnErr)
@@ -129,7 +129,7 @@ func (c *dhcpConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
// connection.
return c.udpConn.WriteTo(p, addr)
default:
return 0, fmt.Errorf("peer is of unexpected type %T", addr)
return 0, fmt.Errorf("addr has an unexpected type %T", addr)
}
}
@@ -188,32 +188,20 @@ func (c *dhcpConn) SetWriteDeadline(t time.Time) error {
)
}
// ipv4DefaultTTL is the default Time to Live value as recommended by
// RFC-1700 (https://datatracker.ietf.org/doc/html/rfc1700) in seconds.
// ipv4DefaultTTL is the default Time to Live value in seconds as recommended by
// RFC-1700.
//
// See https://datatracker.ietf.org/doc/html/rfc1700.
const ipv4DefaultTTL = 64
// errInvalidPktDHCP is returned when the provided payload is not a valid DHCP
// packet.
const errInvalidPktDHCP errors.Error = "packet is not a valid dhcp packet"
// buildEtherPkt wraps the payload with IPv4, UDP and Ethernet frames. The
// payload is expected to be an encoded DHCP packet.
// buildEtherPkt wraps the payload with IPv4, UDP and Ethernet frames.
// Validation of the payload is a caller's responsibility.
func (c *dhcpConn) buildEtherPkt(payload []byte, peer *dhcpUnicastAddr) (pkt []byte, err error) {
dhcpLayer := gopacket.NewPacket(payload, layers.LayerTypeDHCPv4, gopacket.DecodeOptions{
NoCopy: true,
}).Layer(layers.LayerTypeDHCPv4)
// Check if the decoding succeeded and the resulting layer doesn't
// contain any errors. It should guarantee panic-safe converting of the
// layer into gopacket.SerializableLayer.
if dhcpLayer == nil || dhcpLayer.LayerType() != layers.LayerTypeDHCPv4 {
return nil, errInvalidPktDHCP
}
udpLayer := &layers.UDP{
SrcPort: dhcpv4.ServerPort,
DstPort: dhcpv4.ClientPort,
}
ipv4Layer := &layers.IPv4{
Version: uint8(layers.IPProtocolIPv4),
Flags: layers.IPv4DontFragment,
@@ -226,6 +214,7 @@ func (c *dhcpConn) buildEtherPkt(payload []byte, peer *dhcpUnicastAddr) (pkt []b
// Ignore the error since it's only returned for invalid network layer's
// type.
_ = udpLayer.SetNetworkLayerForChecksum(ipv4Layer)
ethLayer := &layers.Ethernet{
SrcMAC: c.srcMAC,
DstMAC: peer.HardwareAddr,
@@ -233,10 +222,19 @@ func (c *dhcpConn) buildEtherPkt(payload []byte, peer *dhcpUnicastAddr) (pkt []b
}
buf := gopacket.NewSerializeBuffer()
err = gopacket.SerializeLayers(buf, gopacket.SerializeOptions{
setts := gopacket.SerializeOptions{
FixLengths: true,
ComputeChecksums: true,
}, ethLayer, ipv4Layer, udpLayer, dhcpLayer.(gopacket.SerializableLayer))
}
err = gopacket.SerializeLayers(
buf,
setts,
ethLayer,
ipv4Layer,
udpLayer,
gopacket.Payload(payload),
)
if err != nil {
return nil, fmt.Errorf("serializing layers: %w", err)
}

View File

@@ -47,7 +47,7 @@ func TestDHCPConn_WriteTo_common(t *testing.T) {
n, err := conn.WriteTo(nil, &unexpectedAddrType{})
require.Error(t, err)
testutil.AssertErrorMsg(t, "peer is of unexpected type *dhcpd.unexpectedAddrType", err)
testutil.AssertErrorMsg(t, "addr has an unexpected type *dhcpd.unexpectedAddrType", err)
assert.Zero(t, n)
})
}
@@ -91,14 +91,13 @@ func TestBuildEtherPkt(t *testing.T) {
}
})
t.Run("non-serializable", func(t *testing.T) {
t.Run("bad_payload", func(t *testing.T) {
// Create an invalid DHCP packet.
invalidPayload := []byte{1, 2, 3, 4}
pkt, err := conn.buildEtherPkt(invalidPayload, nil)
require.Error(t, err)
pkt, err := conn.buildEtherPkt(invalidPayload, peer)
require.NoError(t, err)
assert.ErrorIs(t, err, errInvalidPktDHCP)
assert.Empty(t, pkt)
assert.NotEmpty(t, pkt)
})
t.Run("serializing_error", func(t *testing.T) {

View File

@@ -9,26 +9,31 @@ import (
"net"
"strconv"
"strings"
"time"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/timeutil"
"github.com/insomniacslk/dhcp/dhcpv4"
)
// The aliases for DHCP option types available for explicit declaration.
//
// TODO(e.burkov): Add an option for classless routes.
const (
hexTyp = "hex"
ipTyp = "ip"
ipsTyp = "ips"
textTyp = "text"
typDel = "del"
typBool = "bool"
typDur = "dur"
typHex = "hex"
typIP = "ip"
typIPs = "ips"
typText = "text"
typU8 = "u8"
typU16 = "u16"
)
// parseDHCPOptionHex parses a DHCP option as a hex-encoded string. For
// example:
//
// 252 hex 736f636b733a2f2f70726f78792e6578616d706c652e6f7267
//
// parseDHCPOptionHex parses a DHCP option as a hex-encoded string.
func parseDHCPOptionHex(s string) (val dhcpv4.OptionValue, err error) {
var data []byte
data, err = hex.DecodeString(s)
@@ -39,15 +44,12 @@ func parseDHCPOptionHex(s string) (val dhcpv4.OptionValue, err error) {
return dhcpv4.OptionGeneric{Data: data}, nil
}
// parseDHCPOptionIP parses a DHCP option as a single IP address. For example:
//
// 6 ip 192.168.1.1
//
// parseDHCPOptionIP parses a DHCP option as a single IP address.
func parseDHCPOptionIP(s string) (val dhcpv4.OptionValue, err error) {
var ip net.IP
// All DHCPv4 options require IPv4, so don't put the 16-byte version.
// Otherwise, the clients will receive weird data that looks like four
// IPv4 addresses.
// Otherwise, the clients will receive weird data that looks like four IPv4
// addresses.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/2688.
if ip, err = netutil.ParseIPv4(s); err != nil {
@@ -58,133 +60,348 @@ func parseDHCPOptionIP(s string) (val dhcpv4.OptionValue, err error) {
}
// parseDHCPOptionIPs parses a DHCP option as a comma-separates list of IP
// addresses. For example:
//
// 6 ips 192.168.1.1,192.168.1.2
//
// addresses.
func parseDHCPOptionIPs(s string) (val dhcpv4.OptionValue, err error) {
var ips dhcpv4.IPs
var ip net.IP
var ip dhcpv4.OptionValue
for i, ipStr := range strings.Split(s, ",") {
// See notes in the ipDHCPOptionParserHandler.
if ip, err = netutil.ParseIPv4(ipStr); err != nil {
ip, err = parseDHCPOptionIP(ipStr)
if err != nil {
return nil, fmt.Errorf("parsing ip at index %d: %w", i, err)
}
ips = append(ips, ip)
ips = append(ips, net.IP(ip.(dhcpv4.IP)))
}
return ips, nil
}
// parseDHCPOptionText parses a DHCP option as a simple UTF-8 encoded
// text. For example:
//
// 252 text http://192.168.1.1/wpad.dat
//
func parseDHCPOptionText(s string) (val dhcpv4.OptionValue) {
return dhcpv4.OptionGeneric{Data: []byte(s)}
// parseDHCPOptionDur parses a DHCP option as a duration in a human-readable
// form.
func parseDHCPOptionDur(s string) (val dhcpv4.OptionValue, err error) {
var v timeutil.Duration
err = v.UnmarshalText([]byte(s))
if err != nil {
return nil, fmt.Errorf("decoding dur: %w", err)
}
return dhcpv4.Duration(v.Duration), nil
}
// parseDHCPOption parses an option. See the documentation of parseDHCPOption*
// for more info.
func parseDHCPOption(s string) (opt dhcpv4.Option, err error) {
// parseDHCPOptionUint parses a DHCP option as an unsigned integer. bitSize is
// expected to be 8 or 16.
func parseDHCPOptionUint(s string, bitSize int) (val dhcpv4.OptionValue, err error) {
var v uint64
v, err = strconv.ParseUint(s, 10, bitSize)
if err != nil {
return nil, fmt.Errorf("decoding u%d: %w", bitSize, err)
}
switch bitSize {
case 8:
return dhcpv4.OptionGeneric{Data: []byte{uint8(v)}}, nil
case 16:
return dhcpv4.Uint16(v), nil
default:
return nil, fmt.Errorf("unsupported size of integer %d", bitSize)
}
}
// parseDHCPOptionBool parses a DHCP option as a boolean value. See
// [strconv.ParseBool] for available values.
func parseDHCPOptionBool(s string) (val dhcpv4.OptionValue, err error) {
var v bool
v, err = strconv.ParseBool(s)
if err != nil {
return nil, fmt.Errorf("decoding bool: %w", err)
}
rawVal := [1]byte{}
if v {
rawVal[0] = 1
}
return dhcpv4.OptionGeneric{Data: rawVal[:]}, nil
}
// parseDHCPOptionVal parses a DHCP option value considering typ.
func parseDHCPOptionVal(typ, valStr string) (val dhcpv4.OptionValue, err error) {
switch typ {
case typBool:
val, err = parseDHCPOptionBool(valStr)
case typDel:
val = dhcpv4.OptionGeneric{Data: nil}
case typDur:
val, err = parseDHCPOptionDur(valStr)
case typHex:
val, err = parseDHCPOptionHex(valStr)
case typIP:
val, err = parseDHCPOptionIP(valStr)
case typIPs:
val, err = parseDHCPOptionIPs(valStr)
case typText:
val = dhcpv4.String(valStr)
case typU8:
val, err = parseDHCPOptionUint(valStr, 8)
case typU16:
val, err = parseDHCPOptionUint(valStr, 16)
default:
err = fmt.Errorf("unknown option type %q", typ)
}
return val, err
}
// parseDHCPOption parses an option. For the del option value is ignored. The
// examples of possible option strings:
//
// - 1 bool true
// - 2 del
// - 3 dur 2h5s
// - 4 hex 736f636b733a2f2f70726f78792e6578616d706c652e6f7267
// - 5 ip 192.168.1.1
// - 6 ips 192.168.1.1,192.168.1.2
// - 7 text http://192.168.1.1/wpad.dat
// - 8 u8 255
// - 9 u16 65535
func parseDHCPOption(s string) (code dhcpv4.OptionCode, val dhcpv4.OptionValue, err error) {
defer func() { err = errors.Annotate(err, "invalid option string %q: %w", s) }()
s = strings.TrimSpace(s)
parts := strings.SplitN(s, " ", 3)
if len(parts) < 3 {
return opt, errors.Error("need at least three fields")
var valStr string
if pl := len(parts); pl < 3 {
if pl < 2 || parts[1] != typDel {
return nil, nil, errors.Error("bad option format")
}
} else {
valStr = parts[2]
}
var code64 uint64
code64, err = strconv.ParseUint(parts[0], 10, 8)
if err != nil {
return opt, fmt.Errorf("parsing option code: %w", err)
}
var optVal dhcpv4.OptionValue
switch typ, val := parts[1], parts[2]; typ {
case hexTyp:
optVal, err = parseDHCPOptionHex(val)
case ipTyp:
optVal, err = parseDHCPOptionIP(val)
case ipsTyp:
optVal, err = parseDHCPOptionIPs(val)
case textTyp:
optVal = parseDHCPOptionText(val)
default:
return opt, fmt.Errorf("unknown option type %q", typ)
return nil, nil, fmt.Errorf("parsing option code: %w", err)
}
val, err = parseDHCPOptionVal(parts[1], valStr)
if err != nil {
return opt, err
// Don't wrap an error since it's informative enough as is and there
// also the deferred annotation.
return nil, nil, err
}
return dhcpv4.Option{
Code: dhcpv4.GenericOptionCode(code64),
Value: optVal,
}, nil
return dhcpv4.GenericOptionCode(code64), val, nil
}
// prepareOptions builds the set of DHCP options according to host requirements
// document and values from conf.
func prepareOptions(conf V4ServerConf) (opts dhcpv4.Options) {
// Set default values for host configuration parameters listed in Appendix
// A of RFC-2131. Those parameters, if requested by client, should be
// returned with values defined by Host Requirements Document.
//
// See https://datatracker.ietf.org/doc/html/rfc2131#appendix-A.
//
// See also https://datatracker.ietf.org/doc/html/rfc1122,
// https://datatracker.ietf.org/doc/html/rfc1123, and
// https://datatracker.ietf.org/doc/html/rfc2132.
opts = dhcpv4.Options{
func prepareOptions(conf V4ServerConf) (implicit, explicit dhcpv4.Options) {
// Set default values of host configuration parameters listed in Appendix A
// of RFC-2131.
implicit = dhcpv4.OptionsFromList(
// IP-Layer Per Host
dhcpv4.OptionNonLocalSourceRouting.Code(): []byte{0},
// Set the current recommended default time to live for the
// Internet Protocol which is 64, see
// https://datatracker.ietf.org/doc/html/rfc1700.
dhcpv4.OptionDefaultIPTTL.Code(): []byte{64},
// An Internet host that includes embedded gateway code MUST have a
// configuration switch to disable the gateway function, and this switch
// MUST default to the non-gateway mode.
//
// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.3.5.
dhcpv4.OptGeneric(dhcpv4.OptionIPForwarding, []byte{0x0}),
// A host that supports non-local source-routing MUST have a
// configurable switch to disable forwarding, and this switch MUST
// default to disabled.
//
// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.3.5.
dhcpv4.OptGeneric(dhcpv4.OptionNonLocalSourceRouting, []byte{0x0}),
// Do not set the Policy Filter Option since it only makes sense when
// the non-local source routing is enabled.
// The minimum legal value is 576.
//
// See https://datatracker.ietf.org/doc/html/rfc2132#section-4.4.
dhcpv4.Option{
Code: dhcpv4.OptionMaximumDatagramAssemblySize,
Value: dhcpv4.Uint16(576),
},
// Set the current recommended default time to live for the Internet
// Protocol which is 64.
//
// See https://www.iana.org/assignments/ip-parameters/ip-parameters.xhtml#ip-parameters-2.
dhcpv4.OptGeneric(dhcpv4.OptionDefaultIPTTL, []byte{0x40}),
// For example, after the PTMU estimate is decreased, the timeout should
// be set to 10 minutes; once this timer expires and a larger MTU is
// attempted, the timeout can be set to a much smaller value.
//
// See https://datatracker.ietf.org/doc/html/rfc1191#section-6.6.
dhcpv4.Option{
Code: dhcpv4.OptionPathMTUAgingTimeout,
Value: dhcpv4.Duration(10 * time.Minute),
},
// There is a table describing the MTU values representing all major
// data-link technologies in use in the Internet so that each set of
// similar MTUs is associated with a plateau value equal to the lowest
// MTU in the group.
//
// See https://datatracker.ietf.org/doc/html/rfc1191#section-7.
dhcpv4.OptGeneric(dhcpv4.OptionPathMTUPlateauTable, []byte{
0x0, 0x44,
0x1, 0x28,
0x1, 0xFC,
0x3, 0xEE,
0x5, 0xD4,
0x7, 0xD2,
0x11, 0x0,
0x1F, 0xE6,
0x45, 0xFA,
}),
// IP-Layer Per Interface
dhcpv4.OptionPerformMaskDiscovery.Code(): []byte{0},
dhcpv4.OptionMaskSupplier.Code(): []byte{0},
dhcpv4.OptionPerformRouterDiscovery.Code(): []byte{1},
// The all-routers address is preferred wherever possible, see
// https://datatracker.ietf.org/doc/html/rfc1256#section-5.1.
dhcpv4.OptionRouterSolicitationAddress.Code(): netutil.IPv4allrouter(),
dhcpv4.OptionBroadcastAddress.Code(): netutil.IPv4bcast(),
// Since nearly all networks in the Internet currently support an MTU of
// 576 or greater, we strongly recommend the use of 576 for datagrams
// sent to non-local networks.
//
// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.3.3.
dhcpv4.Option{
Code: dhcpv4.OptionInterfaceMTU,
Value: dhcpv4.Uint16(576),
},
// Set the All Subnets Are Local Option to false since commonly the
// connected hosts aren't expected to be multihomed.
//
// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.3.3.
dhcpv4.OptGeneric(dhcpv4.OptionAllSubnetsAreLocal, []byte{0x00}),
// Set the Perform Mask Discovery Option to false to provide the subnet
// mask by options only.
//
// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.2.2.9.
dhcpv4.OptGeneric(dhcpv4.OptionPerformMaskDiscovery, []byte{0x00}),
// A system MUST NOT send an Address Mask Reply unless it is an
// authoritative agent for address masks. An authoritative agent may be
// a host or a gateway, but it MUST be explicitly configured as a
// address mask agent.
//
// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.2.2.9.
dhcpv4.OptGeneric(dhcpv4.OptionMaskSupplier, []byte{0x00}),
// Set the Perform Router Discovery Option to true as per Router
// Discovery Document.
//
// See https://datatracker.ietf.org/doc/html/rfc1256#section-5.1.
dhcpv4.OptGeneric(dhcpv4.OptionPerformRouterDiscovery, []byte{0x01}),
// The all-routers address is preferred wherever possible.
//
// See https://datatracker.ietf.org/doc/html/rfc1256#section-5.1.
dhcpv4.Option{
Code: dhcpv4.OptionRouterSolicitationAddress,
Value: dhcpv4.IP(netutil.IPv4allrouter()),
},
// Don't set the Static Routes Option since it should be set up by
// system administrator.
//
// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.3.1.2.
// A datagram with the destination address of limited broadcast will be
// received by every host on the connected physical network but will not
// be forwarded outside that network.
//
// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.2.1.3.
dhcpv4.OptBroadcastAddress(netutil.IPv4bcast()),
// Link-Layer Per Interface
dhcpv4.OptionTrailerEncapsulation.Code(): []byte{0},
dhcpv4.OptionEthernetEncapsulation.Code(): []byte{0},
// If the system does not dynamically negotiate use of the trailer
// protocol on a per-destination basis, the default configuration MUST
// disable the protocol.
//
// See https://datatracker.ietf.org/doc/html/rfc1122#section-2.3.1.
dhcpv4.OptGeneric(dhcpv4.OptionTrailerEncapsulation, []byte{0x00}),
// For proxy ARP situations, the timeout needs to be on the order of a
// minute.
//
// See https://datatracker.ietf.org/doc/html/rfc1122#section-2.3.2.1.
dhcpv4.Option{
Code: dhcpv4.OptionArpCacheTimeout,
Value: dhcpv4.Duration(time.Minute),
},
// An Internet host that implements sending both the RFC-894 and the
// RFC-1042 encapsulations MUST provide a configuration switch to select
// which is sent, and this switch MUST default to RFC-894.
//
// See https://datatracker.ietf.org/doc/html/rfc1122#section-2.3.3.
dhcpv4.OptGeneric(dhcpv4.OptionEthernetEncapsulation, []byte{0x00}),
// TCP Per Host
dhcpv4.OptionTCPKeepaliveInterval.Code(): dhcpv4.Duration(0).ToBytes(),
dhcpv4.OptionTCPKeepaliveGarbage.Code(): []byte{0},
// A fixed value must be at least big enough for the Internet diameter,
// i.e., the longest possible path. A reasonable value is about twice
// the diameter, to allow for continued Internet growth.
//
// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.2.1.7.
dhcpv4.Option{
Code: dhcpv4.OptionDefaulTCPTTL,
Value: dhcpv4.Duration(60 * time.Second),
},
// The interval MUST be configurable and MUST default to no less than
// two hours.
//
// See https://datatracker.ietf.org/doc/html/rfc1122#section-4.2.3.6.
dhcpv4.Option{
Code: dhcpv4.OptionTCPKeepaliveInterval,
Value: dhcpv4.Duration(2 * time.Hour),
},
// Unfortunately, some misbehaved TCP implementations fail to respond to
// a probe segment unless it contains data.
//
// See https://datatracker.ietf.org/doc/html/rfc1122#section-4.2.3.6.
dhcpv4.OptGeneric(dhcpv4.OptionTCPKeepaliveGarbage, []byte{0x01}),
// Values From Configuration
dhcpv4.OptionRouter.Code(): netutil.CloneIP(conf.subnet.IP),
dhcpv4.OptionSubnetMask.Code(): dhcpv4.IPMask(conf.subnet.Mask).ToBytes(),
}
// Set the Router Option to working subnet's IP since it's initialized
// with the address of the gateway.
dhcpv4.OptRouter(conf.subnet.IP),
dhcpv4.OptSubnetMask(conf.subnet.Mask),
)
// Set values for explicitly configured options.
explicit = dhcpv4.Options{}
for i, o := range conf.Options {
opt, err := parseDHCPOption(o)
code, val, err := parseDHCPOption(o)
if err != nil {
log.Error("dhcpv4: bad option string at index %d: %s", i, err)
continue
}
opts.Update(opt)
explicit.Update(dhcpv4.Option{Code: code, Value: val})
// Remove those from the implicit options.
delete(implicit, code.Code())
}
return opts
log.Debug("dhcpv4: implicit options:\n%s", implicit.Summary(nil))
log.Debug("dhcpv4: explicit options:\n%s", explicit.Summary(nil))
if len(explicit) == 0 {
explicit = nil
}
return implicit, explicit
}

View File

@@ -7,171 +7,260 @@ import (
"fmt"
"net"
"testing"
"time"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseOpt(t *testing.T) {
testCases := []struct {
name string
in string
wantCode dhcpv4.OptionCode
wantVal dhcpv4.OptionValue
wantErrMsg string
wantOpt dhcpv4.Option
}{{
name: "hex_success",
in: "6 hex c0a80101c0a80102",
name: "hex_success",
in: "6 hex c0a80101c0a80102",
wantCode: dhcpv4.GenericOptionCode(6),
wantVal: dhcpv4.OptionGeneric{Data: []byte{
0xC0, 0xA8, 0x01, 0x01,
0xC0, 0xA8, 0x01, 0x02,
}},
wantErrMsg: "",
wantOpt: dhcpv4.OptDNS(
net.IP{0xC0, 0xA8, 0x01, 0x01},
net.IP{0xC0, 0xA8, 0x01, 0x02},
),
}, {
name: "ip_success",
in: "6 ip 1.2.3.4",
wantCode: dhcpv4.GenericOptionCode(6),
wantVal: dhcpv4.IP(net.IP{0x01, 0x02, 0x03, 0x04}),
wantErrMsg: "",
wantOpt: dhcpv4.OptDNS(
net.IP{0x01, 0x02, 0x03, 0x04},
),
}, {
name: "ip_fail_v6",
in: "6 ip ::1234",
wantErrMsg: "invalid option string \"6 ip ::1234\": bad ipv4 address \"::1234\"",
wantOpt: dhcpv4.Option{},
}, {
name: "ips_success",
in: "6 ips 192.168.1.1,192.168.1.2",
name: "ips_success",
in: "6 ips 192.168.1.1,192.168.1.2",
wantCode: dhcpv4.GenericOptionCode(6),
wantVal: dhcpv4.IPs([]net.IP{
{0xC0, 0xA8, 0x01, 0x01},
{0xC0, 0xA8, 0x01, 0x02},
}),
wantErrMsg: "",
wantOpt: dhcpv4.OptDNS(
net.IP{0xC0, 0xA8, 0x01, 0x01},
net.IP{0xC0, 0xA8, 0x01, 0x02},
),
}, {
name: "text_success",
in: "252 text http://192.168.1.1/",
wantCode: dhcpv4.GenericOptionCode(252),
wantVal: dhcpv4.String("http://192.168.1.1/"),
wantErrMsg: "",
}, {
name: "del_success",
in: "61 del",
wantCode: dhcpv4.GenericOptionCode(dhcpv4.OptionClientIdentifier),
wantVal: dhcpv4.OptionGeneric{Data: nil},
wantErrMsg: "",
}, {
name: "bool_success",
in: "19 bool true",
wantCode: dhcpv4.GenericOptionCode(dhcpv4.OptionIPForwarding),
wantVal: dhcpv4.OptionGeneric{Data: []byte{0x01}},
wantErrMsg: "",
}, {
name: "bool_success_false",
in: "19 bool F",
wantCode: dhcpv4.GenericOptionCode(dhcpv4.OptionIPForwarding),
wantVal: dhcpv4.OptionGeneric{Data: []byte{0x00}},
wantErrMsg: "",
}, {
name: "dur_success",
in: "24 dur 2h5s",
wantCode: dhcpv4.GenericOptionCode(dhcpv4.OptionPathMTUAgingTimeout),
wantVal: dhcpv4.Duration(2*time.Hour + 5*time.Second),
wantErrMsg: "",
}, {
name: "u8_success",
in: "23 u8 64",
wantCode: dhcpv4.GenericOptionCode(dhcpv4.OptionDefaultIPTTL),
wantVal: dhcpv4.OptionGeneric{Data: []byte{0x40}},
wantErrMsg: "",
}, {
name: "u16_success",
in: "22 u16 1234",
wantCode: dhcpv4.GenericOptionCode(dhcpv4.OptionMaximumDatagramAssemblySize),
wantVal: dhcpv4.Uint16(1234),
wantErrMsg: "",
wantOpt: dhcpv4.OptGeneric(
dhcpv4.GenericOptionCode(252),
[]byte("http://192.168.1.1/"),
),
}, {
name: "bad_parts",
in: "6 ip",
wantErrMsg: `invalid option string "6 ip": need at least three fields`,
wantOpt: dhcpv4.Option{},
wantCode: nil,
wantVal: nil,
wantErrMsg: `invalid option string "6 ip": bad option format`,
}, {
name: "bad_code",
in: "256 ip 1.1.1.1",
name: "bad_code",
in: "256 ip 1.1.1.1",
wantCode: nil,
wantVal: nil,
wantErrMsg: `invalid option string "256 ip 1.1.1.1": parsing option code: ` +
`strconv.ParseUint: parsing "256": value out of range`,
wantOpt: dhcpv4.Option{},
}, {
name: "bad_type",
in: "6 bad 1.1.1.1",
wantCode: nil,
wantVal: nil,
wantErrMsg: `invalid option string "6 bad 1.1.1.1": unknown option type "bad"`,
wantOpt: dhcpv4.Option{},
}, {
name: "hex_error",
in: "6 hex ZZZ",
name: "hex_error",
in: "6 hex ZZZ",
wantCode: nil,
wantVal: nil,
wantErrMsg: `invalid option string "6 hex ZZZ": decoding hex: ` +
`encoding/hex: invalid byte: U+005A 'Z'`,
wantOpt: dhcpv4.Option{},
}, {
name: "ip_error",
in: "6 ip 1.2.3.x",
wantCode: nil,
wantVal: nil,
wantErrMsg: "invalid option string \"6 ip 1.2.3.x\": bad ipv4 address \"1.2.3.x\"",
wantOpt: dhcpv4.Option{},
}, {
name: "ips_error",
in: "6 ips 192.168.1.1,192.168.1.x",
name: "ip_error_v6",
in: "6 ip ::1234",
wantCode: nil,
wantVal: nil,
wantErrMsg: "invalid option string \"6 ip ::1234\": bad ipv4 address \"::1234\"",
}, {
name: "ips_error",
in: "6 ips 192.168.1.1,192.168.1.x",
wantCode: nil,
wantVal: nil,
wantErrMsg: "invalid option string \"6 ips 192.168.1.1,192.168.1.x\": " +
"parsing ip at index 1: bad ipv4 address \"192.168.1.x\"",
wantOpt: dhcpv4.Option{},
}, {
name: "bool_error",
in: "19 bool yes",
wantCode: nil,
wantVal: nil,
wantErrMsg: "invalid option string \"19 bool yes\": decoding bool: " +
"strconv.ParseBool: parsing \"yes\": invalid syntax",
}, {
name: "dur_error",
in: "24 dur 3y",
wantCode: nil,
wantVal: nil,
wantErrMsg: "invalid option string \"24 dur 3y\": decoding dur: " +
"unmarshaling duration: time: unknown unit \"y\" in duration \"3y\"",
}, {
name: "u8_error",
in: "23 u8 256",
wantCode: nil,
wantVal: nil,
wantErrMsg: "invalid option string \"23 u8 256\": decoding u8: " +
"strconv.ParseUint: parsing \"256\": value out of range",
}, {
name: "u16_error",
in: "23 u16 65536",
wantCode: nil,
wantVal: nil,
wantErrMsg: "invalid option string \"23 u16 65536\": decoding u16: " +
"strconv.ParseUint: parsing \"65536\": value out of range",
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
opt, err := parseDHCPOption(tc.in)
if tc.wantErrMsg != "" {
require.Error(t, err)
code, val, err := parseDHCPOption(tc.in)
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
assert.Equal(t, tc.wantErrMsg, err.Error())
return
}
require.NoError(t, err)
assert.Equal(t, tc.wantOpt.Code.Code(), opt.Code.Code())
assert.Equal(t, tc.wantOpt.Value.ToBytes(), opt.Value.ToBytes())
assert.Equal(t, tc.wantCode, code)
assert.Equal(t, tc.wantVal, val)
})
}
}
func TestPrepareOptions(t *testing.T) {
allDefault := dhcpv4.Options{
dhcpv4.OptionNonLocalSourceRouting.Code(): []byte{0},
dhcpv4.OptionDefaultIPTTL.Code(): []byte{64},
dhcpv4.OptionPerformMaskDiscovery.Code(): []byte{0},
dhcpv4.OptionMaskSupplier.Code(): []byte{0},
dhcpv4.OptionPerformRouterDiscovery.Code(): []byte{1},
dhcpv4.OptionRouterSolicitationAddress.Code(): []byte{224, 0, 0, 2},
dhcpv4.OptionBroadcastAddress.Code(): []byte{255, 255, 255, 255},
dhcpv4.OptionTrailerEncapsulation.Code(): []byte{0},
dhcpv4.OptionEthernetEncapsulation.Code(): []byte{0},
dhcpv4.OptionTCPKeepaliveInterval.Code(): []byte{0, 0, 0, 0},
dhcpv4.OptionTCPKeepaliveGarbage.Code(): []byte{0},
}
oneIP, otherIP := net.IP{1, 2, 3, 4}, net.IP{5, 6, 7, 8}
testCases := []struct {
name string
opts []string
checks dhcpv4.Options
name string
wantExplicit dhcpv4.Options
opts []string
}{{
name: "all_default",
checks: allDefault,
name: "all_default",
wantExplicit: nil,
opts: nil,
}, {
name: "configured_ip",
wantExplicit: dhcpv4.OptionsFromList(
dhcpv4.OptBroadcastAddress(oneIP),
),
opts: []string{
fmt.Sprintf("%d ip %s", dhcpv4.OptionBroadcastAddress, oneIP),
},
checks: dhcpv4.Options{
dhcpv4.OptionBroadcastAddress.Code(): oneIP,
},
}, {
name: "configured_ips",
wantExplicit: dhcpv4.OptionsFromList(
dhcpv4.Option{
Code: dhcpv4.OptionDomainNameServer,
Value: dhcpv4.IPs{oneIP, otherIP},
},
),
opts: []string{
fmt.Sprintf("%d ips %s,%s", dhcpv4.OptionDomainNameServer, oneIP, otherIP),
},
checks: dhcpv4.Options{
dhcpv4.OptionDomainNameServer.Code(): append(oneIP, otherIP...),
},
}, {
name: "configured_bad",
name: "configured_bad",
wantExplicit: nil,
opts: []string{
"19 bool yes",
"24 dur 3y",
"23 u8 256",
"23 u16 65536",
"20 hex",
"23 hex abc",
"32 ips 1,2,3,4",
"28 256.256.256.256",
},
checks: allDefault,
}, {
name: "configured_del",
wantExplicit: dhcpv4.OptionsFromList(
dhcpv4.OptBroadcastAddress(nil),
),
opts: []string{
"28 del",
},
}, {
name: "rewritten_del",
wantExplicit: dhcpv4.OptionsFromList(
dhcpv4.OptBroadcastAddress(netutil.IPv4bcast()),
),
opts: []string{
"28 del",
"28 ip 255.255.255.255",
},
}, {
name: "configured_and_del",
wantExplicit: dhcpv4.OptionsFromList(
dhcpv4.Option{
Code: dhcpv4.OptionGeoConf,
Value: dhcpv4.String("cba"),
},
),
opts: []string{
"123 text abc",
"123 del",
"123 text cba",
},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
opts := prepareOptions(V4ServerConf{
implicit, explicit := prepareOptions(V4ServerConf{
// Just to avoid nil pointer dereference.
subnet: &net.IPNet{},
Options: tc.opts,
})
for c, v := range tc.checks {
optVal := opts.Get(dhcpv4.GenericOptionCode(c))
require.NotNil(t, optVal)
assert.Len(t, optVal, len(v))
assert.Equal(t, v, optVal)
assert.Equal(t, tc.wantExplicit, explicit)
for c := range explicit {
assert.NotContains(t, implicit, c)
}
})
}

View File

@@ -65,39 +65,42 @@ func hwAddrToLinkLayerAddr(hwa net.HardwareAddr) (lla []byte, err error) {
}
// Create an ICMPv6.RouterAdvertisement packet with all necessary options.
// Data scheme:
//
// ICMPv6:
// type[1]
// code[1]
// chksum[2]
// body (RouterAdvertisement):
// Cur Hop Limit[1]
// Flags[1]: MO......
// Router Lifetime[2]
// Reachable Time[4]
// Retrans Timer[4]
// Option=Prefix Information(3):
// Type[1]
// Length * 8bytes[1]
// Prefix Length[1]
// Flags[1]: LA......
// Valid Lifetime[4]
// Preferred Lifetime[4]
// Reserved[4]
// Prefix[16]
// Option=MTU(5):
// Type[1]
// Length * 8bytes[1]
// Reserved[2]
// MTU[4]
// Option=Source link-layer address(1):
// Link-Layer Address[8/24]
// Option=Recursive DNS Server(25):
// Type[1]
// Length * 8bytes[1]
// Reserved[2]
// Lifetime[4]
// Addresses of IPv6 Recursive DNS Servers[16]
// ICMPv6:
// - type[1]
// - code[1]
// - chksum[2]
// - body (RouterAdvertisement):
// - Cur Hop Limit[1]
// - Flags[1]: MO......
// - Router Lifetime[2]
// - Reachable Time[4]
// - Retrans Timer[4]
// - Option=Prefix Information(3):
// - Type[1]
// - Length * 8bytes[1]
// - Prefix Length[1]
// - Flags[1]: LA......
// - Valid Lifetime[4]
// - Preferred Lifetime[4]
// - Reserved[4]
// - Prefix[16]
// - Option=MTU(5):
// - Type[1]
// - Length * 8bytes[1]
// - Reserved[2]
// - MTU[4]
// - Option=Source link-layer address(1):
// - Link-Layer Address[8/24]
// - Option=Recursive DNS Server(25):
// - Type[1]
// - Length * 8bytes[1]
// - Reserved[2]
// - Lifetime[4]
// - Addresses of IPv6 Recursive DNS Servers[16]
//
// TODO(a.garipov): Replace with an existing implementation from a dependency.
func createICMPv6RAPacket(params icmpv6RA) (data []byte, err error) {
var lla []byte
lla, err = hwAddrToLinkLayerAddr(params.sourceLinkLayerAddress)

View File

@@ -71,12 +71,12 @@ type V4ServerConf struct {
// gateway.
subnet *net.IPNet
// notify is a way to signal to other components that leases have
// change. notify must be called outside of locked sections, since the
// notify is a way to signal to other components that leases have been
// changed. notify must be called outside of locked sections, since the
// clients might want to get the new data.
//
// TODO(a.garipov): This is utter madness and must be refactored. It
// just begs for deadlock bugs and other nastiness.
// TODO(a.garipov): This is utter madness and must be refactored. It just
// begs for deadlock bugs and other nastiness.
notify func(uint32)
}

View File

@@ -20,6 +20,7 @@ import (
"github.com/go-ping/ping"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv4/server4"
"golang.org/x/exp/slices"
//lint:ignore SA1019 See the TODO in go.mod.
"github.com/mdlayher/raw"
@@ -32,6 +33,17 @@ type v4Server struct {
conf V4ServerConf
srv *server4.Server
// implicitOpts are the options listed in Appendix A of RFC 2131 initialized
// with default values. It must not have intersections with [explicitOpts].
implicitOpts dhcpv4.Options
// explicitOpts are the options parsed from the configuration. It must not
// have intersections with [implicitOpts].
explicitOpts dhcpv4.Options
// leasesLock protects leases, leaseHosts, and leasedOffsets.
leasesLock sync.Mutex
// leasedOffsets contains offsets from conf.ipRange.start that have been
// leased.
leasedOffsets *bitSet
@@ -41,12 +53,6 @@ type v4Server struct {
// leases contains all dynamic and static leases.
leases []*Lease
// leasesLock protects leases, leaseHosts, and leasedOffsets.
leasesLock sync.Mutex
// options holds predefined DHCP options to return to clients.
options dhcpv4.Options
}
// WriteDiskConfig4 - write configuration
@@ -252,11 +258,11 @@ func (s *v4Server) rmLeaseByIndex(i int) {
// Remove a dynamic lease with the same properties
// Return error if a static lease is found
func (s *v4Server) rmDynamicLease(lease *Lease) (err error) {
for i := 0; i < len(s.leases); i++ {
l := s.leases[i]
for i, l := range s.leases {
isStatic := l.IsStatic()
if bytes.Equal(l.HWAddr, lease.HWAddr) {
if l.IsStatic() {
if bytes.Equal(l.HWAddr, lease.HWAddr) || l.IP.Equal(lease.IP) {
if isStatic {
return errors.Error("static lease already exists")
}
@@ -268,20 +274,7 @@ func (s *v4Server) rmDynamicLease(lease *Lease) (err error) {
l = s.leases[i]
}
if l.IP.Equal(lease.IP) {
if l.IsStatic() {
return errors.Error("static lease already exists")
}
s.rmLeaseByIndex(i)
if i == len(s.leases) {
break
}
l = s.leases[i]
}
if l.Hostname == lease.Hostname {
if !isStatic && l.Hostname == lease.Hostname {
l.Hostname = ""
}
}
@@ -289,6 +282,10 @@ func (s *v4Server) rmDynamicLease(lease *Lease) (err error) {
return nil
}
// ErrDupHostname is returned by addLease when the added lease has a not empty
// non-unique hostname.
const ErrDupHostname = errors.Error("hostname is not unique")
// addLease adds a dynamic or static lease.
func (s *v4Server) addLease(l *Lease) (err error) {
r := s.conf.ipRange
@@ -304,13 +301,17 @@ func (s *v4Server) addLease(l *Lease) (err error) {
return fmt.Errorf("lease %s (%s) out of range, not adding", l.IP, l.HWAddr)
}
s.leases = append(s.leases, l)
s.leasedOffsets.set(offset, true)
if l.Hostname != "" {
if s.leaseHosts.Has(l.Hostname) {
return ErrDupHostname
}
s.leaseHosts.Add(l.Hostname)
}
s.leases = append(s.leases, l)
s.leasedOffsets.set(offset, true)
return nil
}
@@ -365,10 +366,11 @@ func (s *v4Server) AddStaticLease(l *Lease) (err error) {
return fmt.Errorf("validating hostname: %w", err)
}
// Don't check for hostname uniqueness, since we try to emulate
// dnsmasq here, which means that rmDynamicLease below will
// simply empty the hostname of the dynamic lease if there even
// is one.
// Don't check for hostname uniqueness, since we try to emulate dnsmasq
// here, which means that rmDynamicLease below will simply empty the
// hostname of the dynamic lease if there even is one. In case a static
// lease with the same name already exists, addLease will return an
// error and the lease won't be added.
l.Hostname = hostname
}
@@ -421,19 +423,19 @@ func (s *v4Server) RemoveStaticLease(l *Lease) (err error) {
return fmt.Errorf("validating lease: %w", err)
}
defer func() {
if err != nil {
return
}
s.conf.notify(LeaseChangedDBStore)
s.conf.notify(LeaseChangedRemovedStatic)
}()
s.leasesLock.Lock()
err = s.rmLease(l)
if err != nil {
s.leasesLock.Unlock()
defer s.leasesLock.Unlock()
return err
}
s.leasesLock.Unlock()
s.conf.notify(LeaseChangedDBStore)
s.conf.notify(LeaseChangedRemovedStatic)
return nil
return s.rmLease(l)
}
// addrAvailable sends an ICP request to the specified IP address. It returns
@@ -523,11 +525,7 @@ func (s *v4Server) findExpiredLease() int {
// reserveLease reserves a lease for a client by its MAC-address. It returns
// nil if it couldn't allocate a new lease.
func (s *v4Server) reserveLease(mac net.HardwareAddr) (l *Lease, err error) {
l = &Lease{
HWAddr: make([]byte, len(mac)),
}
copy(l.HWAddr, mac)
l = &Lease{HWAddr: slices.Clone(mac)}
l.IP = s.nextIP()
if l.IP == nil {
@@ -549,21 +547,34 @@ func (s *v4Server) reserveLease(mac net.HardwareAddr) (l *Lease, err error) {
return l, nil
}
func (s *v4Server) commitLease(l *Lease) {
l.Expiry = time.Now().Add(s.conf.leaseTime)
// commitLease refreshes l's values. It takes the desired hostname into account
// when setting it into the lease, but generates a unique one if the provided
// can't be used.
func (s *v4Server) commitLease(l *Lease, hostname string) {
prev := l.Hostname
hostname = s.validHostnameForClient(hostname, l.IP)
func() {
s.leasesLock.Lock()
defer s.leasesLock.Unlock()
if s.leaseHosts.Has(hostname) {
log.Info("dhcpv4: hostname %q already exists", hostname)
s.conf.notify(LeaseChangedDBStore)
if l.Hostname != "" {
s.leaseHosts.Add(l.Hostname)
if prev == "" {
// The lease is just allocated due to DHCPDISCOVER.
hostname = aghnet.GenerateHostname(l.IP)
} else {
hostname = prev
}
}()
}
if l.Hostname != hostname {
l.Hostname = hostname
}
s.conf.notify(LeaseChangedAdded)
l.Expiry = time.Now().Add(s.conf.leaseTime)
if prev != "" && prev != l.Hostname {
s.leaseHosts.Del(prev)
}
if l.Hostname != "" {
s.leaseHosts.Add(l.Hostname)
}
}
// allocateLease allocates a new lease for the MAC address. If there are no IP
@@ -585,8 +596,8 @@ func (s *v4Server) allocateLease(mac net.HardwareAddr) (l *Lease, err error) {
}
}
// processDiscover is the handler for the DHCP Discover request.
func (s *v4Server) processDiscover(req, resp *dhcpv4.DHCPv4) (l *Lease, err error) {
// handleDiscover is the handler for the DHCP Discover request.
func (s *v4Server) handleDiscover(req, resp *dhcpv4.DHCPv4) (l *Lease, err error) {
mac := req.ClientHWAddr
defer s.conf.notify(LeaseChangedDBStore)
@@ -620,33 +631,25 @@ func (s *v4Server) processDiscover(req, resp *dhcpv4.DHCPv4) (l *Lease, err erro
return l, nil
}
type optFQDN struct {
name string
}
// OptionFQDN returns a DHCPv4 option for sending the FQDN to the client
// requested another hostname.
//
// See https://datatracker.ietf.org/doc/html/rfc4702.
func OptionFQDN(fqdn string) (opt dhcpv4.Option) {
optData := []byte{
// Set only S and O DHCP client FQDN option flags.
//
// See https://datatracker.ietf.org/doc/html/rfc4702#section-2.1.
1<<0 | 1<<1,
// The RCODE fields should be set to 0xFF in the server responses.
//
// See https://datatracker.ietf.org/doc/html/rfc4702#section-2.2.
0xFF,
0xFF,
}
optData = append(optData, fqdn...)
func (o *optFQDN) String() string {
return "optFQDN"
}
// flags[1]
// A-RR[1]
// PTR-RR[1]
// name[]
func (o *optFQDN) ToBytes() []byte {
b := make([]byte, 3+len(o.name))
i := 0
b[i] = 0x03 // f_server_overrides | f_server
i++
b[i] = 255 // A-RR
i++
b[i] = 255 // PTR-RR
i++
copy(b[i:], []byte(o.name))
return b
return dhcpv4.OptGeneric(dhcpv4.OptionFQDN, optData)
}
// checkLease checks if the pair of mac and ip is already leased. The mismatch
@@ -676,68 +679,177 @@ func (s *v4Server) checkLease(mac net.HardwareAddr, ip net.IP) (lease *Lease, mi
return nil, false
}
// processRequest is the handler for the DHCP Request request.
func (s *v4Server) processRequest(req, resp *dhcpv4.DHCPv4) (lease *Lease, needsReply bool) {
// handleSelecting handles the DHCPREQUEST generated during SELECTING state.
func (s *v4Server) handleSelecting(
req *dhcpv4.DHCPv4,
reqIP net.IP,
sid net.IP,
) (l *Lease, needsReply bool) {
// Client inserts the address of the selected server in server identifier,
// ciaddr MUST be zero.
mac := req.ClientHWAddr
reqIP := req.RequestedIPAddress()
if reqIP == nil {
reqIP = req.ClientIPAddr
}
if !sid.Equal(s.conf.dnsIPAddrs[0]) {
log.Debug("dhcpv4: bad server identifier in req msg for %s: %s", mac, sid)
sid := req.ServerIdentifier()
if len(sid) != 0 && !sid.Equal(s.conf.dnsIPAddrs[0]) {
log.Debug("dhcpv4: bad OptionServerIdentifier in req msg for %s", mac)
return nil, false
} else if ciaddr := req.ClientIPAddr; ciaddr != nil && !ciaddr.IsUnspecified() {
log.Debug("dhcpv4: non-zero ciaddr in selecting req msg for %s", mac)
return nil, false
}
// Requested IP address MUST be filled in with the yiaddr value from the
// chosen DHCPOFFER.
if ip4 := reqIP.To4(); ip4 == nil {
log.Debug("dhcpv4: bad OptionRequestedIPAddress in req msg for %s", mac)
log.Debug("dhcpv4: bad requested address in req msg for %s: %s", mac, reqIP)
return nil, false
}
var mismatch bool
if lease, mismatch = s.checkLease(mac, reqIP); mismatch {
if l, mismatch = s.checkLease(mac, reqIP); mismatch {
return nil, true
}
if lease == nil {
} else if l == nil {
log.Debug("dhcpv4: no reserved lease for %s", mac)
}
return l, true
}
// handleInitReboot handles the DHCPREQUEST generated during INIT-REBOOT state.
func (s *v4Server) handleInitReboot(req *dhcpv4.DHCPv4, reqIP net.IP) (l *Lease, needsReply bool) {
mac := req.ClientHWAddr
if ip4 := reqIP.To4(); ip4 == nil {
log.Debug("dhcpv4: bad requested address in req msg for %s: %s", mac, reqIP)
return nil, false
}
// ciaddr MUST be zero. The client is seeking to verify a previously
// allocated, cached configuration.
if ciaddr := req.ClientIPAddr; ciaddr != nil && !ciaddr.IsUnspecified() {
log.Debug("dhcpv4: non-zero ciaddr in init-reboot req msg for %s", mac)
return nil, false
}
if !s.conf.subnet.Contains(reqIP) {
// If the DHCP server detects that the client is on the wrong net then
// the server SHOULD send a DHCPNAK message to the client.
log.Debug("dhcpv4: wrong subnet in init-reboot req msg for %s: %s", mac, reqIP)
return nil, true
}
if !lease.IsStatic() {
cliHostname := req.HostName()
hostname := s.validHostnameForClient(cliHostname, reqIP)
if hostname != lease.Hostname && s.leaseHosts.Has(hostname) {
log.Info("dhcpv4: hostname %q already exists", hostname)
lease.Hostname = ""
} else {
lease.Hostname = hostname
}
var mismatch bool
if l, mismatch = s.checkLease(mac, reqIP); mismatch {
return nil, true
} else if l == nil {
// If the DHCP server has no record of this client, then it MUST remain
// silent, and MAY output a warning to the network administrator.
log.Info("dhcpv4: warning: no existing lease for %s", mac)
s.commitLease(lease)
} else if lease.Hostname != "" {
o := &optFQDN{
name: lease.Hostname,
}
fqdn := dhcpv4.Option{
Code: dhcpv4.OptionFQDN,
Value: o,
}
return nil, false
}
resp.UpdateOption(fqdn)
return l, true
}
// handleRenew handles the DHCPREQUEST generated during RENEWING or REBINDING
// state.
func (s *v4Server) handleRenew(req *dhcpv4.DHCPv4) (l *Lease, needsReply bool) {
mac := req.ClientHWAddr
// ciaddr MUST be filled in with client's IP address.
ciaddr := req.ClientIPAddr
if ciaddr == nil || ciaddr.IsUnspecified() || ciaddr.To4() == nil {
log.Debug("dhcpv4: bad ciaddr in renew req msg for %s: %s", mac, ciaddr)
return nil, false
}
var mismatch bool
if l, mismatch = s.checkLease(mac, ciaddr); mismatch {
return nil, true
} else if l == nil {
// If the DHCP server has no record of this client, then it MUST remain
// silent, and MAY output a warning to the network administrator.
log.Info("dhcpv4: warning: no existing lease for %s", mac)
return nil, false
}
return l, true
}
// handleByRequestType handles the DHCPREQUEST according to the state during
// which it's generated by client.
func (s *v4Server) handleByRequestType(req *dhcpv4.DHCPv4) (lease *Lease, needsReply bool) {
reqIP, sid := req.RequestedIPAddress(), req.ServerIdentifier()
if sid != nil && !sid.IsUnspecified() {
// If the DHCPREQUEST message contains a server identifier option, the
// message is in response to a DHCPOFFER message. Otherwise, the
// message is a request to verify or extend an existing lease.
return s.handleSelecting(req, reqIP, sid)
}
if reqIP != nil && !reqIP.IsUnspecified() {
// Requested IP address option MUST be filled in with client's notion of
// its previously assigned address.
return s.handleInitReboot(req, reqIP)
}
// Server identifier MUST NOT be filled in, requested IP address option MUST
// NOT be filled in.
return s.handleRenew(req)
}
// handleRequest is the handler for a DHCPREQUEST message.
//
// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.3.2.
func (s *v4Server) handleRequest(req, resp *dhcpv4.DHCPv4) (lease *Lease, needsReply bool) {
lease, needsReply = s.handleByRequestType(req)
if lease == nil {
return nil, needsReply
}
resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck))
return lease, true
hostname := req.HostName()
isRequested := hostname != "" || req.ParameterRequestList().Has(dhcpv4.OptionHostName)
defer func() {
s.conf.notify(LeaseChangedAdded)
s.conf.notify(LeaseChangedDBStore)
}()
s.leasesLock.Lock()
defer s.leasesLock.Unlock()
if lease.IsStatic() {
if lease.Hostname != "" {
// TODO(e.burkov): This option is used to update the server's DNS
// mapping. The option should only be answered when it has been
// requested.
resp.UpdateOption(OptionFQDN(lease.Hostname))
}
return lease, needsReply
}
s.commitLease(lease, hostname)
if isRequested {
resp.UpdateOption(dhcpv4.OptHostName(lease.Hostname))
}
return lease, needsReply
}
// processRequest is the handler for the DHCP Decline request.
func (s *v4Server) processDecline(req, resp *dhcpv4.DHCPv4) (err error) {
// handleDecline is the handler for the DHCP Decline request.
func (s *v4Server) handleDecline(req, resp *dhcpv4.DHCPv4) (err error) {
s.conf.notify(LeaseChangedDBStore)
s.leasesLock.Lock()
@@ -799,8 +911,8 @@ func (s *v4Server) processDecline(req, resp *dhcpv4.DHCPv4) (err error) {
return nil
}
// processRelease is the handler for the DHCP Release request.
func (s *v4Server) processRelease(req, resp *dhcpv4.DHCPv4) (err error) {
// handleRelease is the handler for the DHCP Release request.
func (s *v4Server) handleRelease(req, resp *dhcpv4.DHCPv4) (err error) {
mac := req.ClientHWAddr
reqIP := req.RequestedIPAddress()
if reqIP == nil {
@@ -841,7 +953,7 @@ func (s *v4Server) processRelease(req, resp *dhcpv4.DHCPv4) (err error) {
// Return 1: OK
// Return 0: error; reply with Nak
// Return -1: error; don't reply
func (s *v4Server) process(req, resp *dhcpv4.DHCPv4) int {
func (s *v4Server) handle(req, resp *dhcpv4.DHCPv4) int {
var err error
// Include server's identifier option since any reply should contain it.
@@ -851,11 +963,11 @@ func (s *v4Server) process(req, resp *dhcpv4.DHCPv4) int {
// TODO(a.garipov): Refactor this into handlers.
var l *Lease
switch req.MessageType() {
switch mt := req.MessageType(); mt {
case dhcpv4.MessageTypeDiscover:
l, err = s.processDiscover(req, resp)
l, err = s.handleDiscover(req, resp)
if err != nil {
log.Error("dhcpv4: processing discover: %s", err)
log.Error("dhcpv4: handling discover: %s", err)
return 0
}
@@ -865,7 +977,7 @@ func (s *v4Server) process(req, resp *dhcpv4.DHCPv4) int {
}
case dhcpv4.MessageTypeRequest:
var toReply bool
l, toReply = s.processRequest(req, resp)
l, toReply = s.handleRequest(req, resp)
if l == nil {
if toReply {
return 0
@@ -873,16 +985,16 @@ func (s *v4Server) process(req, resp *dhcpv4.DHCPv4) int {
return -1 // drop packet
}
case dhcpv4.MessageTypeDecline:
err = s.processDecline(req, resp)
err = s.handleDecline(req, resp)
if err != nil {
log.Error("dhcpv4: processing decline: %s", err)
log.Error("dhcpv4: handling decline: %s", err)
return 0
}
case dhcpv4.MessageTypeRelease:
err = s.processRelease(req, resp)
err = s.handleRelease(req, resp)
if err != nil {
log.Error("dhcpv4: processing release: %s", err)
log.Error("dhcpv4: handling release: %s", err)
return 0
}
@@ -892,30 +1004,42 @@ func (s *v4Server) process(req, resp *dhcpv4.DHCPv4) int {
resp.YourIPAddr = netutil.CloneIP(l.IP)
}
// Set IP address lease time for all DHCPOFFER messages and DHCPACK
// messages replied for DHCPREQUEST.
s.updateOptions(req, resp)
return 1
}
// updateOptions updates the options of the response in accordance with the
// request and RFC 2131.
//
// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.3.1.
func (s *v4Server) updateOptions(req, resp *dhcpv4.DHCPv4) {
// Set IP address lease time for all DHCPOFFER messages and DHCPACK messages
// replied for DHCPREQUEST.
//
// TODO(e.burkov): Inspect why this is always set to configured value.
resp.UpdateOption(dhcpv4.OptIPAddressLeaseTime(s.conf.leaseTime))
// Update values for each explicitly configured parameter requested by
// client.
//
// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.3.1.
requested := req.ParameterRequestList()
for _, code := range requested {
if configured := s.options; configured.Has(code) {
resp.UpdateOption(dhcpv4.OptGeneric(code, configured.Get(code)))
// If the server recognizes the parameter as a parameter defined in the Host
// Requirements Document, the server MUST include the default value for that
// parameter.
for _, code := range req.ParameterRequestList() {
if val := s.implicitOpts.Get(code); val != nil {
resp.UpdateOption(dhcpv4.OptGeneric(code, val))
}
}
// Update the value of Domain Name Server option separately from others if
// not assigned yet since its value is set after server's creating.
if requested.Has(dhcpv4.OptionDomainNameServer) &&
!resp.Options.Has(dhcpv4.OptionDomainNameServer) {
resp.UpdateOption(dhcpv4.OptDNS(s.conf.dnsIPAddrs...))
}
return 1
// If the server has been explicitly configured with a default value for the
// parameter or the parameter has a non-default value on the client's
// subnet, the server MUST include that value in an appropriate option.
for code, val := range s.explicitOpts {
if val != nil {
resp.Options[code] = val
} else {
// Delete options explicitly configured to be removed.
delete(resp.Options, code)
}
}
}
// client(0.0.0.0:68) -> (Request:ClientMAC,Type=Discover,ClientID,ReqIP,HostName) -> server(255.255.255.255:67)
@@ -941,6 +1065,7 @@ func (s *v4Server) packetHandler(conn net.PacketConn, peer net.Addr, req *dhcpv4
resp, err := dhcpv4.NewReplyFromRequest(req)
if err != nil {
log.Debug("dhcpv4: dhcpv4.New: %s", err)
return
}
@@ -951,7 +1076,7 @@ func (s *v4Server) packetHandler(conn net.PacketConn, peer net.Addr, req *dhcpv4
return
}
r := s.process(req, resp)
r := s.handle(req, resp)
if r < 0 {
return
} else if r == 0 {
@@ -961,6 +1086,12 @@ func (s *v4Server) packetHandler(conn net.PacketConn, peer net.Addr, req *dhcpv4
s.send(peer, conn, req, resp)
}
// minDHCPMsgSize is the minimum length of the encoded DHCP message in bytes
// according to RFC-2131.
//
// See https://datatracker.ietf.org/doc/html/rfc2131#section-2.
const minDHCPMsgSize = 576
// send writes resp for peer to conn considering the req's parameters according
// to RFC-2131.
//
@@ -1001,8 +1132,22 @@ func (s *v4Server) send(peer net.Addr, conn net.PacketConn, req, resp *dhcpv4.DH
// Go on since peer is already set to broadcast.
}
log.Debug("dhcpv4: sending to %s: %s", peer, resp.Summary())
if _, err := conn.WriteTo(resp.ToBytes(), peer); err != nil {
pktData := resp.ToBytes()
pktLen := len(pktData)
if pktLen < minDHCPMsgSize {
// Expand the packet to match the minimum DHCP message length. Although
// the dhpcv4 package deals with the BOOTP's lower packet length
// constraint, it seems some clients expecting the length being at least
// 576 bytes as per RFC 2131 (and an obsolete RFC 1533).
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/4337.
pktData = append(pktData, make([]byte, minDHCPMsgSize-pktLen)...)
}
log.Debug("dhcpv4: sending %d bytes to %s: %s", len(pktData), peer, resp.Summary())
_, err := conn.WriteTo(pktData, peer)
if err != nil {
log.Error("dhcpv4: conn.Write to %s failed: %s", peer, err)
}
}
@@ -1037,6 +1182,14 @@ func (s *v4Server) Start() (err error) {
// No available IP addresses which may appear later.
return nil
}
// Update the value of Domain Name Server option separately from others if
// not assigned yet since its value is available only at server's start.
//
// TODO(e.burkov): Initialize as implicit option with the rest of default
// options when it will be possible to do before the call to Start.
if !s.explicitOpts.Has(dhcpv4.OptionDomainNameServer) {
s.implicitOpts.Update(dhcpv4.OptDNS(dnsIPAddrs...))
}
s.conf.dnsIPAddrs = dnsIPAddrs
@@ -1162,7 +1315,7 @@ func v4Create(conf V4ServerConf) (srv DHCPServer, err error) {
s.conf.leaseTime = time.Second * time.Duration(conf.LeaseDuration)
}
s.options = prepareOptions(s.conf)
s.implicitOpts, s.explicitOpts = prepareOptions(s.conf)
return s, nil
}

View File

@@ -8,7 +8,10 @@ import (
"net"
"strings"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/insomniacslk/dhcp/dhcpv4"
@@ -23,6 +26,7 @@ var (
DefaultRangeStart = net.IP{192, 168, 10, 100}
DefaultRangeEnd = net.IP{192, 168, 10, 200}
DefaultGatewayIP = net.IP{192, 168, 10, 1}
DefaultSelfIP = net.IP{192, 168, 10, 2}
DefaultSubnetMask = net.IP{255, 255, 255, 0}
)
@@ -39,6 +43,7 @@ func defaultV4ServerConf() (conf V4ServerConf) {
GatewayIP: DefaultGatewayIP,
SubnetMask: DefaultSubnetMask,
notify: notify4,
dnsIPAddrs: []net.IP{DefaultSelfIP},
}
}
@@ -54,6 +59,148 @@ func defaultSrv(t *testing.T) (s DHCPServer) {
return s
}
func TestV4Server_leasing(t *testing.T) {
const (
staticName = "static-client"
anotherName = "another-client"
)
staticIP := net.IP{192, 168, 10, 10}
anotherIP := DefaultRangeStart
staticMAC := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
anotherMAC := net.HardwareAddr{0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB}
s := defaultSrv(t)
t.Run("add_static", func(t *testing.T) {
err := s.AddStaticLease(&Lease{
Expiry: time.Unix(leaseExpireStatic, 0),
Hostname: staticName,
HWAddr: staticMAC,
IP: staticIP,
})
require.NoError(t, err)
t.Run("same_name", func(t *testing.T) {
err = s.AddStaticLease(&Lease{
Expiry: time.Unix(leaseExpireStatic, 0),
Hostname: staticName,
HWAddr: anotherMAC,
IP: anotherIP,
})
assert.ErrorIs(t, err, ErrDupHostname)
})
t.Run("same_mac", func(t *testing.T) {
wantErrMsg := "dhcpv4: adding static lease: removing " +
"dynamic leases for " + anotherIP.String() +
" (" + staticMAC.String() + "): static lease already exists"
err = s.AddStaticLease(&Lease{
Expiry: time.Unix(leaseExpireStatic, 0),
Hostname: anotherName,
HWAddr: staticMAC,
IP: anotherIP,
})
testutil.AssertErrorMsg(t, wantErrMsg, err)
})
t.Run("same_ip", func(t *testing.T) {
wantErrMsg := "dhcpv4: adding static lease: removing " +
"dynamic leases for " + staticIP.String() +
" (" + anotherMAC.String() + "): static lease already exists"
err = s.AddStaticLease(&Lease{
Expiry: time.Unix(leaseExpireStatic, 0),
Hostname: anotherName,
HWAddr: anotherMAC,
IP: staticIP,
})
testutil.AssertErrorMsg(t, wantErrMsg, err)
})
})
t.Run("add_dynamic", func(t *testing.T) {
s4, ok := s.(*v4Server)
require.True(t, ok)
discoverAnOffer := func(
t *testing.T,
name string,
ip net.IP,
mac net.HardwareAddr,
) (resp *dhcpv4.DHCPv4) {
testutil.CleanupAndRequireSuccess(t, func() (err error) {
return s.ResetLeases(s.GetLeases(LeasesStatic))
})
req, err := dhcpv4.NewDiscovery(
mac,
dhcpv4.WithOption(dhcpv4.OptHostName(name)),
dhcpv4.WithOption(dhcpv4.OptRequestedIPAddress(ip)),
dhcpv4.WithOption(dhcpv4.OptClientIdentifier([]byte{1, 2, 3, 4, 5, 6, 8})),
dhcpv4.WithGatewayIP(DefaultGatewayIP),
)
require.NoError(t, err)
resp = &dhcpv4.DHCPv4{}
res := s4.handle(req, resp)
require.Positive(t, res)
require.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType())
resp.ClientHWAddr = mac
return resp
}
t.Run("same_name", func(t *testing.T) {
resp := discoverAnOffer(t, staticName, anotherIP, anotherMAC)
req, err := dhcpv4.NewRequestFromOffer(resp, dhcpv4.WithOption(
dhcpv4.OptHostName(staticName),
))
require.NoError(t, err)
res := s4.handle(req, resp)
require.Positive(t, res)
assert.Equal(t, aghnet.GenerateHostname(resp.YourIPAddr), resp.HostName())
})
t.Run("same_mac", func(t *testing.T) {
resp := discoverAnOffer(t, anotherName, anotherIP, staticMAC)
req, err := dhcpv4.NewRequestFromOffer(resp, dhcpv4.WithOption(
dhcpv4.OptHostName(anotherName),
))
require.NoError(t, err)
res := s4.handle(req, resp)
require.Positive(t, res)
fqdnOptData := resp.Options.Get(dhcpv4.OptionFQDN)
require.Len(t, fqdnOptData, 3+len(staticName))
assert.Equal(t, []uint8(staticName), fqdnOptData[3:])
assert.Equal(t, staticIP, resp.YourIPAddr)
})
t.Run("same_ip", func(t *testing.T) {
resp := discoverAnOffer(t, anotherName, staticIP, anotherMAC)
req, err := dhcpv4.NewRequestFromOffer(resp, dhcpv4.WithOption(
dhcpv4.OptHostName(anotherName),
))
require.NoError(t, err)
res := s4.handle(req, resp)
require.Positive(t, res)
assert.NotEqual(t, staticIP, resp.YourIPAddr)
})
})
}
func TestV4Server_AddRemove_static(t *testing.T) {
s := defaultSrv(t)
@@ -182,7 +329,7 @@ func TestV4_AddReplace(t *testing.T) {
}
}
func TestV4Server_Process_optionsPriority(t *testing.T) {
func TestV4Server_handle_optionsPriority(t *testing.T) {
defaultIP := net.IP{192, 168, 1, 1}
knownIP := net.IP{1, 2, 3, 4}
@@ -199,6 +346,8 @@ func TestV4Server_Process_optionsPriority(t *testing.T) {
stringutil.WriteToBuilder(b, ",", ip.String())
}
conf.Options = []string{b.String()}
} else {
defer func() { s.implicitOpts.Update(dhcpv4.OptDNS(defaultIP)) }()
}
ss, err := v4Create(conf)
@@ -228,7 +377,7 @@ func TestV4Server_Process_optionsPriority(t *testing.T) {
resp, err = dhcpv4.NewReplyFromRequest(req)
require.NoError(t, err)
res := s.process(req, resp)
res := s.handle(req, resp)
require.Equal(t, 1, res)
o := resp.GetOneOption(dhcpv4.OptionDomainNameServer)
@@ -254,6 +403,111 @@ func TestV4Server_Process_optionsPriority(t *testing.T) {
})
}
func TestV4Server_updateOptions(t *testing.T) {
testIP := net.IP{1, 2, 3, 4}
dontWant := func(c dhcpv4.OptionCode) (opt dhcpv4.Option) {
return dhcpv4.OptGeneric(c, nil)
}
testCases := []struct {
name string
wantOpts dhcpv4.Options
reqMods []dhcpv4.Modifier
confOpts []string
}{{
name: "requested_default",
wantOpts: dhcpv4.OptionsFromList(
dhcpv4.OptBroadcastAddress(netutil.IPv4bcast()),
),
reqMods: []dhcpv4.Modifier{
dhcpv4.WithRequestedOptions(dhcpv4.OptionBroadcastAddress),
},
confOpts: nil,
}, {
name: "requested_non-default",
wantOpts: dhcpv4.OptionsFromList(
dhcpv4.OptBroadcastAddress(testIP),
),
reqMods: []dhcpv4.Modifier{
dhcpv4.WithRequestedOptions(dhcpv4.OptionBroadcastAddress),
},
confOpts: []string{
fmt.Sprintf("%d ip %s", dhcpv4.OptionBroadcastAddress, testIP),
},
}, {
name: "non-requested_default",
wantOpts: dhcpv4.OptionsFromList(
dontWant(dhcpv4.OptionBroadcastAddress),
),
reqMods: nil,
confOpts: nil,
}, {
name: "non-requested_non-default",
wantOpts: dhcpv4.OptionsFromList(
dhcpv4.OptBroadcastAddress(testIP),
),
reqMods: nil,
confOpts: []string{
fmt.Sprintf("%d ip %s", dhcpv4.OptionBroadcastAddress, testIP),
},
}, {
name: "requested_deleted",
wantOpts: dhcpv4.OptionsFromList(
dontWant(dhcpv4.OptionBroadcastAddress),
),
reqMods: []dhcpv4.Modifier{
dhcpv4.WithRequestedOptions(dhcpv4.OptionBroadcastAddress),
},
confOpts: []string{
fmt.Sprintf("%d del", dhcpv4.OptionBroadcastAddress),
},
}, {
name: "requested_non-default_deleted",
wantOpts: dhcpv4.OptionsFromList(
dontWant(dhcpv4.OptionBroadcastAddress),
),
reqMods: []dhcpv4.Modifier{
dhcpv4.WithRequestedOptions(dhcpv4.OptionBroadcastAddress),
},
confOpts: []string{
fmt.Sprintf("%d ip %s", dhcpv4.OptionBroadcastAddress, testIP),
fmt.Sprintf("%d del", dhcpv4.OptionBroadcastAddress),
},
}}
for _, tc := range testCases {
req, err := dhcpv4.New(tc.reqMods...)
require.NoError(t, err)
resp, err := dhcpv4.NewReplyFromRequest(req)
require.NoError(t, err)
conf := defaultV4ServerConf()
conf.Options = tc.confOpts
s, err := v4Create(conf)
require.NoError(t, err)
require.IsType(t, (*v4Server)(nil), s)
s4, _ := s.(*v4Server)
t.Run(tc.name, func(t *testing.T) {
s4.updateOptions(req, resp)
for c, v := range tc.wantOpts {
if v == nil {
assert.NotContains(t, resp.Options, c)
continue
}
assert.Equal(t, v, resp.Options.Get(dhcpv4.GenericOptionCode(c)))
}
})
}
}
func TestV4StaticLease_Get(t *testing.T) {
sIface := defaultSrv(t)
@@ -261,6 +515,7 @@ func TestV4StaticLease_Get(t *testing.T) {
require.True(t, ok)
s.conf.dnsIPAddrs = []net.IP{{192, 168, 10, 1}}
s.implicitOpts.Update(dhcpv4.OptDNS(s.conf.dnsIPAddrs...))
l := &Lease{
Hostname: "static-1.local",
@@ -282,7 +537,7 @@ func TestV4StaticLease_Get(t *testing.T) {
resp, err = dhcpv4.NewReplyFromRequest(req)
require.NoError(t, err)
assert.Equal(t, 1, s.process(req, resp))
assert.Equal(t, 1, s.handle(req, resp))
})
// Don't continue if we got any errors in the previous subtest.
@@ -305,7 +560,7 @@ func TestV4StaticLease_Get(t *testing.T) {
resp, err = dhcpv4.NewReplyFromRequest(req)
require.NoError(t, err)
assert.Equal(t, 1, s.process(req, resp))
assert.Equal(t, 1, s.handle(req, resp))
})
require.NoError(t, err)
@@ -349,6 +604,7 @@ func TestV4DynamicLease_Get(t *testing.T) {
require.True(t, ok)
s.conf.dnsIPAddrs = []net.IP{{192, 168, 10, 1}}
s.implicitOpts.Update(dhcpv4.OptDNS(s.conf.dnsIPAddrs...))
var req, resp *dhcpv4.DHCPv4
mac := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
@@ -363,7 +619,7 @@ func TestV4DynamicLease_Get(t *testing.T) {
resp, err = dhcpv4.NewReplyFromRequest(req)
require.NoError(t, err)
assert.Equal(t, 1, s.process(req, resp))
assert.Equal(t, 1, s.handle(req, resp))
})
// Don't continue if we got any errors in the previous subtest.
@@ -397,7 +653,7 @@ func TestV4DynamicLease_Get(t *testing.T) {
resp, err = dhcpv4.NewReplyFromRequest(req)
require.NoError(t, err)
assert.Equal(t, 1, s.process(req, resp))
assert.Equal(t, 1, s.handle(req, resp))
})
require.NoError(t, err)