mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2026-10-12 02:01:26 +00:00
Pull request 2734: AGDNS-3863-gopacket-dhcp-vol.32
Updates #4923.
Squashed commit of the following:
commit d423443053c188101efc0234709c9b899446e684
Merge: aed226338 6f4120483
Author: Eugene Burkov <[email protected]>
Date: Fri Jul 31 18:22:21 2026 +0300
Merge branch 'master' into AGDNS-3863-gopacket-dhcp-vol.32
commit aed226338259c0360141db94d87595a8295cbeed
Author: Eugene Burkov <[email protected]>
Date: Fri Jul 31 16:47:21 2026 +0300
dhcpsvc: imp docs
commit 7c13ad6ca7229b01895f8e81a449ec9b71922325
Author: Eugene Burkov <[email protected]>
Date: Thu Jul 30 13:17:22 2026 +0300
dhcpsvc: imp code
commit 54fa1eeb4c163f84d204aeeccbf5bb4033b67cdb
Author: Eugene Burkov <[email protected]>
Date: Tue Jul 28 20:03:19 2026 +0300
dhcpsvc: imp code, resolve few todos
commit 4b81d0945e6d11d50b5f3da7cdaa4390d4255453
Author: Eugene Burkov <[email protected]>
Date: Tue Jul 28 15:41:31 2026 +0300
all: imp gitattrs
commit ce0a5fefee564578674b003955d5a38b6f4d0faf
Author: Eugene Burkov <[email protected]>
Date: Tue Jul 28 15:29:29 2026 +0300
dhcpsvc: fix windows line breaks
commit ceed0784247908c3993ccc3968061f0780679780
Author: Eugene Burkov <[email protected]>
Date: Tue Jul 28 15:03:19 2026 +0300
dhcpsvc: handle renew
This commit is contained in:
@@ -3,3 +3,4 @@ client/* linguist-vendored
|
||||
# grepping. Technically, this file must be reviewed when new icons appear, but
|
||||
# that happens fairly rarely.
|
||||
client/src/components/ui/Icons.js -diff
|
||||
* text=auto eol=lf
|
||||
|
||||
@@ -25,7 +25,7 @@ type Config struct {
|
||||
// NetworkDeviceManager is the manager of network devices. It must not be
|
||||
// nil.
|
||||
//
|
||||
// TODO(e.burkov): Set.
|
||||
// TODO(e.burkov): Implement and set.
|
||||
NetworkDeviceManager NetworkDeviceManager
|
||||
|
||||
// Logger will be used to log the DHCP events. It must not be nil.
|
||||
|
||||
@@ -62,7 +62,7 @@ func TestIPv4Config_Validate(t *testing.T) {
|
||||
RangeEnd: testIPv4Conf.RangeEnd,
|
||||
LeaseDuration: testIPv4Conf.LeaseDuration,
|
||||
},
|
||||
wantErrMsg: "gateway ip " + testRangeStartV6Str + " must be a valid ipv4" + "\n" +
|
||||
wantErrMsg: "gateway ip: " + testRangeStartV6Str + ": must be a valid ipv4" + "\n" +
|
||||
"range start " + testRangeStartV4Str + " is not within " + testRangeStartV6Str + "/24",
|
||||
}, {
|
||||
name: "bad_subnet_mask",
|
||||
@@ -75,7 +75,7 @@ func TestIPv4Config_Validate(t *testing.T) {
|
||||
RangeEnd: testIPv4Conf.RangeEnd,
|
||||
LeaseDuration: testIPv4Conf.LeaseDuration,
|
||||
},
|
||||
wantErrMsg: "subnet mask " + testRangeStartV6Str + " must be a valid ipv4 cidr mask",
|
||||
wantErrMsg: "subnet mask: " + testRangeStartV6Str + ": must be a valid ipv4 cidr mask",
|
||||
}, {
|
||||
name: "bad_range_start",
|
||||
conf: &dhcpsvc.IPv4Config{
|
||||
@@ -87,7 +87,7 @@ func TestIPv4Config_Validate(t *testing.T) {
|
||||
RangeEnd: testIPv4Conf.RangeEnd,
|
||||
LeaseDuration: testIPv4Conf.LeaseDuration,
|
||||
},
|
||||
wantErrMsg: "range start " + testRangeStartV6Str + " must be a valid ipv4" + "\n" +
|
||||
wantErrMsg: "range start: " + testRangeStartV6Str + ": must be a valid ipv4" + "\n" +
|
||||
"range start " + testRangeStartV6Str + " is not within " +
|
||||
testGatewayIPv4Str + "/24" + "\n" + "invalid ip range: " + testRangeStartV6Str +
|
||||
" and " + testRangeEndV4Str + " must be within the same address family",
|
||||
@@ -102,7 +102,7 @@ func TestIPv4Config_Validate(t *testing.T) {
|
||||
RangeEnd: netip.MustParseAddr(testRangeStartV6Str),
|
||||
LeaseDuration: testIPv4Conf.LeaseDuration,
|
||||
},
|
||||
wantErrMsg: "range end " + testRangeStartV6Str + " must be a valid ipv4" + "\n" +
|
||||
wantErrMsg: "range end: " + testRangeStartV6Str + ": must be a valid ipv4" + "\n" +
|
||||
"range end " + testRangeStartV6Str + " is not within " + testGatewayIPv4Str + "/24" +
|
||||
"\n" + "invalid ip range: " + testRangeStartV4Str + " and " + testRangeStartV6Str +
|
||||
" must be within the same address family",
|
||||
|
||||
@@ -38,15 +38,17 @@ type Interface interface {
|
||||
|
||||
// MACByIP returns the MAC address for the given IP address leased. It
|
||||
// returns nil if there is no such client, due to an assumption that a DHCP
|
||||
// client must always have a MAC address.
|
||||
//
|
||||
// TODO(e.burkov): Think of a contract for the returned value.
|
||||
// client must always have a MAC address. mac must be a valid MAC address
|
||||
// according to [netutil.ValidateMAC].
|
||||
MACByIP(ip netip.Addr) (mac net.HardwareAddr)
|
||||
|
||||
// IPByHost returns the IP address of the DHCP client with the given
|
||||
// hostname. The hostname will be an empty string if there is no such
|
||||
// client, due to an assumption that a DHCP client must always have a
|
||||
// hostname, either set or generated.
|
||||
//
|
||||
// TODO(e.burkov): Support several IP addresses for a single hostname.
|
||||
// This is possible if the client has several network interfaces.
|
||||
IPByHost(host string) (ip netip.Addr)
|
||||
|
||||
// Leases returns all the active DHCP leases. The returned slice should be
|
||||
@@ -70,8 +72,6 @@ type Interface interface {
|
||||
RemoveLease(ctx context.Context, l *Lease) (err error)
|
||||
|
||||
// Reset removes all the DHCP leases.
|
||||
//
|
||||
// TODO(e.burkov): If it's really needed?
|
||||
Reset(ctx context.Context) (err error)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,10 +20,6 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TODO(e.burkov): Use addresses and prefixes from [RFC 5737].
|
||||
//
|
||||
// [RFC 5737]: https://datatracker.ietf.org/doc/html/rfc5737
|
||||
|
||||
// testLocalTLD is a common local TLD for tests.
|
||||
const testLocalTLD = "local"
|
||||
|
||||
@@ -180,7 +176,7 @@ var (
|
||||
|
||||
// testIfaceHWAddr is a common valid hardware address of the test network
|
||||
// interface.
|
||||
testIfaceHWAddr = net.HardwareAddr{0x01, 0x01, 0x01, 0x01, 0x01, 0x01}
|
||||
testIfaceHWAddr = net.HardwareAddr{0x00, 0x00, 0x5E, 0x00, 0x53, 0xFF}
|
||||
)
|
||||
|
||||
// testInterfaceConf is a common valid set of interface configurations for
|
||||
@@ -216,19 +212,19 @@ var testInterfaceConf = map[string]*dhcpsvc.InterfaceConfig{
|
||||
// NOTE: Keep in sync with testdata.
|
||||
var (
|
||||
// testHWUnknown is the test MAC address for an unknown client.
|
||||
testHWUnknown = net.HardwareAddr{0x0, 0x1, 0x2, 0x3, 0x4, 0x5}
|
||||
testHWUnknown = net.HardwareAddr{0x00, 0x00, 0x5E, 0x00, 0x53, 0x01}
|
||||
|
||||
// testHWStatic is the test MAC address for a known static lease.
|
||||
testHWStatic = net.HardwareAddr{0x1, 0x2, 0x3, 0x4, 0x5, 0x6}
|
||||
testHWStatic = net.HardwareAddr{0x00, 0x00, 0x5E, 0x00, 0x53, 0x02}
|
||||
|
||||
// testHWDynamic is the test MAC address for a known dynamic lease.
|
||||
testHWDynamic = net.HardwareAddr{0x2, 0x3, 0x4, 0x5, 0x6, 0x7}
|
||||
testHWDynamic = net.HardwareAddr{0x00, 0x00, 0x5E, 0x00, 0x53, 0x03}
|
||||
|
||||
// testHWExpired is the test MAC address for a known expired lease.
|
||||
testHWExpired = net.HardwareAddr{0x3, 0x4, 0x5, 0x6, 0x7, 0x8}
|
||||
testHWExpired = net.HardwareAddr{0x00, 0x00, 0x5E, 0x00, 0x53, 0x04}
|
||||
|
||||
// testHWAnother is the test MAC address for a lease with another IP.
|
||||
testHWAnother = net.HardwareAddr{0x4, 0x5, 0x6, 0x7, 0x8, 0x9}
|
||||
testHWAnother = net.HardwareAddr{0x00, 0x00, 0x5E, 0x00, 0x53, 0x05}
|
||||
)
|
||||
|
||||
// IPv4 addresses for tests.
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package dhcpsvc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// newMustErr returns an error that indicates that valName must be as must
|
||||
// describes.
|
||||
//
|
||||
// TODO(e.burkov): Use [validate] and remove this function.
|
||||
func newMustErr(valName, must string, val fmt.Stringer) (err error) {
|
||||
return fmt.Errorf("%s %s must %s", valName, val, must)
|
||||
}
|
||||
@@ -146,7 +146,7 @@ func (iface *dhcpInterfaceV4) handleDiscover(
|
||||
return
|
||||
}
|
||||
|
||||
lease, err := iface.common.allocateLease(ctx, mac, now)
|
||||
lease, err := iface.common.allocateLease(ctx, mac, mk, now)
|
||||
if err != nil {
|
||||
l.ErrorContext(ctx, "allocating a lease", slogutil.KeyError, err)
|
||||
|
||||
|
||||
@@ -187,14 +187,7 @@ func (iface *dhcpInterfaceV6) handleRequest(
|
||||
return respond6(fd, resp)
|
||||
}
|
||||
|
||||
var ianaOpt layers.DHCPv6Option
|
||||
|
||||
lease, err := iface.leaseForRequest(ctx, req, fd.ether.SrcMAC)
|
||||
if err != nil {
|
||||
ianaOpt = newIANAWithStatus(iana.ID, layers.DHCPv6StatusCodeNoAddrsAvail)
|
||||
} else {
|
||||
ianaOpt = iface.iaNAFromLease(lease, iana.ID)
|
||||
}
|
||||
ianaOpt := iface.ianaForRequest(ctx, req, iana.ID, fd.ether.SrcMAC)
|
||||
|
||||
resp.Options = iface.newRequestRespOpts(fd, req, cliID, ianaOpt)
|
||||
|
||||
@@ -241,7 +234,7 @@ func (iface *dhcpInterfaceV6) handleConfirm(
|
||||
resp := &layers.DHCPv6{
|
||||
MsgType: layers.DHCPv6MsgTypeReply,
|
||||
TransactionID: req.TransactionID,
|
||||
Options: iface.newConfirmRespOpts(fd, cliID, status),
|
||||
Options: iface.newConfirmRespOpts(fd, req, cliID, status),
|
||||
}
|
||||
|
||||
return respond6(fd, resp)
|
||||
@@ -250,7 +243,9 @@ func (iface *dhcpInterfaceV6) handleConfirm(
|
||||
// handleRenew handles messages of type RENEW. req must not be nil and must be
|
||||
// a valid DHCPv6 message of type RENEW. fd must be valid.
|
||||
//
|
||||
// TODO(e.burkov): Implement. This is a stub for now.
|
||||
// TODO(e.burkov): The current implementation renews only the first valid IA_NA
|
||||
// option. It does not verify that the addresses in the IA match the stored
|
||||
// lease, since clients are identified by MAC address rather than DUID+IAID.
|
||||
func (iface *dhcpInterfaceV6) handleRenew(
|
||||
ctx context.Context,
|
||||
fd *frameData6,
|
||||
@@ -264,7 +259,30 @@ func (iface *dhcpInterfaceV6) handleRenew(
|
||||
l := iface.common.logger
|
||||
l.DebugContext(ctx, "handling message", "type", req.MsgType, "cli_id", cliID)
|
||||
|
||||
return nil
|
||||
iface.common.indexMu.Lock()
|
||||
defer iface.common.indexMu.Unlock()
|
||||
|
||||
resp := &layers.DHCPv6{
|
||||
MsgType: layers.DHCPv6MsgTypeReply,
|
||||
TransactionID: req.TransactionID,
|
||||
}
|
||||
|
||||
iana, ok := iface.firstIANA(ctx, req)
|
||||
if !ok {
|
||||
// With no IA_NA options and no requested addresses there's nothing to
|
||||
// renew. Respond with no IA options similarly to how the Request
|
||||
// handler does.
|
||||
//
|
||||
// See RFC 9915 Section 18.3.4.
|
||||
resp.Options = iface.newRenewRespOpts(fd, req, cliID, layers.DHCPv6Option{})
|
||||
|
||||
return respond6(fd, resp)
|
||||
}
|
||||
|
||||
ianaOpt := iface.ianaForRenew(ctx, req, iana, fd.ether.SrcMAC)
|
||||
resp.Options = iface.newRenewRespOpts(fd, req, cliID, ianaOpt)
|
||||
|
||||
return respond6(fd, resp)
|
||||
}
|
||||
|
||||
// handleRebind handles messages of type REBIND. req must not be nil and must
|
||||
|
||||
@@ -487,6 +487,92 @@ func TestDHCPServer_ServeEther6_confirm(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDHCPServer_ServeEther6_renew(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
in gopacket.Packet
|
||||
want *dhcpsvc.Lease
|
||||
name string
|
||||
wantOpts layers.DHCPv6Options
|
||||
}{{
|
||||
in: newDHCPv6Renew(t, testHWDynamic, testIPv6Dynamic),
|
||||
name: "success",
|
||||
want: testLease6Dynamic,
|
||||
wantOpts: layers.DHCPv6Options{
|
||||
newOptServerDUID(t, testIfaceHWAddr),
|
||||
newOptClientDUID(t, testHWDynamic),
|
||||
newOptIANA(t, testIAID, testIPv6Dynamic, testLeaseTTL),
|
||||
newOptPreference(t, 0),
|
||||
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
|
||||
},
|
||||
}, {
|
||||
in: newDHCPv6Renew(t, testHWStatic, testIPv6Static),
|
||||
name: "success_static",
|
||||
want: testLease6Static,
|
||||
wantOpts: layers.DHCPv6Options{
|
||||
newOptServerDUID(t, testIfaceHWAddr),
|
||||
newOptClientDUID(t, testHWStatic),
|
||||
newOptIANA(t, testIAID, testIPv6Static, testLeaseTTL),
|
||||
newOptPreference(t, 0),
|
||||
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
|
||||
},
|
||||
}, {
|
||||
in: newDHCPv6Renew(t, testHWUnknown, testIPv6Unknown),
|
||||
name: "no_binding",
|
||||
want: nil,
|
||||
wantOpts: layers.DHCPv6Options{
|
||||
newOptServerDUID(t, testIfaceHWAddr),
|
||||
newOptClientDUID(t, testHWUnknown),
|
||||
newOptPreference(t, 0),
|
||||
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
|
||||
},
|
||||
}, {
|
||||
in: newDHCPv6Renew(t, testHWUnknown, netip.Addr{}),
|
||||
name: "no_iana",
|
||||
want: nil,
|
||||
wantOpts: layers.DHCPv6Options{
|
||||
newOptServerDUID(t, testIfaceHWAddr),
|
||||
newOptClientDUID(t, testHWUnknown),
|
||||
newOptPreference(t, 0),
|
||||
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
|
||||
},
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
req := testutil.RequireTypeAssert[*layers.DHCPv6](t, tc.in.Layer(layers.LayerTypeDHCPv6))
|
||||
|
||||
db := newTestDatabase(t, testLeases)
|
||||
|
||||
onStore := func(ctx context.Context, leases []*dhcpsvc.Lease) (err error) {
|
||||
assert.Contains(t, leases, tc.want)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if tc.want != nil {
|
||||
db.onStore = onStore
|
||||
}
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceAddrV6)
|
||||
startTestDHCPServer(t, &dhcpsvc.Config{
|
||||
Database: db,
|
||||
Interfaces: testIPv6InterfacesConf,
|
||||
Logger: testLogger,
|
||||
NetworkDeviceManager: ndMgr,
|
||||
Enabled: true,
|
||||
})
|
||||
|
||||
testutil.RequireSend(t, inCh, tc.in, testTimeout)
|
||||
|
||||
assertValidResponse6(t, req, outCh, tc.wantOpts)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// newDHCPv6Solicit creates a new DHCPv6 SOLICIT packet for testing.
|
||||
func newDHCPv6Solicit(
|
||||
tb testing.TB,
|
||||
@@ -710,3 +796,31 @@ func assertValidDHCPv6(
|
||||
tb.Errorf("request message type: %v: %s", errors.ErrUnexpectedValue, req.MsgType)
|
||||
}
|
||||
}
|
||||
|
||||
// newDHCPv6Renew creates a new DHCPv6 RENEW packet for testing.
|
||||
func newDHCPv6Renew(tb testing.TB, mac net.HardwareAddr, reqIP netip.Addr) (pkt gopacket.Packet) {
|
||||
tb.Helper()
|
||||
|
||||
eth := newEthernetLayer(tb, mac, testIfaceHWAddr, layers.EthernetTypeIPv6)
|
||||
ip, udp := newIPv6UDPLayer(tb, netip.AddrPort{}, netip.AddrPort{})
|
||||
|
||||
dhcp := &layers.DHCPv6{
|
||||
MsgType: layers.DHCPv6MsgTypeRenew,
|
||||
HopCount: 0,
|
||||
// Don't specify link and peer addresses, as they are intended for relay
|
||||
// messages.
|
||||
LinkAddr: nil,
|
||||
PeerAddr: nil,
|
||||
TransactionID: testTransactionID,
|
||||
Options: layers.DHCPv6Options{
|
||||
newOptClientDUID(tb, mac),
|
||||
newOptServerDUID(tb, testIfaceHWAddr),
|
||||
},
|
||||
}
|
||||
|
||||
if reqIP.IsValid() && reqIP.Is6() {
|
||||
dhcp.Options = append(dhcp.Options, newOptIANA(tb, testIAID, reqIP, testLeaseTTL))
|
||||
}
|
||||
|
||||
return newTestPacket(tb, layers.LinkTypeEthernet, eth, ip, udp, dhcp)
|
||||
}
|
||||
|
||||
@@ -181,20 +181,18 @@ func (iface *netInterface) findExpiredLease(now time.Time) (l *Lease) {
|
||||
}
|
||||
|
||||
// allocateLease allocates a new lease for the MAC address. If there are no IP
|
||||
// addresses left, both lease and err are nil. mac must be a valid according to
|
||||
// [netutil.ValidateMAC].
|
||||
//
|
||||
// TODO(e.burkov): Pass the precalculated macKey.
|
||||
// addresses left, both lease and err are nil. mac must be a valid hardware
|
||||
// address according to [netutil.ValidateMAC], macKey must be calculated from
|
||||
// mac.
|
||||
//
|
||||
// TODO(e.burkov): Support allocating the exact requested address if it is
|
||||
// available.
|
||||
func (iface *netInterface) allocateLease(
|
||||
ctx context.Context,
|
||||
mac net.HardwareAddr,
|
||||
macKey macKey,
|
||||
now time.Time,
|
||||
) (lease *Lease, err error) {
|
||||
key := macToKey(mac)
|
||||
|
||||
for {
|
||||
lease, err = iface.reserveLease(ctx, mac, now)
|
||||
if err != nil {
|
||||
@@ -208,7 +206,7 @@ func (iface *netInterface) allocateLease(
|
||||
}
|
||||
|
||||
if ok {
|
||||
iface.leases[key] = lease
|
||||
iface.leases[macKey] = lease
|
||||
|
||||
off, _ := iface.addrSpace.offset(lease.IP)
|
||||
iface.leasedOffsets.set(off, true)
|
||||
@@ -252,9 +250,7 @@ func (iface *netInterface) reserveLease(
|
||||
|
||||
err = iface.index.remove(ctx, lease, iface)
|
||||
if err != nil {
|
||||
// TODO(e.burkov): Reconsider the severity of this error, it actually
|
||||
// seems impossible to get the error about the existing lease from the
|
||||
// method.
|
||||
// The lease may have been allocated but not yet committed.
|
||||
iface.logger.DebugContext(ctx, "deleting expired lease", slogutil.KeyError, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -65,8 +65,6 @@ type ipPredicate func(ip netip.Addr) (ok bool)
|
||||
|
||||
// find finds the first IP address in r for which p returns true. It returns an
|
||||
// empty [netip.Addr] if there are no addresses that satisfy p.
|
||||
//
|
||||
// TODO(e.burkov): Use.
|
||||
func (r ipRange) find(p ipPredicate) (ip netip.Addr) {
|
||||
for ip = r.start; !r.end.Less(ip); ip = ip.Next() {
|
||||
if p(ip) {
|
||||
|
||||
@@ -132,8 +132,6 @@ func (c *JSONDatabaseConfig) Validate() (err error) {
|
||||
|
||||
// JSONDatabase is a [Database] implementation that stores leases in a JSON
|
||||
// file.
|
||||
//
|
||||
// TODO(e.burkov): Test.
|
||||
type JSONDatabase struct {
|
||||
// mu protects the database file from concurrent access.
|
||||
mu *sync.RWMutex
|
||||
|
||||
@@ -97,9 +97,6 @@ func (idx *leaseIndex) add(l *Lease, iface *netInterface) (err error) {
|
||||
// remove removes l from idx and from iface. l must be valid, iface should
|
||||
// contain the same lease or the lease itself. It returns an error if the lease
|
||||
// not found.
|
||||
//
|
||||
// TODO(e.burkov): Consider using the iface's logger after simplifying
|
||||
// relations between index and interfaces.
|
||||
func (idx *leaseIndex) remove(
|
||||
ctx context.Context,
|
||||
l *Lease,
|
||||
@@ -189,13 +186,13 @@ func (idx *leaseIndex) dbLoad(
|
||||
return fmt.Errorf("loading leases: %w", err)
|
||||
}
|
||||
|
||||
idx.addDBLeases(ctx, logger, leases, ifaces4, ifaces6)
|
||||
idx.indexLeases(ctx, logger, leases, ifaces4, ifaces6)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addDBLeases adds leases to the server. logger must not be nil.
|
||||
func (idx *leaseIndex) addDBLeases(
|
||||
// indexLeases adds leases to idx. logger must not be nil.
|
||||
func (idx *leaseIndex) indexLeases(
|
||||
ctx context.Context,
|
||||
logger *slog.Logger,
|
||||
leases []*Lease,
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"time"
|
||||
@@ -353,9 +352,6 @@ func (iface *dhcpInterfaceV4) appendTimeOptions(
|
||||
|
||||
// appendHostnameOption appends the hostname option to the response, if it's
|
||||
// provided.
|
||||
//
|
||||
// TODO(e.burkov): Lease should always has a hostname, investigate when it
|
||||
// isn't the case.
|
||||
func appendHostnameOption(opts layers.DHCPOptions, hostname string) (res layers.DHCPOptions) {
|
||||
if hostname == "" {
|
||||
return opts
|
||||
@@ -377,26 +373,38 @@ func msg4Type(msg *layers.DHCPv4) (typ layers.DHCPMsgType, ok bool) {
|
||||
|
||||
// requestedIPv4 returns the IPv4 address, requested by client in the DHCP
|
||||
// message, if any.
|
||||
//
|
||||
// TODO(e.burkov): DRY with other IP-from-option helpers.
|
||||
func requestedIPv4(msg *layers.DHCPv4) (ip netip.Addr, ok bool) {
|
||||
for _, opt := range msg.Options {
|
||||
if opt.Type == layers.DHCPOptRequestIP && len(opt.Data) == net.IPv4len {
|
||||
return netip.AddrFromSlice(opt.Data)
|
||||
if opt.Type == layers.DHCPOptRequestIP {
|
||||
ip, ok = netip.AddrFromSlice(opt.Data)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
ip = ip.Unmap()
|
||||
if ok && ip.Is4() {
|
||||
return ip, true
|
||||
}
|
||||
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
|
||||
// serverID4 returns the server ID of the DHCP message, if any.
|
||||
func serverID4(msg *layers.DHCPv4) (ip netip.Addr, ok bool) {
|
||||
for _, opt := range msg.Options {
|
||||
if opt.Type == layers.DHCPOptServerID && len(opt.Data) == net.IPv4len {
|
||||
return netip.AddrFromSlice(opt.Data)
|
||||
if opt.Type == layers.DHCPOptServerID {
|
||||
ip, ok = netip.AddrFromSlice(opt.Data)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
ip = ip.Unmap()
|
||||
if ok && ip.Is4() {
|
||||
return ip, true
|
||||
}
|
||||
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
|
||||
|
||||
@@ -100,8 +100,6 @@ func (opt *IANAOption) UnmarshalBinary(data []byte) (err error) {
|
||||
|
||||
// Encode serializes opt into a DHCPv6 IA_NA option. Each contained
|
||||
// [IAAddrOption] is encoded as a nested IA Address option.
|
||||
//
|
||||
// TODO(e.burkov): Use.
|
||||
func (opt IANAOption) Encode() (iaOpt layers.DHCPv6Option) {
|
||||
// Each nested IA Address option: code (2) + length (2) + data (24).
|
||||
const nestedAddrSize = 2 + 2 + iaAddrDataLen
|
||||
|
||||
@@ -16,6 +16,8 @@ import (
|
||||
func TestDHCPServer_AddLease(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
outOfRangeAddr := testIPv4Conf.RangeEnd.Next()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
want *dhcpsvc.Lease
|
||||
@@ -24,10 +26,10 @@ func TestDHCPServer_AddLease(t *testing.T) {
|
||||
name: "outside_range",
|
||||
want: &dhcpsvc.Lease{
|
||||
Hostname: testLease4HostnameUnknown,
|
||||
IP: netip.MustParseAddr("1.2.3.4"),
|
||||
IP: outOfRangeAddr,
|
||||
HWAddr: testHWUnknown,
|
||||
},
|
||||
wantErrMsg: "adding lease: no interface for ip 1.2.3.4",
|
||||
wantErrMsg: "adding lease: no interface for ip " + outOfRangeAddr.String(),
|
||||
}, {
|
||||
name: "duplicate_ip",
|
||||
want: &dhcpsvc.Lease{
|
||||
|
||||
@@ -4,42 +4,42 @@
|
||||
"expires": "",
|
||||
"ip": "192.0.2.101",
|
||||
"hostname": "static4",
|
||||
"mac": "01:02:03:04:05:06",
|
||||
"mac": "00:00:5e:00:53:02",
|
||||
"static": true
|
||||
},
|
||||
{
|
||||
"expires": "2025-01-02T01:01:01Z",
|
||||
"ip": "192.0.2.102",
|
||||
"hostname": "dynamic4",
|
||||
"mac": "02:03:04:05:06:07",
|
||||
"mac": "00:00:5e:00:53:03",
|
||||
"static": false
|
||||
},
|
||||
{
|
||||
"expires": "2025-01-01T00:01:01Z",
|
||||
"ip": "192.0.2.103",
|
||||
"hostname": "expired4",
|
||||
"mac": "03:04:05:06:07:08",
|
||||
"mac": "00:00:5e:00:53:04",
|
||||
"static": false
|
||||
},
|
||||
{
|
||||
"expires": "",
|
||||
"ip": "2001:db8::65",
|
||||
"hostname": "static6",
|
||||
"mac": "01:02:03:04:05:06",
|
||||
"mac": "00:00:5e:00:53:02",
|
||||
"static": true
|
||||
},
|
||||
{
|
||||
"expires": "2025-01-02T01:01:01Z",
|
||||
"ip": "2001:db8::66",
|
||||
"hostname": "dynamic6",
|
||||
"mac": "02:03:04:05:06:07",
|
||||
"mac": "00:00:5e:00:53:03",
|
||||
"static": false
|
||||
},
|
||||
{
|
||||
"expires": "2025-01-01T00:01:01Z",
|
||||
"ip": "2001:db8::67",
|
||||
"hostname": "expired6",
|
||||
"mac": "03:04:05:06:07:08",
|
||||
"mac": "00:00:5e:00:53:04",
|
||||
"static": false
|
||||
}
|
||||
],
|
||||
|
||||
@@ -4,42 +4,42 @@
|
||||
"expires": "2025-01-02T01:01:01Z",
|
||||
"ip": "192.0.2.102",
|
||||
"hostname": "dynamic4",
|
||||
"mac": "02:03:04:05:06:07",
|
||||
"mac": "00:00:5e:00:53:03",
|
||||
"static": false
|
||||
},
|
||||
{
|
||||
"expires": "2025-01-02T01:01:01Z",
|
||||
"ip": "2001:db8::66",
|
||||
"hostname": "dynamic6",
|
||||
"mac": "02:03:04:05:06:07",
|
||||
"mac": "00:00:5e:00:53:03",
|
||||
"static": false
|
||||
},
|
||||
{
|
||||
"expires": "2025-01-01T00:01:01Z",
|
||||
"ip": "192.0.2.103",
|
||||
"hostname": "expired4",
|
||||
"mac": "03:04:05:06:07:08",
|
||||
"mac": "00:00:5e:00:53:04",
|
||||
"static": false
|
||||
},
|
||||
{
|
||||
"expires": "2025-01-01T00:01:01Z",
|
||||
"ip": "2001:db8::67",
|
||||
"hostname": "expired6",
|
||||
"mac": "03:04:05:06:07:08",
|
||||
"mac": "00:00:5e:00:53:04",
|
||||
"static": false
|
||||
},
|
||||
{
|
||||
"expires": "",
|
||||
"ip": "192.0.2.101",
|
||||
"hostname": "static4",
|
||||
"mac": "01:02:03:04:05:06",
|
||||
"mac": "00:00:5e:00:53:02",
|
||||
"static": true
|
||||
},
|
||||
{
|
||||
"expires": "",
|
||||
"ip": "2001:db8::65",
|
||||
"hostname": "static6",
|
||||
"mac": "01:02:03:04:05:06",
|
||||
"mac": "00:00:5e:00:53:02",
|
||||
"static": true
|
||||
}
|
||||
],
|
||||
|
||||
@@ -49,6 +49,9 @@ type IPv4Config struct {
|
||||
|
||||
// SubnetMask is the IPv4 subnet mask of the network. It should be a valid
|
||||
// IPv4 CIDR (i.e. all 1s followed by all 0s).
|
||||
//
|
||||
// TODO(e.burkov): Use the a unsinged integer type for the mask length
|
||||
// instead of [netip.Addr] to reduce the chance of misconfiguration.
|
||||
SubnetMask netip.Addr
|
||||
|
||||
// RangeStart is the first address in the range to assign to DHCP clients.
|
||||
@@ -98,28 +101,26 @@ func (c *IPv4Config) Validate() (err error) {
|
||||
}
|
||||
|
||||
// validateSubnet validates the subnet configuration.
|
||||
//
|
||||
// TODO(e.burkov): Use [validate].
|
||||
func (c *IPv4Config) validateSubnet(orig []error) (errs []error) {
|
||||
errs = orig
|
||||
|
||||
if !c.GatewayIP.Is4() {
|
||||
err := newMustErr("gateway ip", "be a valid ipv4", c.GatewayIP)
|
||||
err := fmt.Errorf("gateway ip: %s: must be a valid ipv4", c.GatewayIP)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if !c.SubnetMask.Is4() {
|
||||
err := newMustErr("subnet mask", "be a valid ipv4 cidr mask", c.SubnetMask)
|
||||
err := fmt.Errorf("subnet mask: %s: must be a valid ipv4 cidr mask", c.SubnetMask)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if !c.RangeStart.Is4() {
|
||||
err := newMustErr("range start", "be a valid ipv4", c.RangeStart)
|
||||
err := fmt.Errorf("range start: %s: must be a valid ipv4", c.RangeStart)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if !c.RangeEnd.Is4() {
|
||||
err := newMustErr("range end", "be a valid ipv4", c.RangeEnd)
|
||||
err := fmt.Errorf("range end: %s: must be a valid ipv4", c.RangeEnd)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
@@ -185,8 +186,6 @@ func (srv *DHCPServer) newDHCPInterfaceV4(
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO(e.burkov): Add a helper for converting [netip.Addr] to subnet mask
|
||||
// to [netutil].
|
||||
maskLen, _ := net.IPMask(conf.SubnetMask.AsSlice()).Size()
|
||||
|
||||
// Ignore the error since it's already checked in [IPv4Config.Validate].
|
||||
@@ -275,13 +274,14 @@ func (iface *dhcpInterfaceV4) respondACK(
|
||||
// present. req and resp must not be nil, fd must be valid.
|
||||
//
|
||||
// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.3.1.
|
||||
//
|
||||
// TODO(e.burkov): Add a message according to RFC 2131.
|
||||
func (iface *dhcpInterfaceV4) respondNAK(
|
||||
ctx context.Context,
|
||||
req *layers.DHCPv4,
|
||||
fd *frameData4,
|
||||
idOpt []byte,
|
||||
) {
|
||||
// TODO(e.burkov): According to RFC 2131 we should add a message.
|
||||
opts := newRespOptions(layers.DHCPMsgTypeNak, fd, idOpt)
|
||||
|
||||
// If 'giaddr' is set in the DHCPREQUEST message, the client is on a
|
||||
|
||||
+94
-19
@@ -422,7 +422,7 @@ func (iface *dhcpInterfaceV6) allocateForSolicit(
|
||||
return lease, iana.ID
|
||||
}
|
||||
|
||||
lease, err = iface.common.allocateLease(ctx, mac, iface.clock.Now())
|
||||
lease, err = iface.common.allocateLease(ctx, mac, key, iface.clock.Now())
|
||||
if err != nil {
|
||||
l.DebugContext(ctx, "no address available", "iaid", iana.ID, slogutil.KeyError, err)
|
||||
|
||||
@@ -541,7 +541,7 @@ func (iface *dhcpInterfaceV6) newSolicitRespOpts(
|
||||
|
||||
// newRequestRespOpts returns the common option list for Reply responses to a
|
||||
// Request message. fd, req, and cliID must not be nil. iana must be a valid
|
||||
// IA_NA option.
|
||||
// IA_NA option, or be empty if the response should not contain an IA_NA option.
|
||||
//
|
||||
// TODO(e.burkov): Keep the Reply option set aligned with the current Advertise
|
||||
// response shape until the wider DHCPv6 implementation is completed.
|
||||
@@ -575,14 +575,14 @@ func (iface *dhcpInterfaceV6) newRequestRespOpts(
|
||||
// See RFC 9915 Section 18.3.3.
|
||||
func (iface *dhcpInterfaceV6) newConfirmRespOpts(
|
||||
fd *frameData6,
|
||||
req *layers.DHCPv6,
|
||||
cliID *layers.DHCPv6DUID,
|
||||
status layers.DHCPv6StatusCode,
|
||||
) (opts layers.DHCPv6Options) {
|
||||
opts = append(
|
||||
opts,
|
||||
opts = layers.DHCPv6Options{
|
||||
layers.NewDHCPv6Option(layers.DHCPv6OptServerID, fd.duidData),
|
||||
layers.NewDHCPv6Option(layers.DHCPv6OptClientID, cliID.Encode()),
|
||||
)
|
||||
}
|
||||
|
||||
// If the Status Code option does not appear in a message in which the
|
||||
// option could appear, the status of the message is assumed to be Success.
|
||||
@@ -592,7 +592,37 @@ func (iface *dhcpInterfaceV6) newConfirmRespOpts(
|
||||
opts = append(opts, newStatusCodeOption(status))
|
||||
}
|
||||
|
||||
return opts
|
||||
return iface.appendRequestedOptions(opts, req)
|
||||
}
|
||||
|
||||
// newRenewRespOpts returns the common option list for Reply responses to a
|
||||
// Renew message. fd, req, and cliID must not be nil. iana must be a valid
|
||||
// IA_NA option, or be empty if the response should not contain an IA_NA option.
|
||||
//
|
||||
// See RFC 9915 Section 18.3.4.
|
||||
//
|
||||
// TODO(e.burkov): DRY with other response option builders.
|
||||
func (iface *dhcpInterfaceV6) newRenewRespOpts(
|
||||
fd *frameData6,
|
||||
req *layers.DHCPv6,
|
||||
cliID *layers.DHCPv6DUID,
|
||||
iana layers.DHCPv6Option,
|
||||
) (opts layers.DHCPv6Options) {
|
||||
opts = append(opts, layers.NewDHCPv6Option(layers.DHCPv6OptServerID, fd.duidData))
|
||||
opts = append(opts, layers.NewDHCPv6Option(layers.DHCPv6OptClientID, cliID.Encode()))
|
||||
|
||||
if iana.Code != 0 {
|
||||
opts = append(opts, iana)
|
||||
}
|
||||
|
||||
// The server preference value MUST default to 0 unless otherwise configured
|
||||
// by the server administrator.
|
||||
//
|
||||
// See RFC 9915 Section 18.3.9.
|
||||
opts = append(opts, newPreferenceOption(0))
|
||||
opts = append(opts, newSOLMaxRTOption(DefaultSolMaxRT))
|
||||
|
||||
return iface.appendRequestedOptions(opts, req)
|
||||
}
|
||||
|
||||
// iaNAFromLease returns an IA_NA option with a single IA Address sub-option
|
||||
@@ -616,34 +646,79 @@ func (iface *dhcpInterfaceV6) iaNAFromLease(lease *Lease, iaid uint32) (iana lay
|
||||
}.Encode()
|
||||
}
|
||||
|
||||
// leaseForRequest returns the committed lease for req. It reuses an already
|
||||
// reserved lease for the client when possible, or allocates and commits the new
|
||||
// address. iface.common.indexMu must be locked.
|
||||
func (iface *dhcpInterfaceV6) leaseForRequest(
|
||||
// ianaForRequest returns the IANA filled with committed lease data for req. It
|
||||
// reuses an already reserved lease for the client when possible, or allocates
|
||||
// and commits the new address. req must be a valid DHCPv6 message of type
|
||||
// REQUEST, iaid must not be zero, and mac must be a valid MAC address according
|
||||
// to [netutil.ValidateMAC]. iface.common.indexMu must be locked.
|
||||
func (iface *dhcpInterfaceV6) ianaForRequest(
|
||||
ctx context.Context,
|
||||
req *layers.DHCPv6,
|
||||
iaid uint32,
|
||||
mac net.HardwareAddr,
|
||||
) (lease *Lease, err error) {
|
||||
) (iana layers.DHCPv6Option) {
|
||||
key := macToKey(mac)
|
||||
l := iface.common.logger
|
||||
|
||||
lease, ok := iface.common.leases[key]
|
||||
if !ok {
|
||||
lease, err = iface.common.allocateLease(ctx, mac, iface.clock.Now())
|
||||
var err error
|
||||
lease, err = iface.common.allocateLease(ctx, mac, key, iface.clock.Now())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("allocating lease for mac %s: %w", mac, err)
|
||||
}
|
||||
}
|
||||
l.ErrorContext(ctx, "allocating lease", slogutil.KeyError, err)
|
||||
|
||||
err = iface.commit(ctx, req, lease)
|
||||
if err != nil {
|
||||
return newIANAWithStatus(iaid, layers.DHCPv6StatusCodeNoAddrsAvail)
|
||||
}
|
||||
} else if err := iface.commit(ctx, req, lease); err != nil {
|
||||
l.WarnContext(ctx, "committing lease", slogutil.KeyError, err)
|
||||
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return nil, err
|
||||
return newIANAWithStatus(iaid, layers.DHCPv6StatusCodeNoAddrsAvail)
|
||||
}
|
||||
|
||||
return lease, nil
|
||||
return iface.iaNAFromLease(lease, iaid)
|
||||
}
|
||||
|
||||
// ianaForRenew returns the IANA filled with committed lease data for req. It
|
||||
// reuses an already reserved lease for the client, if it exists. req must be a
|
||||
// valid DHCPv6 message of type RENEW, iaid must not be zero, and mac must be a
|
||||
// valid MAC address according to [netutil.ValidateMAC]. iface.common.indexMu
|
||||
// must be locked.
|
||||
func (iface *dhcpInterfaceV6) ianaForRenew(
|
||||
ctx context.Context,
|
||||
req *layers.DHCPv6,
|
||||
reqIANA *IANAOption,
|
||||
mac net.HardwareAddr,
|
||||
) (iana layers.DHCPv6Option) {
|
||||
key := macToKey(mac)
|
||||
l := iface.common.logger
|
||||
|
||||
reqIP, hasReqIP := reqIANA.requestedAddr()
|
||||
if !hasReqIP {
|
||||
// With no requested addresses there's nothing to renew. Respond with
|
||||
// no IA options similarly to how the Request handler does.
|
||||
//
|
||||
// See RFC 9915 Section 18.3.4.
|
||||
return layers.DHCPv6Option{}
|
||||
}
|
||||
|
||||
lease, hasLease := iface.common.leases[key]
|
||||
if !hasLease || lease.IP != reqIP {
|
||||
// No binding found for this client. The server returns the IA with a
|
||||
// NoBinding status code.
|
||||
//
|
||||
// See RFC 9915 Section 18.3.4.
|
||||
return layers.DHCPv6Option{}
|
||||
}
|
||||
|
||||
err := iface.commit(ctx, req, lease)
|
||||
if err != nil {
|
||||
l.WarnContext(ctx, "committing lease", slogutil.KeyError, err)
|
||||
|
||||
return newIANAWithStatus(reqIANA.ID, layers.DHCPv6StatusCodeNoAddrsAvail)
|
||||
}
|
||||
|
||||
return iface.iaNAFromLease(lease, reqIANA.ID)
|
||||
}
|
||||
|
||||
// commit updates the lease allocated previously via a SOLICIT, or during
|
||||
|
||||
Reference in New Issue
Block a user