Pull request 2727: AGDNS-3863-gopacket-dhcp-vol.30

Updates #4923.

Squashed commit of the following:

commit 26f8b000f83888c873ae3bf959636d4e95c58f01
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Thu Jul 23 16:37:14 2026 +0300

    dhcpsvc: imp code

commit 76d4f8997d
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Jul 21 14:25:14 2026 +0300

    dhcpsvc: imp tests
This commit is contained in:
Eugene Burkov 2026-07-23 14:13:45 +00:00
parent f078919e22
commit 599cfa9eb5
31 changed files with 840 additions and 927 deletions

View file

@ -7,11 +7,16 @@ import (
// Database is the interface for storing DHCP leases.
type Database interface {
// Load loads leases from the database. If err is not nil, leases must be
// nil. It must be safe for concurrent use.
// nil. Leases must not be modified.
//
// Implementations must document the sorting order, if the one is defined.
// It must be safe for concurrent use.
Load(ctx context.Context) (leases []*Lease, err error)
// Store stores leases to the database. leases must be valid. It must be
// safe for concurrent use.
// Store stores leases to the database. leases must not be nil and all its
// elements must be valid. leases must not be modified until Store returns.
//
// It must be safe for concurrent use.
Store(ctx context.Context, leases []*Lease) (err error)
}

View file

@ -3,12 +3,9 @@ package dhcpsvc_test
import (
"cmp"
"context"
"io/fs"
"net"
"net/netip"
"os"
"path"
"path/filepath"
"slices"
"testing"
"time"
@ -33,9 +30,6 @@ const testLocalTLD = "local"
// testIfaceName is the name of the test network interface.
const testIfaceName = "iface0"
// testDBLeasesFilename is the common name of a leases database file for tests.
const testDBLeasesFilename = "leases.json"
// testTimeout is a common timeout for tests and contexts.
const testTimeout = 10 * time.Second
@ -45,9 +39,6 @@ const testLeaseTTL = 24 * time.Hour
// testLogger is a common logger for tests.
var testLogger = slogutil.NewDiscardLogger()
// testdata is a filesystem containing data for tests.
var testdata = os.DirFS("testdata")
// testCurrentTime is the fixed time returned by [testClock] to ensure
// reproducible tests.
var testCurrentTime = time.Date(2025, 1, 1, 1, 1, 1, 0, time.UTC)
@ -59,6 +50,40 @@ var testClock = &faketime.Clock{
},
}
// Lease hostnames for test cases.
//
// NOTE: Keep in sync with testdata.
const (
// testLease4HostnameUnknown is the test hostname for an unknown DHCPv4
// lease.
testLease4HostnameUnknown = "unknown4"
// testLease4HostnameStatic is the test hostname for a static DHCPv4 lease.
testLease4HostnameStatic = "static4"
// testLease4HostnameDynamic is the test hostname for a dynamic DHCPv4
// lease.
testLease4HostnameDynamic = "dynamic4"
// testLease4HostnameExpired is the test hostname for an expired DHCPv4
// lease.
testLease4HostnameExpired = "expired4"
// testLease6HostnameUnknown is the test hostname for an unknown DHCPv6
// lease.
testLease6HostnameUnknown = "unknown6"
// testLease6HostnameStatic is the test hostname for a static DHCPv6 lease.
testLease6HostnameStatic = "static6"
// testLease6HostnameDynamic is the test hostname for a dynamic DHCPv6 lease.
testLease6HostnameDynamic = "dynamic6"
// testLease6HostnameExpired is the test hostname for an expired DHCPv6
// lease.
testLease6HostnameExpired = "expired6"
)
const (
// testGatewayIPv4Str is the string representation of the gateway IPv4
// address used in tests.
@ -173,13 +198,13 @@ var testInterfaceConf = map[string]*dhcpsvc.InterfaceConfig{
SubnetMask: netip.MustParseAddr(testAnotherSubnetMaskV4Str),
RangeStart: netip.MustParseAddr(testAnotherRangeStartV4Str),
RangeEnd: netip.MustParseAddr(testAnotherRangeEndV4Str),
LeaseDuration: 1 * time.Hour,
LeaseDuration: testLeaseTTL,
},
IPv6: &dhcpsvc.IPv6Config{
Enabled: true,
Clock: timeutil.SystemClock{},
RangeStart: netip.MustParseAddr(testAnotherRangeStartV6Str),
LeaseDuration: 1 * time.Hour,
LeaseDuration: testLeaseTTL,
RAAllowSLAAC: true,
RASLAACOnly: true,
},
@ -206,18 +231,126 @@ var (
testHWAnother = net.HardwareAddr{0x4, 0x5, 0x6, 0x7, 0x8, 0x9}
)
// IPv4 addresses for tests.
//
// NOTE: Keep in sync with testdata.
var (
// testIPv4Unknown is the test IP address for an unknown client.
testIPv4Unknown = netip.MustParseAddr("192.0.2.142")
// testIPv4Static is the test IP address for a known static lease.
testIPv4Static = netip.MustParseAddr("192.0.2.101")
// testIPv4Dynamic is the test IP address for a known dynamic lease.
testIPv4Dynamic = netip.MustParseAddr("192.0.2.102")
// testIPv4Expired is the test IP address for a known expired lease.
testIPv4Expired = netip.MustParseAddr("192.0.2.103")
// testIPv4OtherSubnet is the test IP address for a client on another
// subnet.
testIPv4OtherSubnet = netip.MustParseAddr(testAnotherGatewayIPv4Str)
// testIPv4RelayAgent is the test IP address of the relay agent.
testIPv4RelayAgent = netip.MustParseAddr("10.0.0.1")
)
// IPv6 addresses for tests.
//
// NOTE: Keep in sync with testdata.
var (
// testIPv6Unknown is the test IP address for an unknown client.
testIPv6Unknown = netip.MustParseAddr("2001:db8::64")
// testIPv6Dynamic is the test IP address for a known dynamic lease.
testIPv6Dynamic = netip.MustParseAddr("2001:db8::66")
// testIPv6Expired is the test IP address for a known expired lease.
testIPv6Expired = netip.MustParseAddr("2001:db8::67")
// testIPv6Static is the test IP address for a known static lease.
testIPv6Static = netip.MustParseAddr("2001:db8::65")
)
// Time-related variables for test cases.
//
// NOTE: Keep in sync with testdata.
var (
// testExpiryDynamicLease is the test expiry time for a dynamic lease.
testExpiryDynamicLease = time.Date(2025, 1, 1, 10, 1, 1, 0, time.UTC)
// testExpiryDynamicLease is the test expiry time for a dynamic lease, not
// yet expired according to [testClock].
testExpiryDynamicLease = testCurrentTime.Add(testLeaseTTL)
// testTTLDynamicLease is the test TTL for the dynamic lease.
testTTLDynamicLease = testExpiryDynamicLease.Sub(testCurrentTime)
// testExpiryExpiredLease is the test expiry time for an expired lease
// according to [testClock].
testExpiryExpiredLease = testCurrentTime.Add(-time.Hour)
)
// TODO(e.burkov): Add a default lease expiry time, according to
// [testCurrentTime].
var (
// testLease4Dynamic is a common valid dynamic DHCPv4 lease for tests.
testLease4Dynamic = &dhcpsvc.Lease{
IP: testIPv4Dynamic,
HWAddr: testHWDynamic,
Expiry: testExpiryDynamicLease,
Hostname: testLease4HostnameDynamic,
IsStatic: false,
}
// testLease4Static is a common valid static DHCPv4 lease for tests.
testLease4Static = &dhcpsvc.Lease{
IP: testIPv4Static,
HWAddr: testHWStatic,
Expiry: time.Time{},
Hostname: testLease4HostnameStatic,
IsStatic: true,
}
// testLease4Expired is a common expired DHCPv4 lease for tests.
testLease4Expired = &dhcpsvc.Lease{
IP: testIPv4Expired,
HWAddr: testHWExpired,
Expiry: testExpiryExpiredLease,
Hostname: testLease4HostnameExpired,
IsStatic: false,
}
// testLease6Dynamic is a common valid dynamic DHCPv6 lease for tests.
testLease6Dynamic = &dhcpsvc.Lease{
IP: testIPv6Dynamic,
HWAddr: testHWDynamic,
Expiry: testExpiryDynamicLease,
Hostname: testLease6HostnameDynamic,
IsStatic: false,
}
// testLease6Static is a common valid static DHCPv6 lease for tests.
testLease6Static = &dhcpsvc.Lease{
IP: testIPv6Static,
HWAddr: testHWStatic,
Expiry: time.Time{},
Hostname: testLease6HostnameStatic,
IsStatic: true,
}
// testLease6Expired is a common expired DHCPv6 lease for tests.
testLease6Expired = &dhcpsvc.Lease{
IP: testIPv6Expired,
HWAddr: testHWExpired,
Expiry: testExpiryExpiredLease,
Hostname: testLease6HostnameExpired,
IsStatic: false,
}
// testLeases4 is a common set of leases for tests, containing only IPv4
// leases.
testLeases4 = []*dhcpsvc.Lease{testLease4Dynamic, testLease4Expired, testLease4Static}
// testLeases6 is a common set of leases for tests, containing only IPv6
// leases.
testLeases6 = []*dhcpsvc.Lease{testLease6Dynamic, testLease6Expired, testLease6Static}
// testLeases is a set of leases for tests, containing both IPv4 and IPv6
// leases.
testLeases = slices.Concat(testLeases4, testLeases6)
)
// fullLayersStack4 is the complete stack of layers expected to appear in the
@ -240,6 +373,8 @@ var fullLayersStack6 = []gopacket.LayerType{
// testDatabase is a mock implementation of the [dhcpsvc.Database] interface for
// tests.
//
// TODO(e.burkov): Consider moving to aghtest.
type testDatabase struct {
onLoad func(ctx context.Context) (leases []*dhcpsvc.Lease, err error)
onStore func(ctx context.Context, leases []*dhcpsvc.Lease) (err error)
@ -258,45 +393,27 @@ func (db *testDatabase) Store(ctx context.Context, leases []*dhcpsvc.Lease) (err
return db.onStore(ctx, leases)
}
// newTestDatabase copies the leases database file located in the testdata FS,
// under tb.Name()/leases.json, to a temporary directory and constructs a
// [*testDatabase] that properly loads the leases from that file.
//
// TODO(e.burkov): Move leases from testdata to the literals in tests, and
// improve this helper.
func newTestDatabase(tb testing.TB) (db *testDatabase) {
// newTestDatabase creates a new *testDatabase for testing. If initial is not
// nil, db.Load is set to return it. By default, db.Store panics on any call.
func newTestDatabase(tb testing.TB, initial []*dhcpsvc.Lease) (db *testDatabase) {
tb.Helper()
data, err := fs.ReadFile(testdata, path.Join(tb.Name(), testDBLeasesFilename))
require.NoError(tb, err)
dst := filepath.Join(tb.TempDir(), testDBLeasesFilename)
require.NoError(tb, os.WriteFile(dst, data, dhcpsvc.JSONDatabasePerm))
jsonDB := dhcpsvc.NewJSONDatabase(&dhcpsvc.JSONDatabaseConfig{
Logger: testLogger,
FilePath: dst,
})
db = newPanicDatabase(tb)
db.onLoad = jsonDB.Load
return db
}
// newPanicDatabase returns a *testDatabase that panics on any call to its
// methods.
func newPanicDatabase(tb testing.TB) (db *testDatabase) {
tb.Helper()
return &testDatabase{
onLoad: func(ctx context.Context) (leases []*dhcpsvc.Lease, err error) {
db = &testDatabase{
onLoad: func(ctx context.Context) (_ []*dhcpsvc.Lease, _ error) {
panic(testutil.UnexpectedCall(ctx))
},
onStore: func(ctx context.Context, leases []*dhcpsvc.Lease) (err error) {
onStore: func(ctx context.Context, leases []*dhcpsvc.Lease) (_ error) {
panic(testutil.UnexpectedCall(ctx, leases))
},
}
if initial != nil {
db.onLoad = func(ctx context.Context) (leases []*dhcpsvc.Lease, err error) {
return initial, nil
}
}
return db
}
// newTestDHCPServer creates a new DHCPServer for testing. It uses the default

View file

@ -1,6 +1,7 @@
package dhcpsvc
import (
"cmp"
"context"
"fmt"
"net/netip"
@ -303,7 +304,17 @@ func (iface *dhcpInterfaceV4) handleInitReboot(
return
}
iface.updateAndRespond(ctx, l, req, lease, fd, idOpt)
lease.Hostname = cmp.Or(hostname4(req), lease.Hostname)
err := iface.updateLease(ctx, lease)
if err != nil {
l.ErrorContext(ctx, "init-reboot request failed", slogutil.KeyError, err)
iface.respondNAK(ctx, req, fd, idOpt)
return
}
iface.respondACK(ctx, req, fd, lease, idOpt)
}
// handleRenew handles messages of type DHCPREQUEST in RENEWING or REBINDING
@ -342,7 +353,18 @@ func (iface *dhcpInterfaceV4) handleRenew(
return
}
iface.updateAndRespond(ctx, l, req, lease, fd, idOpt)
lease.Hostname = cmp.Or(hostname4(req), lease.Hostname)
err := iface.updateLease(ctx, lease)
if err != nil {
l.ErrorContext(ctx, "renew request failed", slogutil.KeyError, err)
iface.respondNAK(ctx, req, fd, idOpt)
return
}
iface.respondACK(ctx, req, fd, lease, idOpt)
}
// handleDecline handles messages of type DHCPDECLINE. req must be a

View file

@ -6,7 +6,6 @@ import (
"net"
"net/netip"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/golibs/testutil"
@ -27,48 +26,11 @@ var testIPv4InterfacesConf = map[string]*dhcpsvc.InterfaceConfig{
},
}
// Lease hostnames for test cases.
//
// NOTE: Keep in sync with testdata.
const (
// testLease4HostnameStatic is the test hostname for a static DHCPv4 lease.
testLease4HostnameStatic = "static4"
// testLease4HostnameDynamic is the test hostname for a dynamic DHCPv4
// lease.
testLease4HostnameDynamic = "dynamic4"
// testLease4HostnameExpired is the test hostname for an expired DHCPv4
// lease.
testLease4HostnameExpired = "expired4"
)
// testXid is a common transaction ID for DHCPv4 tests.
//
// TODO(e.burkov): Generate unique IDs when they will be actually used.
const testXid = 1
// IP addresses for test cases.
//
// NOTE: Keep in sync with testdata.
var (
// testIPv4Unknown is the test IP address for an unknown client.
testIPv4Unknown = netip.MustParseAddr("192.0.2.142")
// testIPv4Static is the test IP address for a known static lease.
testIPv4Static = netip.MustParseAddr("192.0.2.101")
// testIPv4Dynamic is the test IP address for a known dynamic lease.
testIPv4Dynamic = netip.MustParseAddr("192.0.2.102")
// testIPv4OtherSubnet is the test IP address for a client on another
// subnet.
testIPv4OtherSubnet = netip.MustParseAddr(testAnotherGatewayIPv4Str)
// testIPv4RelayAgent is the test IP address of the relay agent.
testIPv4RelayAgent = netip.MustParseAddr("10.0.0.1")
)
func TestDHCPServer_ServeEther4_discover(t *testing.T) {
t.Parallel()
@ -99,7 +61,7 @@ func TestDHCPServer_ServeEther4_discover(t *testing.T) {
wantOpts: layers.DHCPOptions{
newOptMessageType(t, layers.DHCPMsgTypeOffer),
newOptServerID(t, testIfaceAddrV4),
newOptLeaseTime(t, testTTLDynamicLease),
newOptLeaseTime(t, testLeaseTTL),
newOptHostname(t, testLease4HostnameDynamic),
},
}, {
@ -116,14 +78,12 @@ func TestDHCPServer_ServeEther4_discover(t *testing.T) {
for _, tc := range testCases {
req := testutil.RequireTypeAssert[*layers.DHCPv4](t, tc.in.Layer(layers.LayerTypeDHCPv4))
db := newTestDatabase(t)
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceAddrV4)
startTestDHCPServer(t, &dhcpsvc.Config{
Database: db,
Database: newTestDatabase(t, testLeases),
Interfaces: testIPv4InterfacesConf,
NetworkDeviceManager: ndMgr,
Enabled: true,
@ -145,7 +105,7 @@ func TestDHCPServer_ServeEther4_discoverExpired(t *testing.T) {
ndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceAddrV4)
startTestDHCPServer(t, &dhcpsvc.Config{
Database: newTestDatabase(t),
Database: newTestDatabase(t, testLeases),
Interfaces: testIPv4InterfacesConf,
NetworkDeviceManager: ndMgr,
Enabled: true,
@ -164,8 +124,6 @@ func TestDHCPServer_ServeEther4_discoverExpired(t *testing.T) {
func TestDHCPServer_ServeEther4_release(t *testing.T) {
t.Parallel()
ipMismatch := testIPv4Dynamic.Next().Next()
testCases := []struct {
req gopacket.Packet
want *dhcpsvc.Lease
@ -175,7 +133,7 @@ func TestDHCPServer_ServeEther4_release(t *testing.T) {
want: &dhcpsvc.Lease{
IP: testIPv4Dynamic,
HWAddr: testHWDynamic,
Expiry: testClock.Now().Add(testLeaseTTL),
Expiry: testExpiryDynamicLease,
Hostname: testLease4HostnameDynamic,
IsStatic: false,
},
@ -185,7 +143,7 @@ func TestDHCPServer_ServeEther4_release(t *testing.T) {
want: nil,
name: "not_found",
}, {
req: newDHCPRELEASE(t, testHWAnother, ipMismatch),
req: newDHCPRELEASE(t, testHWDynamic, testIPv4Unknown),
want: nil,
name: "mismatch_ip",
}, {
@ -195,11 +153,11 @@ func TestDHCPServer_ServeEther4_release(t *testing.T) {
}}
for _, tc := range testCases {
db := newTestDatabase(t)
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
db := newTestDatabase(t, testLeases)
onStore := func(ctx context.Context, leases []*dhcpsvc.Lease) (err error) {
assert.NotContains(testutil.NewPanicT(t), leases, tc.want)
@ -223,7 +181,6 @@ func TestDHCPServer_ServeEther4_release(t *testing.T) {
}
}
// TODO(e.burkov): Increase maintainability index.
func TestDHCPServer_ServeEther4_requestSelecting(t *testing.T) {
t.Parallel()
@ -246,7 +203,7 @@ func TestDHCPServer_ServeEther4_requestSelecting(t *testing.T) {
want: &dhcpsvc.Lease{
IP: testIPv4Conf.RangeStart,
HWAddr: testHWUnknown,
Expiry: testClock.Now().Add(testLeaseTTL),
Expiry: testExpiryDynamicLease,
Hostname: "",
IsStatic: false,
},
@ -269,22 +226,6 @@ func TestDHCPServer_ServeEther4_requestSelecting(t *testing.T) {
want: nil,
name: "wrong_server_id",
wantOpts: nil,
}, {
discover: nil,
request: newDHCPREQUEST(t, &dhcpRequestConfig{
options: layers.DHCPOptions{
newOptRequestIP(t, testIPv4Conf.RangeEnd.Next()),
newOptServerID(t, testIfaceAddrV4),
},
clientHWAddr: testHWUnknown,
flags: dhcpsvc.FlagsBroadcast,
}),
want: nil,
name: "no_lease",
wantOpts: layers.DHCPOptions{
newOptMessageType(t, layers.DHCPMsgTypeNak),
newOptServerID(t, testIfaceAddrV4),
},
}, {
discover: newDHCPDISCOVER(t, testHWStatic),
request: newDHCPREQUEST(t, &dhcpRequestConfig{
@ -318,11 +259,11 @@ func TestDHCPServer_ServeEther4_requestSelecting(t *testing.T) {
}}
for _, tc := range testCases {
db := newTestDatabase(t)
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
db := newTestDatabase(t, testLeases)
onStore := func(ctx context.Context, leases []*dhcpsvc.Lease) (err error) {
assert.Contains(t, leases, tc.want)
@ -342,12 +283,10 @@ func TestDHCPServer_ServeEther4_requestSelecting(t *testing.T) {
Enabled: true,
})
if tc.discover != nil {
testutil.RequireSend(t, inCh, tc.discover, testTimeout)
testutil.RequireSend(t, inCh, tc.discover, testTimeout)
_, ok := testutil.RequireReceive(t, outCh, testTimeout)
require.True(t, ok)
}
_, ok := testutil.RequireReceive(t, outCh, testTimeout)
require.True(t, ok)
if tc.wantOpts == nil {
dev.onWritePacketData = unexpectedWritePacketData
@ -360,6 +299,38 @@ func TestDHCPServer_ServeEther4_requestSelecting(t *testing.T) {
}
}
func TestDHCPServer_ServeEther4_requestSelectingNoLease(t *testing.T) {
t.Parallel()
request := newDHCPREQUEST(t, &dhcpRequestConfig{
options: layers.DHCPOptions{
newOptRequestIP(t, testIPv4Conf.RangeEnd.Next()),
newOptServerID(t, testIfaceAddrV4),
},
clientHWAddr: testHWUnknown,
flags: dhcpsvc.FlagsBroadcast,
})
db := newTestDatabase(t, testLeases)
ndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceAddrV4)
startTestDHCPServer(t, &dhcpsvc.Config{
Database: db,
Interfaces: testIPv4InterfacesConf,
Logger: testLogger,
NetworkDeviceManager: ndMgr,
Enabled: true,
})
testutil.RequireSend(t, inCh, request, testTimeout)
assertValidResponse4(t, dhcpv4FromPacket(t, request), outCh, layers.DHCPOptions{
newOptMessageType(t, layers.DHCPMsgTypeNak),
newOptServerID(t, testIfaceAddrV4),
})
}
// TODO(e.burkov): Test expired.
func TestDHCPServer_ServeEther4_requestInitReboot(t *testing.T) {
t.Parallel()
@ -374,13 +345,7 @@ func TestDHCPServer_ServeEther4_requestInitReboot(t *testing.T) {
clientHWAddr: testHWStatic,
flags: dhcpsvc.FlagsBroadcast,
}),
want: &dhcpsvc.Lease{
IP: testIPv4Static,
HWAddr: testHWStatic,
Expiry: time.Time{},
Hostname: testLease4HostnameStatic,
IsStatic: true,
},
want: testLease4Static,
name: "success",
wantOpts: layers.DHCPOptions{
newOptMessageType(t, layers.DHCPMsgTypeAck),
@ -445,11 +410,11 @@ func TestDHCPServer_ServeEther4_requestInitReboot(t *testing.T) {
}}
for _, tc := range testCases {
db := newTestDatabase(t)
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
db := newTestDatabase(t, testLeases)
onStore := func(ctx context.Context, leases []*dhcpsvc.Lease) (err error) {
assert.Contains(t, leases, tc.want)
@ -493,18 +458,12 @@ func TestDHCPServer_ServeEther4_requestRenewSuccess(t *testing.T) {
clientIP: testIPv4Dynamic,
flags: dhcpsvc.FlagsBroadcast,
}),
want: &dhcpsvc.Lease{
IP: testIPv4Dynamic,
Expiry: testExpiryDynamicLease,
Hostname: "dynamic4",
HWAddr: testHWDynamic,
IsStatic: false,
},
want: testLease4Dynamic,
name: "success",
wantOpts: layers.DHCPOptions{
newOptMessageType(t, layers.DHCPMsgTypeAck),
newOptServerID(t, testIfaceAddrV4),
newOptLeaseTime(t, testTTLDynamicLease),
newOptLeaseTime(t, testLeaseTTL),
newOptHostname(t, testLease4HostnameDynamic),
},
}, {
@ -513,13 +472,7 @@ func TestDHCPServer_ServeEther4_requestRenewSuccess(t *testing.T) {
clientIP: testIPv4Static,
flags: dhcpsvc.FlagsBroadcast,
}),
want: &dhcpsvc.Lease{
IP: testIPv4Static,
Expiry: time.Time{},
Hostname: "static4",
HWAddr: testHWStatic,
IsStatic: true,
},
want: testLease4Static,
name: "static",
wantOpts: layers.DHCPOptions{
newOptMessageType(t, layers.DHCPMsgTypeAck),
@ -533,18 +486,12 @@ func TestDHCPServer_ServeEther4_requestRenewSuccess(t *testing.T) {
clientIP: testIPv4Dynamic,
relayAgentIP: testIPv4RelayAgent,
}),
want: &dhcpsvc.Lease{
IP: testIPv4Dynamic,
HWAddr: testHWDynamic,
Expiry: testExpiryDynamicLease,
Hostname: "dynamic4",
IsStatic: false,
},
want: testLease4Dynamic,
name: "relay_agent",
wantOpts: layers.DHCPOptions{
newOptMessageType(t, layers.DHCPMsgTypeAck),
newOptServerID(t, testIfaceAddrV4),
newOptLeaseTime(t, testTTLDynamicLease),
newOptLeaseTime(t, testLeaseTTL),
newOptHostname(t, testLease4HostnameDynamic),
},
}, {
@ -552,28 +499,22 @@ func TestDHCPServer_ServeEther4_requestRenewSuccess(t *testing.T) {
clientHWAddr: testHWDynamic,
clientIP: testIPv4Dynamic,
}),
want: &dhcpsvc.Lease{
IP: testIPv4Dynamic,
HWAddr: testHWDynamic,
Expiry: testExpiryDynamicLease,
Hostname: "dynamic4",
IsStatic: false,
},
want: testLease4Dynamic,
name: "ciaddr_unicast",
wantOpts: layers.DHCPOptions{
newOptMessageType(t, layers.DHCPMsgTypeAck),
newOptServerID(t, testIfaceAddrV4),
newOptLeaseTime(t, testTTLDynamicLease),
newOptLeaseTime(t, testLeaseTTL),
newOptHostname(t, testLease4HostnameDynamic),
},
}}
for _, tc := range testCases {
db := newTestDatabase(t)
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
db := newTestDatabase(t, testLeases)
onStore := func(ctx context.Context, leases []*dhcpsvc.Lease) (err error) {
assert.Contains(t, leases, tc.want)
@ -636,8 +577,6 @@ func TestDHCPServer_ServeEther4_requestRenewFail(t *testing.T) {
}}
for _, tc := range testCases {
db := newTestDatabase(t)
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
@ -647,7 +586,7 @@ func TestDHCPServer_ServeEther4_requestRenewFail(t *testing.T) {
}
startTestDHCPServer(t, &dhcpsvc.Config{
Database: db,
Database: newTestDatabase(t, testLeases),
Interfaces: testIPv4InterfacesConf,
NetworkDeviceManager: ndMgr,
Enabled: true,
@ -690,12 +629,10 @@ func TestDHCPServer_ServeEther4_decline(t *testing.T) {
}}
for _, tc := range testCases {
db := newTestDatabase(t)
// The lease should be blocked.
wantLease := &dhcpsvc.Lease{
IP: tc.want,
Expiry: testClock.Now().Add(testLeaseTTL),
Expiry: testExpiryDynamicLease,
Hostname: "",
HWAddr: dhcpsvc.BlockedHardwareAddr,
IsStatic: false,
@ -704,6 +641,8 @@ func TestDHCPServer_ServeEther4_decline(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
db := newTestDatabase(t, testLeases)
onStore := func(ctx context.Context, leases []*dhcpsvc.Lease) (err error) {
assert.Contains(t, leases, wantLease)

View file

@ -6,7 +6,6 @@ import (
"net/netip"
"slices"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
@ -37,35 +36,15 @@ const testIAID = 1
// TODO(e.burkov): Generate unique IDs when they will be actually used.
var testTransactionID = []byte{0x01, 0x02, 0x03}
// IP addresses for test cases.
//
// NOTE: Keep in sync with testdata.
var (
// testIPv6Unknown is the test IP address for an unknown client.
testIPv6Unknown = netip.MustParseAddr("2001:db8::64")
// testIPv6Dynamic is the test IP address for a known dynamic lease.
testIPv6Dynamic = netip.MustParseAddr("2001:db8::66")
// testIPv6Expired is the test IP address for a known expired lease.
testIPv6Expired = netip.MustParseAddr("2001:db8::67")
// testIPv6Static is the test IP address for a known static lease.
testIPv6Static = netip.MustParseAddr("2001:db8::65")
)
// TODO(e.burkov): Increase maintainability index.
func TestDHCPServer_ServeEther6_solicit(t *testing.T) {
t.Parallel()
testCases := []struct {
in gopacket.Packet
want *dhcpsvc.Lease
name string
wantOpts layers.DHCPv6Options
}{{
in: newDHCPv6SOLICIT(t, testHWUnknown, testIPv6Unknown, false),
want: nil,
name: "new",
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
@ -76,7 +55,6 @@ func TestDHCPServer_ServeEther6_solicit(t *testing.T) {
},
}, {
in: newDHCPv6SOLICIT(t, testHWStatic, testIPv6Static, false),
want: nil,
name: "existing_static",
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
@ -87,7 +65,6 @@ func TestDHCPServer_ServeEther6_solicit(t *testing.T) {
},
}, {
in: newDHCPv6SOLICIT(t, testHWDynamic, testIPv6Dynamic, false),
want: nil,
name: "existing_dynamic",
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
@ -98,7 +75,6 @@ func TestDHCPServer_ServeEther6_solicit(t *testing.T) {
},
}, {
in: newDHCPv6SOLICIT(t, testHWExpired, testIPv6Expired, false),
want: nil,
name: "existing_expired",
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
@ -107,16 +83,50 @@ func TestDHCPServer_ServeEther6_solicit(t *testing.T) {
newOptPreference(t, 0),
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
},
}, {
}}
for _, tc := range testCases {
req := testutil.RequireTypeAssert[*layers.DHCPv6](t, tc.in.Layer(layers.LayerTypeDHCPv6))
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
db := newTestDatabase(t, testLeases)
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)
})
}
}
func TestDHCPServer_ServeEther6_solicitRapidCommit(t *testing.T) {
t.Parallel()
testCases := []struct {
in gopacket.Packet
want *dhcpsvc.Lease
name string
wantOpts layers.DHCPv6Options
}{{
in: newDHCPv6SOLICIT(t, testHWUnknown, testIPv6Unknown, true),
want: &dhcpsvc.Lease{
IP: testIPv6Conf.RangeStart,
Expiry: testClock.Now().Add(testLeaseTTL),
Expiry: testExpiryDynamicLease,
Hostname: aghnet.GenerateHostname(testIPv6Conf.RangeStart),
HWAddr: testHWUnknown,
IsStatic: false,
},
name: "new_rapid_commit",
name: "new",
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
newOptClientDUID(t, testHWUnknown),
@ -126,15 +136,9 @@ func TestDHCPServer_ServeEther6_solicit(t *testing.T) {
layers.NewDHCPv6Option(layers.DHCPv6OptRapidCommit, []byte{}),
},
}, {
in: newDHCPv6SOLICIT(t, testHWStatic, testIPv6Static, true),
want: &dhcpsvc.Lease{
IP: testIPv6Static,
Expiry: time.Time{},
Hostname: "static6",
HWAddr: testHWStatic,
IsStatic: true,
},
name: "existing_rapid_commit",
in: newDHCPv6SOLICIT(t, testHWStatic, testIPv6Static, true),
want: testLease6Static,
name: "existing",
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
newOptClientDUID(t, testHWStatic),
@ -144,15 +148,9 @@ func TestDHCPServer_ServeEther6_solicit(t *testing.T) {
layers.NewDHCPv6Option(layers.DHCPv6OptRapidCommit, []byte{}),
},
}, {
in: newDHCPv6SOLICIT(t, testHWDynamic, testIPv6Dynamic, true),
want: &dhcpsvc.Lease{
IP: testIPv6Dynamic,
Expiry: testExpiryDynamicLease,
Hostname: "dynamic6",
HWAddr: testHWDynamic,
IsStatic: false,
},
name: "existing_dynamic_rapid_commit",
in: newDHCPv6SOLICIT(t, testHWDynamic, testIPv6Dynamic, true),
want: testLease6Dynamic,
name: "existing_dynamic",
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
newOptClientDUID(t, testHWDynamic),
@ -165,12 +163,12 @@ func TestDHCPServer_ServeEther6_solicit(t *testing.T) {
in: newDHCPv6SOLICIT(t, testHWExpired, testIPv6Expired, true),
want: &dhcpsvc.Lease{
IP: testIPv6Expired,
Expiry: testClock.Now().Add(testLeaseTTL),
Hostname: "expired6",
Expiry: testExpiryDynamicLease,
Hostname: testLease6HostnameExpired,
HWAddr: testHWExpired,
IsStatic: false,
},
name: "existing_expired_rapid_commit",
name: "existing_expired",
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
newOptClientDUID(t, testHWExpired),
@ -184,16 +182,17 @@ func TestDHCPServer_ServeEther6_solicit(t *testing.T) {
for _, tc := range testCases {
req := testutil.RequireTypeAssert[*layers.DHCPv6](t, tc.in.Layer(layers.LayerTypeDHCPv6))
db := newTestDatabase(t)
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
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
}
@ -216,8 +215,6 @@ func TestDHCPServer_ServeEther6_solicit(t *testing.T) {
// TODO(e.burkov): Add tests for REQUEST causing errors. This would require a
// custom implementation of the address checker at least.
//
// TODO(e.burkov): Increase maintainability index.
func TestDHCPServer_ServeEther6_request(t *testing.T) {
t.Parallel()
@ -225,16 +222,14 @@ func TestDHCPServer_ServeEther6_request(t *testing.T) {
testCases := []struct {
in gopacket.Packet
solicit gopacket.Packet
want *dhcpsvc.Lease
name string
wantOpts layers.DHCPv6Options
}{{
in: newDHCPv6REQUEST(t, testHWUnknown, testIPv6Unknown),
solicit: nil,
in: newDHCPv6REQUEST(t, testHWUnknown, testIPv6Unknown),
want: &dhcpsvc.Lease{
IP: testIPv6Conf.RangeStart,
Expiry: testClock.Now().Add(testLeaseTTL),
Expiry: testExpiryDynamicLease,
Hostname: aghnet.GenerateHostname(testIPv6Conf.RangeStart),
HWAddr: testHWUnknown,
IsStatic: false,
@ -248,10 +243,9 @@ func TestDHCPServer_ServeEther6_request(t *testing.T) {
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
},
}, {
in: newDHCPv6REQUEST(t, testHWUnknown, notOnLinkAddr),
solicit: nil,
want: nil,
name: "not_on_link",
in: newDHCPv6REQUEST(t, testHWUnknown, notOnLinkAddr),
want: nil,
name: "not_on_link",
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
newOptClientDUID(t, testHWUnknown),
@ -260,15 +254,8 @@ func TestDHCPServer_ServeEther6_request(t *testing.T) {
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
},
}, {
in: newDHCPv6REQUEST(t, testHWStatic, testIPv6Static),
solicit: nil,
want: &dhcpsvc.Lease{
IP: testIPv6Static,
Expiry: time.Time{},
Hostname: "static6",
HWAddr: testHWStatic,
IsStatic: true,
},
in: newDHCPv6REQUEST(t, testHWStatic, testIPv6Static),
want: testLease6Static,
name: "existing_static",
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
@ -278,62 +265,25 @@ func TestDHCPServer_ServeEther6_request(t *testing.T) {
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
},
}, {
in: newDHCPv6REQUEST(t, testHWUnknown, netip.Addr{}),
solicit: nil,
want: nil,
name: "no_iana",
in: newDHCPv6REQUEST(t, testHWUnknown, netip.Addr{}),
want: nil,
name: "no_iana",
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
newOptClientDUID(t, testHWUnknown),
newOptPreference(t, 0),
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
},
}, {
in: newDHCPv6REQUEST(t, testHWUnknown, testIPv6Unknown),
solicit: newDHCPv6SOLICIT(t, testHWUnknown, testIPv6Unknown, false),
want: &dhcpsvc.Lease{
IP: testIPv6Conf.RangeStart,
Expiry: testClock.Now().Add(testLeaseTTL),
Hostname: aghnet.GenerateHostname(testIPv6Conf.RangeStart),
HWAddr: testHWUnknown,
IsStatic: false,
},
name: "preceding_solicit",
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
newOptClientDUID(t, testHWUnknown),
newOptIANA(t, testIAID, testIPv6Conf.RangeStart),
newOptPreference(t, 0),
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
},
}, {
in: newDHCPv6REQUEST(t, testHWUnknown, testIPv6Unknown),
solicit: newDHCPv6SOLICIT(t, testHWUnknown, testIPv6Unknown, true),
want: &dhcpsvc.Lease{
IP: testIPv6Conf.RangeStart,
Expiry: testClock.Now().Add(testLeaseTTL),
Hostname: aghnet.GenerateHostname(testIPv6Conf.RangeStart),
HWAddr: testHWUnknown,
IsStatic: false,
},
name: "preceding_solicit_rapid_commit",
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
newOptClientDUID(t, testHWUnknown),
newOptIANA(t, testIAID, testIPv6Conf.RangeStart),
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)
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
db := newTestDatabase(t, testLeases)
onStore := func(ctx context.Context, leases []*dhcpsvc.Lease) (err error) {
assert.Contains(t, leases, tc.want)
@ -353,13 +303,92 @@ func TestDHCPServer_ServeEther6_request(t *testing.T) {
Enabled: true,
})
if tc.solicit != nil {
testutil.RequireSend(t, inCh, tc.solicit, testTimeout)
testutil.RequireSend(t, inCh, tc.in, testTimeout)
_, ok := testutil.RequireReceive(t, outCh, testTimeout)
require.True(t, ok)
assertValidResponse6(t, req, outCh, tc.wantOpts)
})
}
}
func TestDHCPServer_ServeEther6_requestWithSolicit(t *testing.T) {
t.Parallel()
testCases := []struct {
in gopacket.Packet
solicit gopacket.Packet
want *dhcpsvc.Lease
name string
wantOpts layers.DHCPv6Options
}{{
in: newDHCPv6REQUEST(t, testHWUnknown, testIPv6Unknown),
solicit: newDHCPv6SOLICIT(t, testHWUnknown, testIPv6Unknown, false),
want: &dhcpsvc.Lease{
IP: testIPv6Conf.RangeStart,
Expiry: testExpiryDynamicLease,
Hostname: aghnet.GenerateHostname(testIPv6Conf.RangeStart),
HWAddr: testHWUnknown,
IsStatic: false,
},
name: "preceding_solicit",
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
newOptClientDUID(t, testHWUnknown),
newOptIANA(t, testIAID, testIPv6Conf.RangeStart),
newOptPreference(t, 0),
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
},
}, {
in: newDHCPv6REQUEST(t, testHWUnknown, testIPv6Unknown),
solicit: newDHCPv6SOLICIT(t, testHWUnknown, testIPv6Unknown, true),
want: &dhcpsvc.Lease{
IP: testIPv6Conf.RangeStart,
Expiry: testExpiryDynamicLease,
Hostname: aghnet.GenerateHostname(testIPv6Conf.RangeStart),
HWAddr: testHWUnknown,
IsStatic: false,
},
name: "preceding_solicit_rapid_commit",
wantOpts: layers.DHCPv6Options{
newOptServerDUID(t, testIfaceHWAddr),
newOptClientDUID(t, testHWUnknown),
newOptIANA(t, testIAID, testIPv6Conf.RangeStart),
newOptPreference(t, 0),
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
},
}}
for _, tc := range testCases {
req := testutil.RequireTypeAssert[*layers.DHCPv6](t, tc.in.Layer(layers.LayerTypeDHCPv6))
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
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
}
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.solicit, testTimeout)
_, ok := testutil.RequireReceive(t, outCh, testTimeout)
require.True(t, ok)
testutil.RequireSend(t, inCh, tc.in, testTimeout)
assertValidResponse6(t, req, outCh, tc.wantOpts)

View file

@ -1,6 +1,7 @@
package dhcpsvc
import (
"bytes"
"context"
"encoding/json"
"fmt"
@ -62,7 +63,7 @@ func (jl *jsonLease) compareNames(other *jsonLease) (res int) {
return strings.Compare(jl.Hostname, other.Hostname)
}
// toJSONLease converts *Lease to *jsonLease.
// toJSONLease converts *Lease to *jsonLease. l must not be nil.
func toJSONLease(l *Lease) (jl *jsonLease) {
var expiryStr string
if !l.IsStatic {
@ -208,7 +209,12 @@ func (db *JSONDatabase) Store(ctx context.Context, leases []*Lease) (err error)
dl.Leases = slices.Insert(dl.Leases, i, lease)
}
buf, err := json.Marshal(dl)
// TODO(e.burkov): Consider pooling buffers.
buf := &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetIndent("", " ")
err = enc.Encode(dl)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
@ -217,7 +223,7 @@ func (db *JSONDatabase) Store(ctx context.Context, leases []*Lease) (err error)
db.mu.Lock()
defer db.mu.Unlock()
err = maybe.WriteFile(db.filePath, buf, jsonDatabasePerm)
err = maybe.WriteFile(db.filePath, buf.Bytes(), jsonDatabasePerm)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err

View file

@ -0,0 +1,130 @@
package dhcpsvc_test
import (
"io/fs"
"os"
"path/filepath"
"testing"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// dbFilePath returns the path to the database file for the given test.
func dbFilePath(tb testing.TB) (p string) {
return filepath.Join("testdata", tb.Name()+".json")
}
func TestJSONDatabase_Load(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
wantErrMsg string
want []*dhcpsvc.Lease
}{{
name: "success",
wantErrMsg: "",
want: testLeases,
}, {
name: "no_file",
wantErrMsg: "",
want: nil,
}, {
name: "empty",
wantErrMsg: "",
want: nil,
}, {
want: nil,
name: "bad_format",
wantErrMsg: "loading db: decoding db: json: cannot unmarshal array " +
"into Go value of type dhcpsvc.jsonLeasesData",
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
db := dhcpsvc.NewJSONDatabase(&dhcpsvc.JSONDatabaseConfig{
Logger: testLogger,
FilePath: dbFilePath(t),
})
ctx := testutil.ContextWithTimeout(t, testTimeout)
leases, err := db.Load(ctx)
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
// Use unordered comparison since the order of leases is undefined.
assert.ElementsMatch(t, tc.want, leases)
})
}
}
func TestJSONDatabase_Store(t *testing.T) {
t.Parallel()
const dbFileName = "leases.json"
dirToRemove := t.TempDir()
require.NoError(t, os.RemoveAll(dirToRemove))
testCases := []struct {
wantErr error
name string
path string
in []*dhcpsvc.Lease
}{{
wantErr: nil,
name: "success",
path: filepath.Join(t.TempDir(), dbFileName),
in: testLeases,
}, {
wantErr: nil,
name: "nil",
path: filepath.Join(t.TempDir(), dbFileName),
in: nil,
}, {
wantErr: nil,
name: "empty",
path: filepath.Join(t.TempDir(), dbFileName),
in: []*dhcpsvc.Lease{},
}, {
wantErr: fs.ErrNotExist,
name: "bad_dir",
path: filepath.Join(dirToRemove, dbFileName),
in: testLeases,
}}
for _, tc := range testCases {
db := dhcpsvc.NewJSONDatabase(&dhcpsvc.JSONDatabaseConfig{
Logger: testLogger,
FilePath: tc.path,
})
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctx := testutil.ContextWithTimeout(t, testTimeout)
err := db.Store(ctx, tc.in)
assert.ErrorIs(t, err, tc.wantErr)
if tc.wantErr != nil {
return
}
require.FileExists(t, tc.path)
content, err := os.ReadFile(tc.path)
require.NoError(t, err)
wantContent, err := os.ReadFile(dbFilePath(t))
require.NoError(t, err)
assert.Equal(t, wantContent, content)
})
}
}

View file

@ -211,7 +211,7 @@ func (idx *leaseIndex) addDBLeases(
continue
}
err = idx.add(l, iface)
err = idx.add(l.Clone(), iface)
if err != nil {
logger.WarnContext(ctx, "adding lease", "idx", i, slogutil.KeyError, err)

View file

@ -2,15 +2,12 @@ package dhcpsvc_test
import (
"context"
"net"
"net/netip"
"path/filepath"
"strings"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -19,171 +16,135 @@ import (
func TestDHCPServer_AddLease(t *testing.T) {
t.Parallel()
// TODO(e.burkov): Use a mock.
leasesPath := filepath.Join(t.TempDir(), "leases.json")
db := dhcpsvc.NewJSONDatabase(&dhcpsvc.JSONDatabaseConfig{
Logger: testLogger,
FilePath: leasesPath,
})
srv := newTestDHCPServer(t, &dhcpsvc.Config{
Database: db,
Enabled: true,
})
// NOTE: Keep in sync with testdata.
const (
existHost = "host1"
newHost = "host2"
ipv6Host = "host3"
)
// NOTE: Keep in sync with testdata.
var (
existIP = netip.MustParseAddr("192.0.2.2")
newIP = netip.MustParseAddr("192.0.2.3")
newIPv6 = netip.MustParseAddr("2001:db8::2")
existMAC = errors.Must(net.ParseMAC("01:02:03:04:05:06"))
newMAC = errors.Must(net.ParseMAC("06:05:04:03:02:01"))
ipv6MAC = errors.Must(net.ParseMAC("02:03:04:05:06:07"))
)
ctx := testutil.ContextWithTimeout(t, testTimeout)
require.NoError(t, srv.AddLease(ctx, &dhcpsvc.Lease{
Hostname: existHost,
IP: existIP,
HWAddr: existMAC,
IsStatic: true,
}))
testCases := []struct {
name string
lease *dhcpsvc.Lease
want *dhcpsvc.Lease
wantErrMsg string
}{{
name: "outside_range",
lease: &dhcpsvc.Lease{
Hostname: newHost,
want: &dhcpsvc.Lease{
Hostname: testLease4HostnameUnknown,
IP: netip.MustParseAddr("1.2.3.4"),
HWAddr: newMAC,
HWAddr: testHWUnknown,
},
wantErrMsg: "adding lease: no interface for ip 1.2.3.4",
}, {
name: "duplicate_ip",
lease: &dhcpsvc.Lease{
Hostname: newHost,
IP: existIP,
HWAddr: newMAC,
want: &dhcpsvc.Lease{
Hostname: testLease4HostnameUnknown,
IP: testIPv4Static,
HWAddr: testHWUnknown,
},
wantErrMsg: "adding lease: lease for ip " + existIP.String() +
wantErrMsg: "adding lease: lease for ip " + testIPv4Static.String() +
" already exists",
}, {
name: "duplicate_hostname",
lease: &dhcpsvc.Lease{
Hostname: existHost,
IP: newIP,
HWAddr: newMAC,
want: &dhcpsvc.Lease{
Hostname: testLease4HostnameStatic,
IP: testIPv4Unknown,
HWAddr: testHWUnknown,
},
wantErrMsg: "adding lease: lease for hostname " + existHost +
wantErrMsg: "adding lease: lease for hostname " + testLease4HostnameStatic +
" already exists",
}, {
name: "duplicate_hostname_case",
lease: &dhcpsvc.Lease{
Hostname: strings.ToUpper(existHost),
IP: newIP,
HWAddr: newMAC,
want: &dhcpsvc.Lease{
Hostname: strings.ToUpper(testLease4HostnameStatic),
IP: testIPv4Unknown,
HWAddr: testHWUnknown,
},
wantErrMsg: "adding lease: lease for hostname " +
strings.ToUpper(existHost) + " already exists",
strings.ToUpper(testLease4HostnameStatic) + " already exists",
}, {
name: "duplicate_mac",
lease: &dhcpsvc.Lease{
Hostname: newHost,
IP: newIP,
HWAddr: existMAC,
want: &dhcpsvc.Lease{
Hostname: testLease4HostnameUnknown,
IP: testIPv4Unknown,
HWAddr: testHWStatic,
},
wantErrMsg: "adding lease: lease for mac " + existMAC.String() +
wantErrMsg: "adding lease: lease for mac " + testHWStatic.String() +
" already exists",
}, {
name: "valid",
lease: &dhcpsvc.Lease{
Hostname: newHost,
IP: newIP,
HWAddr: newMAC,
want: &dhcpsvc.Lease{
Hostname: testLease4HostnameUnknown,
IP: testIPv4Unknown,
HWAddr: testHWUnknown,
},
wantErrMsg: "",
}, {
name: "valid_v6",
lease: &dhcpsvc.Lease{
Hostname: ipv6Host,
IP: newIPv6,
HWAddr: ipv6MAC,
want: &dhcpsvc.Lease{
Hostname: testLease6HostnameUnknown,
IP: testIPv6Unknown,
HWAddr: testHWUnknown,
},
wantErrMsg: "",
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctx = testutil.ContextWithTimeout(t, testTimeout)
testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.AddLease(ctx, tc.lease))
t.Parallel()
onStore := func(ctx context.Context, leases []*dhcpsvc.Lease) (err error) {
assert.Contains(t, leases, tc.want)
return nil
}
db := newTestDatabase(t, testLeases)
if tc.wantErrMsg == "" {
db.onStore = onStore
}
srv := newTestDHCPServer(t, &dhcpsvc.Config{
Database: db,
Enabled: true,
})
ctx := testutil.ContextWithTimeout(t, testTimeout)
err := srv.AddLease(ctx, tc.want)
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
})
}
assert.NotEmpty(t, srv.Leases())
}
func TestDHCPServer_index(t *testing.T) {
t.Parallel()
// TODO(e.burkov): Use a mock.
db := newTestDatabase(t)
srv := newTestDHCPServer(t, &dhcpsvc.Config{
Database: db,
Database: newTestDatabase(t, testLeases),
Enabled: true,
})
// NOTE: Keep in sync with testdata.
const (
host1 = "host1"
host2 = "host2"
host3 = "host3"
host4 = "host4"
host5 = "host5"
)
// NOTE: Keep in sync with testdata.
var (
ip1 = netip.MustParseAddr("192.0.2.2")
ip2 = netip.MustParseAddr("192.0.2.3")
ip3 = netip.MustParseAddr("198.51.100.3")
ip4 = netip.MustParseAddr("198.51.100.4")
mac1 = errors.Must(net.ParseMAC("01:02:03:04:05:06"))
mac2 = errors.Must(net.ParseMAC("06:05:04:03:02:01"))
mac3 = errors.Must(net.ParseMAC("02:03:04:05:06:07"))
)
t.Run("ip_idx", func(t *testing.T) {
assert.Equal(t, ip1, srv.IPByHost(host1))
assert.Equal(t, ip2, srv.IPByHost(host2))
assert.Equal(t, ip3, srv.IPByHost(host3))
assert.Equal(t, ip4, srv.IPByHost(host4))
assert.Zero(t, srv.IPByHost(host5))
t.Parallel()
assert.Equal(t, testIPv4Static, srv.IPByHost(testLease4HostnameStatic))
assert.Equal(t, testIPv4Dynamic, srv.IPByHost(testLease4HostnameDynamic))
// TODO(e.burkov): Consider treating expired leases as non-existent.
assert.Equal(t, testIPv4Expired, srv.IPByHost(testLease4HostnameExpired))
assert.Zero(t, srv.IPByHost(testLease4HostnameUnknown))
})
t.Run("name_idx", func(t *testing.T) {
assert.Equal(t, host1, srv.HostByIP(ip1))
assert.Equal(t, host2, srv.HostByIP(ip2))
assert.Equal(t, host3, srv.HostByIP(ip3))
assert.Equal(t, host4, srv.HostByIP(ip4))
t.Parallel()
assert.Equal(t, testLease4HostnameStatic, srv.HostByIP(testIPv4Static))
assert.Equal(t, testLease4HostnameDynamic, srv.HostByIP(testIPv4Dynamic))
assert.Equal(t, testLease4HostnameExpired, srv.HostByIP(testIPv4Expired))
assert.Zero(t, srv.HostByIP(testIPv4Unknown))
assert.Zero(t, srv.HostByIP(netip.Addr{}))
})
t.Run("mac_idx", func(t *testing.T) {
assert.Equal(t, mac1, srv.MACByIP(ip1))
assert.Equal(t, mac2, srv.MACByIP(ip2))
assert.Equal(t, mac3, srv.MACByIP(ip3))
assert.Equal(t, mac1, srv.MACByIP(ip4))
t.Parallel()
assert.Equal(t, testHWStatic, srv.MACByIP(testIPv4Static))
assert.Equal(t, testHWDynamic, srv.MACByIP(testIPv4Dynamic))
assert.Equal(t, testHWExpired, srv.MACByIP(testIPv4Expired))
assert.Zero(t, srv.MACByIP(testIPv4Unknown))
assert.Zero(t, srv.MACByIP(netip.Addr{}))
})
}
@ -191,39 +152,6 @@ func TestDHCPServer_index(t *testing.T) {
func TestDHCPServer_UpdateStaticLease(t *testing.T) {
t.Parallel()
// TODO(e.burkov): Use a mock.
db := newTestDatabase(t)
db.onStore = func(ctx context.Context, leases []*dhcpsvc.Lease) (err error) {
return nil
}
srv := newTestDHCPServer(t, &dhcpsvc.Config{
Database: db,
Enabled: true,
})
// NOTE: Keep in sync with testdata.
const (
host1 = "host1"
host2 = "host2"
host3 = "host3"
host4 = "host4"
host5 = "host5"
host6 = "host6"
)
// NOTE: Keep in sync with testdata.
var (
ip1 = netip.MustParseAddr("192.0.2.2")
ip2 = netip.MustParseAddr("192.0.2.3")
ip3 = netip.MustParseAddr("192.0.2.4")
ip4 = netip.MustParseAddr("2001:db8::2")
mac1 = errors.Must(net.ParseMAC("01:02:03:04:05:06"))
mac2 = errors.Must(net.ParseMAC("06:05:04:03:02:01"))
mac3 = errors.Must(net.ParseMAC("06:05:04:03:02:02"))
)
testCases := []struct {
name string
lease *dhcpsvc.Lease
@ -231,67 +159,99 @@ func TestDHCPServer_UpdateStaticLease(t *testing.T) {
}{{
name: "outside_range",
lease: &dhcpsvc.Lease{
Hostname: host1,
IP: netip.MustParseAddr("1.2.3.4"),
HWAddr: mac1,
IP: testIPv4Conf.RangeEnd.Next(),
Expiry: time.Time{},
Hostname: testLease4HostnameStatic,
HWAddr: testHWStatic,
IsStatic: true,
},
wantErrMsg: "updating static lease: no interface for ip 1.2.3.4",
wantErrMsg: "updating static lease: no interface for ip " +
testIPv4Conf.RangeEnd.Next().String(),
}, {
name: "not_found",
lease: &dhcpsvc.Lease{
Hostname: host3,
IP: ip3,
HWAddr: mac2,
IP: testIPv4Unknown,
Expiry: time.Time{},
Hostname: testLease4HostnameUnknown,
HWAddr: testHWUnknown,
IsStatic: true,
},
wantErrMsg: "updating static lease: no lease for mac " + mac2.String(),
wantErrMsg: "updating static lease: no lease for mac " + testHWUnknown.String(),
}, {
name: "duplicate_ip",
lease: &dhcpsvc.Lease{
Hostname: host1,
IP: ip2,
HWAddr: mac1,
IP: testIPv4Dynamic,
Expiry: time.Time{},
Hostname: testLease4HostnameStatic,
HWAddr: testHWStatic,
IsStatic: true,
},
wantErrMsg: "updating static lease: lease for ip " + ip2.String() +
wantErrMsg: "updating static lease: lease for ip " + testIPv4Dynamic.String() +
" already exists",
}, {
name: "duplicate_hostname",
lease: &dhcpsvc.Lease{
Hostname: host2,
IP: ip1,
HWAddr: mac1,
IP: testIPv4Unknown,
Expiry: time.Time{},
Hostname: testLease4HostnameDynamic,
HWAddr: testHWStatic,
IsStatic: true,
},
wantErrMsg: "updating static lease: lease for hostname " + host2 +
wantErrMsg: "updating static lease: lease for hostname " + testLease4HostnameDynamic +
" already exists",
}, {
name: "duplicate_hostname_case",
lease: &dhcpsvc.Lease{
Hostname: strings.ToUpper(host2),
IP: ip1,
HWAddr: mac1,
IP: testIPv4Unknown,
Expiry: time.Time{},
Hostname: strings.ToUpper(testLease4HostnameDynamic),
HWAddr: testHWStatic,
IsStatic: true,
},
wantErrMsg: "updating static lease: lease for hostname " +
strings.ToUpper(host2) + " already exists",
strings.ToUpper(testLease4HostnameDynamic) + " already exists",
}, {
name: "valid",
lease: &dhcpsvc.Lease{
Hostname: host3,
IP: ip3,
HWAddr: mac1,
IP: testIPv4Unknown,
Expiry: time.Time{},
Hostname: testLease4HostnameStatic,
HWAddr: testHWStatic,
IsStatic: true,
},
wantErrMsg: "",
}, {
name: "valid_v6",
lease: &dhcpsvc.Lease{
Hostname: host6,
IP: ip4,
HWAddr: mac3,
IP: testIPv6Unknown,
Expiry: time.Time{},
Hostname: testLease6HostnameUnknown,
HWAddr: testHWStatic,
IsStatic: true,
},
wantErrMsg: "",
}}
for _, tc := range testCases {
// TODO(e.burkov): Make parallel.
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
onStore := func(ctx context.Context, leases []*dhcpsvc.Lease) (err error) {
assert.Contains(t, leases, tc.lease)
return nil
}
db := newTestDatabase(t, testLeases)
if tc.wantErrMsg == "" {
db.onStore = onStore
}
srv := newTestDHCPServer(t, &dhcpsvc.Config{
Database: db,
Enabled: true,
})
ctx := testutil.ContextWithTimeout(t, testTimeout)
testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.UpdateStaticLease(ctx, tc.lease))
})
@ -301,95 +261,85 @@ func TestDHCPServer_UpdateStaticLease(t *testing.T) {
func TestDHCPServer_RemoveLease(t *testing.T) {
t.Parallel()
// TODO(e.burkov): Use a mock.
db := newTestDatabase(t)
db.onStore = func(ctx context.Context, leases []*dhcpsvc.Lease) (err error) {
return nil
}
srv := newTestDHCPServer(t, &dhcpsvc.Config{
Database: db,
Enabled: true,
})
// NOTE: Keep in sync with testdata.
const (
host1 = "host1"
host2 = "host2"
host3 = "host3"
)
// NOTE: Keep in sync with testdata.
var (
existIP = netip.MustParseAddr("192.0.2.2")
newIP = netip.MustParseAddr("192.0.2.3")
newIPv6 = netip.MustParseAddr("2001:db8::2")
existMAC = errors.Must(net.ParseMAC("01:02:03:04:05:06"))
newMAC = errors.Must(net.ParseMAC("02:03:04:05:06:07"))
ipv6MAC = errors.Must(net.ParseMAC("06:05:04:03:02:01"))
)
testCases := []struct {
name string
lease *dhcpsvc.Lease
want *dhcpsvc.Lease
wantErrMsg string
}{{
name: "not_found_mac",
lease: &dhcpsvc.Lease{
Hostname: host1,
IP: existIP,
HWAddr: newMAC,
want: &dhcpsvc.Lease{
Hostname: testLease4HostnameStatic,
IP: testIPv4Static,
HWAddr: testHWUnknown,
},
wantErrMsg: "removing lease: no lease for mac " + newMAC.String(),
wantErrMsg: "removing lease: no lease for mac " + testHWUnknown.String(),
}, {
name: "not_found_ip",
lease: &dhcpsvc.Lease{
Hostname: host1,
IP: newIP,
HWAddr: existMAC,
want: &dhcpsvc.Lease{
Hostname: testLease4HostnameStatic,
IP: testIPv4Unknown,
HWAddr: testHWStatic,
},
wantErrMsg: "removing lease: no lease for ip " + newIP.String(),
wantErrMsg: "removing lease: no lease for ip " + testIPv4Unknown.String(),
}, {
name: "not_found_host",
lease: &dhcpsvc.Lease{
Hostname: host2,
IP: existIP,
HWAddr: existMAC,
want: &dhcpsvc.Lease{
Hostname: testLease4HostnameUnknown,
IP: testIPv4Static,
HWAddr: testHWStatic,
},
wantErrMsg: "removing lease: no lease for hostname " + host2,
wantErrMsg: "removing lease: no lease for hostname " + testLease4HostnameUnknown,
}, {
name: "valid",
lease: &dhcpsvc.Lease{
Hostname: host1,
IP: existIP,
HWAddr: existMAC,
want: &dhcpsvc.Lease{
Hostname: testLease4HostnameStatic,
IP: testIPv4Static,
HWAddr: testHWStatic,
},
wantErrMsg: "",
}, {
name: "valid_v6",
lease: &dhcpsvc.Lease{
Hostname: host3,
IP: newIPv6,
HWAddr: ipv6MAC,
want: &dhcpsvc.Lease{
Hostname: testLease6HostnameStatic,
IP: testIPv6Static,
HWAddr: testHWStatic,
},
wantErrMsg: "",
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
onStore := func(ctx context.Context, leases []*dhcpsvc.Lease) (err error) {
assert.NotContains(t, leases, tc.want)
return nil
}
db := newTestDatabase(t, testLeases)
if tc.wantErrMsg == "" {
db.onStore = onStore
}
srv := newTestDHCPServer(t, &dhcpsvc.Config{
Database: db,
Enabled: true,
})
ctx := testutil.ContextWithTimeout(t, testTimeout)
testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.RemoveLease(ctx, tc.lease))
err := srv.RemoveLease(ctx, tc.want)
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
})
}
assert.Empty(t, srv.Leases())
}
func TestDHCPServer_Reset(t *testing.T) {
// TODO(e.burkov): Use a mock.
db := newTestDatabase(t)
db.onStore = func(ctx context.Context, leases []*dhcpsvc.Lease) (err error) {
t.Parallel()
db := newTestDatabase(t, testLeases)
db.onStore = func(_ context.Context, leases []*dhcpsvc.Lease) (err error) {
assert.Empty(t, leases)
return nil
@ -400,9 +350,7 @@ func TestDHCPServer_Reset(t *testing.T) {
Enabled: true,
})
const leasesNum = 4
require.Len(t, srv.Leases(), leasesNum)
require.ElementsMatch(t, srv.Leases(), testLeases)
ctx := testutil.ContextWithTimeout(t, testTimeout)
require.NoError(t, srv.Reset(ctx))
@ -411,28 +359,12 @@ func TestDHCPServer_Reset(t *testing.T) {
}
func TestServer_Leases(t *testing.T) {
// TODO(e.burkov): Use a mock.
db := newTestDatabase(t)
t.Parallel()
srv := newTestDHCPServer(t, &dhcpsvc.Config{
Database: db,
Database: newTestDatabase(t, testLeases),
Enabled: true,
})
expiry, err := time.Parse(time.RFC3339, "2042-01-02T03:04:05Z")
require.NoError(t, err)
wantLeases := []*dhcpsvc.Lease{{
Expiry: expiry,
IP: netip.MustParseAddr("192.0.2.3"),
Hostname: "example.host",
HWAddr: errors.Must(net.ParseMAC("AA:AA:AA:AA:AA:AA")),
IsStatic: false,
}, {
Expiry: time.Time{},
IP: netip.MustParseAddr("192.0.2.4"),
Hostname: "example.static.host",
HWAddr: errors.Must(net.ParseMAC("BB:BB:BB:BB:BB:BB")),
IsStatic: true,
}}
assert.ElementsMatch(t, wantLeases, srv.Leases())
assert.ElementsMatch(t, testLeases, srv.Leases())
}

View file

@ -1,19 +0,0 @@
{
"leases": [
{
"expires": "",
"ip": "192.0.2.2",
"hostname": "host1",
"mac": "01:02:03:04:05:06",
"static": true
},
{
"expires": "",
"ip": "2001:db8::2",
"hostname": "host3",
"mac": "06:05:04:03:02:01",
"static": true
}
],
"version": 1
}

View file

@ -1,33 +0,0 @@
{
"leases": [
{
"expires": "",
"ip": "192.0.2.2",
"hostname": "host1",
"mac": "01:02:03:04:05:06",
"static": true
},
{
"expires": "",
"ip": "192.0.2.3",
"hostname": "host2",
"mac": "06:05:04:03:02:01",
"static": true
},
{
"expires": "",
"ip": "2001:db8::2",
"hostname": "host3",
"mac": "02:03:04:05:06:07",
"static": true
},
{
"expires": "",
"ip": "2001:db8::3",
"hostname": "host4",
"mac": "06:05:04:03:02:02",
"static": true
}
],
"version": 1
}

View file

@ -1,19 +0,0 @@
{
"leases": [
{
"expires": "2025-01-01T10:01:01Z",
"ip": "192.0.2.102",
"hostname": "dynamic",
"mac": "02:03:04:05:06:07",
"static": false
},
{
"expires": "2025-01-01T10:01:01Z",
"ip": "192.0.2.103",
"hostname": "mismatch",
"mac": "04:05:06:07:08:09",
"static": false
}
],
"version": 1
}

View file

@ -1,26 +0,0 @@
{
"leases": [
{
"expires": "2025-01-01T10:01:01Z",
"ip": "192.0.2.102",
"hostname": "dynamic4",
"mac": "02:03:04:05:06:07",
"static": false
},
{
"expires": "2025-01-01T01:01:01Z",
"ip": "192.0.2.103",
"hostname": "expired4",
"mac": "03:04:05:06:07:08",
"static": false
},
{
"expires": "",
"ip": "192.0.2.101",
"hostname": "static4",
"mac": "01:02:03:04:05:06",
"static": true
}
],
"version": 1
}

View file

@ -1,12 +0,0 @@
{
"leases": [
{
"expires": "2025-01-01T01:01:00Z",
"ip": "192.0.2.100",
"hostname": "dynamic4",
"mac": "02:03:04:05:06:07",
"static": false
}
],
"version": 1
}

View file

@ -1,19 +0,0 @@
{
"leases": [
{
"expires": "2025-01-01T10:01:01Z",
"ip": "192.0.2.102",
"hostname": "dynamic",
"mac": "02:03:04:05:06:07",
"static": false
},
{
"expires": "2025-01-01T10:01:01Z",
"ip": "192.0.2.103",
"hostname": "mismatch",
"mac": "04:05:06:07:08:09",
"static": false
}
],
"version": 1
}

View file

@ -1,19 +0,0 @@
{
"leases": [
{
"expires": "",
"ip": "192.0.2.101",
"hostname": "static4",
"mac": "01:02:03:04:05:06",
"static": true
},
{
"expires": "2025-01-01T10:01:01Z",
"ip": "192.0.2.102",
"hostname": "dynamic4",
"mac": "02:03:04:05:06:07",
"static": false
}
],
"version": 1
}

View file

@ -1,26 +0,0 @@
{
"leases": [
{
"expires": "",
"ip": "192.0.2.101",
"hostname": "static4",
"mac": "01:02:03:04:05:06",
"static": true
},
{
"expires": "2025-01-01T10:01:01Z",
"ip": "192.0.2.102",
"hostname": "dynamic4",
"mac": "02:03:04:05:06:07",
"static": false
},
{
"expires": "2025-01-01T01:01:01Z",
"ip": "192.0.2.103",
"hostname": "expired4",
"mac": "03:04:05:06:07:08",
"static": false
}
],
"version": 1
}

View file

@ -1,26 +0,0 @@
{
"leases": [
{
"expires": "",
"ip": "192.0.2.101",
"hostname": "static4",
"mac": "01:02:03:04:05:06",
"static": true
},
{
"expires": "2025-01-01T10:01:01Z",
"ip": "192.0.2.102",
"hostname": "dynamic4",
"mac": "02:03:04:05:06:07",
"static": false
},
{
"expires": "2025-01-01T01:01:01Z",
"ip": "192.0.2.103",
"hostname": "expired4",
"mac": "03:04:05:06:07:08",
"static": false
}
],
"version": 1
}

View file

@ -1,19 +0,0 @@
{
"leases": [
{
"expires": "2025-01-01T01:01:01Z",
"ip": "192.0.2.103",
"hostname": "expired4",
"mac": "03:04:05:06:07:08",
"static": false
},
{
"expires": "",
"ip": "192.0.2.101",
"hostname": "static4",
"mac": "01:02:03:04:05:06",
"static": true
}
],
"version": 1
}

View file

@ -1,26 +0,0 @@
{
"leases": [
{
"expires": "2025-01-01T10:01:01Z",
"ip": "2001:db8::66",
"hostname": "dynamic6",
"mac": "02:03:04:05:06:07",
"static": false
},
{
"expires": "2025-01-01T01:01:00Z",
"ip": "2001:db8::67",
"hostname": "expired6",
"mac": "03:04:05:06:07:08",
"static": false
},
{
"expires": "",
"ip": "2001:db8::65",
"hostname": "static6",
"mac": "01:02:03:04:05:06",
"static": true
}
],
"version": 1
}

View file

@ -1,26 +0,0 @@
{
"leases": [
{
"expires": "2025-01-01T10:01:01Z",
"ip": "2001:db8::66",
"hostname": "dynamic6",
"mac": "02:03:04:05:06:07",
"static": false
},
{
"expires": "2025-01-01T01:01:00Z",
"ip": "2001:db8::67",
"hostname": "expired6",
"mac": "03:04:05:06:07:08",
"static": false
},
{
"expires": "",
"ip": "2001:db8::65",
"hostname": "static6",
"mac": "01:02:03:04:05:06",
"static": true
}
],
"version": 1
}

View file

@ -1,26 +0,0 @@
{
"leases": [
{
"expires": "",
"ip": "192.0.2.2",
"hostname": "host1",
"mac": "01:02:03:04:05:06",
"static": true
},
{
"expires": "",
"ip": "192.0.2.3",
"hostname": "host2",
"mac": "01:02:03:04:05:07",
"static": true
},
{
"expires": "",
"ip": "2001:db8::2",
"hostname": "host4",
"mac": "06:05:04:03:02:02",
"static": true
}
],
"version": 1
}

View file

@ -1,33 +0,0 @@
{
"leases": [
{
"expires": "",
"ip": "192.0.2.2",
"hostname": "host1",
"mac": "01:02:03:04:05:06",
"static": true
},
{
"expires": "",
"ip": "192.0.2.3",
"hostname": "host2",
"mac": "06:05:04:03:02:01",
"static": true
},
{
"expires": "",
"ip": "198.51.100.3",
"hostname": "host3",
"mac": "02:03:04:05:06:07",
"static": true
},
{
"expires": "",
"ip": "198.51.100.4",
"hostname": "host4",
"mac": "01:02:03:04:05:06",
"static": true
}
],
"version": 1
}

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1,4 @@
{
"leases": [],
"version": 1
}

View file

@ -0,0 +1,47 @@
{
"leases": [
{
"expires": "",
"ip": "192.0.2.101",
"hostname": "static4",
"mac": "01:02:03:04:05:06",
"static": true
},
{
"expires": "2025-01-02T01:01:01Z",
"ip": "192.0.2.102",
"hostname": "dynamic4",
"mac": "02:03:04:05:06:07",
"static": false
},
{
"expires": "2025-01-01T00:01:01Z",
"ip": "192.0.2.103",
"hostname": "expired4",
"mac": "03:04:05:06:07:08",
"static": false
},
{
"expires": "",
"ip": "2001:db8::65",
"hostname": "static6",
"mac": "01:02:03:04:05:06",
"static": true
},
{
"expires": "2025-01-02T01:01:01Z",
"ip": "2001:db8::66",
"hostname": "dynamic6",
"mac": "02:03:04:05:06:07",
"static": false
},
{
"expires": "2025-01-01T00:01:01Z",
"ip": "2001:db8::67",
"hostname": "expired6",
"mac": "03:04:05:06:07:08",
"static": false
}
],
"version": 1
}

View file

@ -0,0 +1,4 @@
{
"leases": [],
"version": 1
}

View file

@ -0,0 +1,4 @@
{
"leases": [],
"version": 1
}

View file

@ -0,0 +1,47 @@
{
"leases": [
{
"expires": "2025-01-02T01:01:01Z",
"ip": "192.0.2.102",
"hostname": "dynamic4",
"mac": "02:03:04:05:06:07",
"static": false
},
{
"expires": "2025-01-02T01:01:01Z",
"ip": "2001:db8::66",
"hostname": "dynamic6",
"mac": "02:03:04:05:06:07",
"static": false
},
{
"expires": "2025-01-01T00:01:01Z",
"ip": "192.0.2.103",
"hostname": "expired4",
"mac": "03:04:05:06:07:08",
"static": false
},
{
"expires": "2025-01-01T00:01:01Z",
"ip": "2001:db8::67",
"hostname": "expired6",
"mac": "03:04:05:06:07:08",
"static": false
},
{
"expires": "",
"ip": "192.0.2.101",
"hostname": "static4",
"mac": "01:02:03:04:05:06",
"static": true
},
{
"expires": "",
"ip": "2001:db8::65",
"hostname": "static6",
"mac": "01:02:03:04:05:06",
"static": true
}
],
"version": 1
}

View file

@ -1,18 +0,0 @@
{
"leases": [
{
"expires": "2042-01-02T03:04:05Z",
"ip": "192.0.2.3",
"hostname": "example.host",
"mac": "AA:AA:AA:AA:AA:AA",
"static": false
},
{
"ip": "192.0.2.4",
"hostname": "example.static.host",
"mac": "BB:BB:BB:BB:BB:BB",
"static": true
}
],
"version": 1
}

View file

@ -1,7 +1,6 @@
package dhcpsvc
import (
"cmp"
"context"
"fmt"
"log/slog"
@ -11,7 +10,6 @@ import (
"time"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/timeutil"
"github.com/AdguardTeam/golibs/validate"
"github.com/gopacket/gopacket/layers"
@ -226,7 +224,7 @@ func (srv *DHCPServer) newDHCPInterfaceV4(
// updateLease updates lease in the database. lease must be valid and not
// expired.
//
// TODO(e.burkov): Consider simplifying this wrapping.
// TODO(e.burkov): Consider simplifying the wrapping.
func (iface *dhcpInterfaceV4) updateLease(ctx context.Context, lease *Lease) (err error) {
return iface.common.index.update(ctx, lease, iface.common)
}
@ -343,7 +341,7 @@ type dhcpInterfacesV4 []*dhcpInterfaceV4
// returns false if there is no such interface. ip must be valid.
func (ifaces dhcpInterfacesV4) find(ip netip.Addr) (iface4 *netInterface, ok bool) {
i := slices.IndexFunc(ifaces, func(iface *dhcpInterfaceV4) (contains bool) {
return iface.subnet.Contains(ip)
return iface.common.addrSpace.contains(ip)
})
if i < 0 {
return nil, false
@ -352,31 +350,6 @@ func (ifaces dhcpInterfacesV4) find(ip netip.Addr) (iface4 *netInterface, ok boo
return ifaces[i].common, true
}
// updateAndRespond updates the lease and sends a DHCPACK or DHCPNAK response to
// the client according to the update result. idOpt is an expected to be the
// value of the DHCP option Client Identifier, nil if not present. req must be
// a DHCPREQUEST message, lease, and l must not be nil, fd must be valid.
func (iface *dhcpInterfaceV4) updateAndRespond(
ctx context.Context,
l *slog.Logger,
req *layers.DHCPv4,
lease *Lease,
fd *frameData4,
idOpt []byte,
) {
lease.Hostname = cmp.Or(hostname4(req), lease.Hostname)
err := iface.updateLease(ctx, lease)
if err != nil {
l.ErrorContext(ctx, "init-reboot request failed", slogutil.KeyError, err)
iface.respondNAK(ctx, req, fd, idOpt)
return
}
iface.respondACK(ctx, req, fd, lease, idOpt)
}
// FlagsBroadcast is the DHCPv4 message flags field with the broadcast bit set.
const FlagsBroadcast uint16 = 1 << 15