mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2026-07-01 05:11:37 +00:00
Pull request 2676: AGDNS-3863-gopacket-dhcp-vol.26
Updates #4923. Squashed commit of the following: commit072006739bMerge:763b23a3754e6e3002Author: Eugene Burkov <e.burkov@adguard.com> Date: Wed Jun 17 14:40:42 2026 +0300 Merge branch 'master' into AGDNS-3863-gopacket-dhcp-vol.26 commit763b23a37bAuthor: Eugene Burkov <e.burkov@adguard.com> Date: Tue Jun 16 17:28:05 2026 +0300 dhcpsvc: fix lease expiry logic commit84cb6fde32Author: Eugene Burkov <e.burkov@adguard.com> Date: Mon Jun 15 19:13:42 2026 +0300 dhcpsvc: imp tests commitcbe23b49f2Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Jun 9 20:45:59 2026 +0300 dhcpsvc: add v6 tests
This commit is contained in:
parent
54e6e30022
commit
798cd4d2fd
10 changed files with 755 additions and 249 deletions
|
|
@ -41,9 +41,6 @@ const testTimeout = 10 * time.Second
|
|||
// testLeaseTTL is the lease duration used in tests.
|
||||
const testLeaseTTL = 24 * time.Hour
|
||||
|
||||
// testXid is a common transaction ID for DHCPv4 tests.
|
||||
const testXid = 1
|
||||
|
||||
// testLogger is a common logger for tests.
|
||||
var testLogger = slogutil.NewDiscardLogger()
|
||||
|
||||
|
|
@ -102,11 +99,15 @@ const (
|
|||
const (
|
||||
// testRangeStartV6Str is the string representation of the range start of
|
||||
// the IPv6 interface used in tests.
|
||||
testRangeStartV6Str = "2001:db8::1"
|
||||
testRangeStartV6Str = "2001:db8::2"
|
||||
|
||||
// testAnotherRangeStartV6Str is the string representation of the range
|
||||
// start of the second IPv6 interface used in tests.
|
||||
testAnotherRangeStartV6Str = "2001:db9::1"
|
||||
|
||||
// testIfaceAddrV6Str is the string representation of the interface's IPv6
|
||||
// address used in tests.
|
||||
testIfaceAddrV6Str = "2001:db8::1"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -133,10 +134,23 @@ var (
|
|||
RASLAACOnly: true,
|
||||
}
|
||||
|
||||
// testIfaceAddr is a common valid IPv4 address of the test network
|
||||
// disabledIPv4Conf is a configuration of IPv4 part of the interfaces
|
||||
// configuration that is disabled.
|
||||
disabledIPv4Conf = &dhcpsvc.IPv4Config{Enabled: false}
|
||||
|
||||
// disabledIPv6Conf is a configuration of IPv6 part of the interfaces
|
||||
// configuration that is disabled.
|
||||
disabledIPv6Conf = &dhcpsvc.IPv6Config{Enabled: false}
|
||||
|
||||
// testIfaceAddrV4 is a common valid IPv4 address of the test network
|
||||
// interface, compliant with [testIPv4Conf], i.e. outside of the range,
|
||||
// within the subnet, not equal to the gateway.
|
||||
testIfaceAddr = netip.MustParseAddr(testIfaceAddrV4Str)
|
||||
testIfaceAddrV4 = netip.MustParseAddr(testIfaceAddrV4Str)
|
||||
|
||||
// testIfaceAddrV6 is a common valid IPv6 address of the test network
|
||||
// interface, compliant with [testIPv6Conf], i.e. outside of the range,
|
||||
// within the subnet, not equal to the gateway.
|
||||
testIfaceAddrV6 = netip.MustParseAddr(testIfaceAddrV6Str)
|
||||
|
||||
// testIfaceHWAddr is a common valid hardware address of the test network
|
||||
// interface.
|
||||
|
|
@ -171,19 +185,44 @@ var testInterfaceConf = map[string]*dhcpsvc.InterfaceConfig{
|
|||
},
|
||||
}
|
||||
|
||||
// disabledIPv6Config is a configuration of IPv6 part of the interfaces
|
||||
// configuration that is disabled.
|
||||
var disabledIPv6Config = &dhcpsvc.IPv6Config{Enabled: false}
|
||||
// Hardware addresses for test cases.
|
||||
//
|
||||
// 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}
|
||||
|
||||
// fullLayersStack is the complete stack of layers expected to appear in the
|
||||
// testHWStatic is the test MAC address for a known static lease.
|
||||
testHWStatic = net.HardwareAddr{0x1, 0x2, 0x3, 0x4, 0x5, 0x6}
|
||||
|
||||
// testHWDynamic is the test MAC address for a known dynamic lease.
|
||||
testHWDynamic = net.HardwareAddr{0x2, 0x3, 0x4, 0x5, 0x6, 0x7}
|
||||
|
||||
// testHWExpired is the test MAC address for a known expired lease.
|
||||
testHWExpired = net.HardwareAddr{0x3, 0x4, 0x5, 0x6, 0x7, 0x8}
|
||||
|
||||
// testHWAnother is the test MAC address for a lease with another IP.
|
||||
testHWAnother = net.HardwareAddr{0x4, 0x5, 0x6, 0x7, 0x8, 0x9}
|
||||
)
|
||||
|
||||
// fullLayersStack4 is the complete stack of layers expected to appear in the
|
||||
// DHCP response packets.
|
||||
var fullLayersStack = []gopacket.LayerType{
|
||||
var fullLayersStack4 = []gopacket.LayerType{
|
||||
layers.LayerTypeEthernet,
|
||||
layers.LayerTypeIPv4,
|
||||
layers.LayerTypeUDP,
|
||||
layers.LayerTypeDHCPv4,
|
||||
}
|
||||
|
||||
// fullLayersStack6 is the complete stack of layers expected to appear in the
|
||||
// DHCPv6 response packets.
|
||||
var fullLayersStack6 = []gopacket.LayerType{
|
||||
layers.LayerTypeEthernet,
|
||||
layers.LayerTypeIPv6,
|
||||
layers.LayerTypeUDP,
|
||||
layers.LayerTypeDHCPv6,
|
||||
}
|
||||
|
||||
// newTempDB copies the leases database file located in the testdata FS, under
|
||||
// tb.Name()/leases.json, to a temporary directory and returns the path to the
|
||||
// copied file.
|
||||
|
|
@ -235,3 +274,39 @@ func newTestDHCPServer(tb testing.TB, conf *dhcpsvc.Config) (srv *dhcpsvc.DHCPSe
|
|||
func startTestDHCPServer(tb testing.TB, conf *dhcpsvc.Config) {
|
||||
servicetest.RequireRun(tb, newTestDHCPServer(tb, conf), testTimeout)
|
||||
}
|
||||
|
||||
// newTestPacket creates a valid packet from ls using first as first layer
|
||||
// decoder.
|
||||
func newTestPacket(
|
||||
tb testing.TB,
|
||||
first gopacket.Decoder,
|
||||
ls ...gopacket.SerializableLayer,
|
||||
) (pkg gopacket.Packet) {
|
||||
tb.Helper()
|
||||
|
||||
buf := gopacket.NewSerializeBuffer()
|
||||
|
||||
opts := gopacket.SerializeOptions{
|
||||
FixLengths: true,
|
||||
ComputeChecksums: true,
|
||||
}
|
||||
err := gopacket.SerializeLayers(buf, opts, ls...)
|
||||
require.NoError(tb, err)
|
||||
|
||||
return gopacket.NewPacket(buf.Bytes(), first, gopacket.Default)
|
||||
}
|
||||
|
||||
// assertNoResponse asserts that no response is received on the channel within
|
||||
// the timeout.
|
||||
//
|
||||
// TODO(e.burkov): Improve the helper to not rely on timeout.
|
||||
func assertNoResponse(tb testing.TB, outCh <-chan []byte, timeout time.Duration) {
|
||||
tb.Helper()
|
||||
|
||||
var resp []byte
|
||||
require.Panics(tb, func() {
|
||||
resp, _ = testutil.RequireReceive(testutil.NewPanicT(tb), outCh, timeout)
|
||||
})
|
||||
|
||||
require.Nil(tb, resp)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import (
|
|||
var testIPv4InterfacesConf = map[string]*dhcpsvc.InterfaceConfig{
|
||||
testIfaceName: {
|
||||
IPv4: testIPv4Conf,
|
||||
IPv6: disabledIPv6Config,
|
||||
IPv6: disabledIPv6Conf,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -33,54 +33,42 @@ var testIPv4InterfacesConf = map[string]*dhcpsvc.InterfaceConfig{
|
|||
//
|
||||
// NOTE: Keep in sync with testdata.
|
||||
const (
|
||||
// testLeaseHostnameStatic is the test hostname for the static lease.
|
||||
testLeaseHostnameStatic = "static4"
|
||||
// testLease4HostnameStatic is the test hostname for a static DHCPv4 lease.
|
||||
testLease4HostnameStatic = "static4"
|
||||
|
||||
// testLeaseHostnameDynamic is the test hostname for the dynamic lease.
|
||||
testLeaseHostnameDynamic = "dynamic4"
|
||||
// testLease4HostnameDynamic is the test hostname for a dynamic DHCPv4
|
||||
// lease.
|
||||
testLease4HostnameDynamic = "dynamic4"
|
||||
|
||||
// testLeaseHostnameExpired is the test hostname for the expired lease.
|
||||
testLeaseHostnameExpired = "expired4"
|
||||
// testLease4HostnameExpired is the test hostname for an expired DHCPv4
|
||||
// lease.
|
||||
testLease4HostnameExpired = "expired4"
|
||||
)
|
||||
|
||||
// Hardware addresses for test cases.
|
||||
// testXid is a common transaction ID for DHCPv4 tests.
|
||||
//
|
||||
// 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}
|
||||
|
||||
// testHWStatic is the test MAC address for a known static lease.
|
||||
testHWStatic = net.HardwareAddr{0x1, 0x2, 0x3, 0x4, 0x5, 0x6}
|
||||
|
||||
// testHWDynamic is the test MAC address for a known dynamic lease.
|
||||
testHWDynamic = net.HardwareAddr{0x2, 0x3, 0x4, 0x5, 0x6, 0x7}
|
||||
|
||||
// testHWExpired is the test MAC address for a known expired lease.
|
||||
testHWExpired = net.HardwareAddr{0x3, 0x4, 0x5, 0x6, 0x7, 0x8}
|
||||
|
||||
// testHWAnother is the test MAC address for a lease with another IP.
|
||||
testHWAnother = net.HardwareAddr{0x4, 0x5, 0x6, 0x7, 0x8, 0x9}
|
||||
)
|
||||
// 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 (
|
||||
// testIPUnknown is the test IP address for an unknown client.
|
||||
testIPUnknown = netip.MustParseAddr("192.0.2.142")
|
||||
// testIPv4Unknown is the test IP address for an unknown client.
|
||||
testIPv4Unknown = netip.MustParseAddr("192.0.2.142")
|
||||
|
||||
// testIPStatic is the test IP address for a known static lease.
|
||||
testIPStatic = netip.MustParseAddr("192.0.2.101")
|
||||
// testIPv4Static is the test IP address for a known static lease.
|
||||
testIPv4Static = netip.MustParseAddr("192.0.2.101")
|
||||
|
||||
// testIPDynamic is the test IP address for a known dynamic lease.
|
||||
testIPDynamic = netip.MustParseAddr("192.0.2.102")
|
||||
// testIPv4Dynamic is the test IP address for a known dynamic lease.
|
||||
testIPv4Dynamic = netip.MustParseAddr("192.0.2.102")
|
||||
|
||||
// testIPOtherSubnet is the test IP address for a client on another subnet.
|
||||
testIPOtherSubnet = netip.MustParseAddr(testAnotherGatewayIPv4Str)
|
||||
// testIPv4OtherSubnet is the test IP address for a client on another
|
||||
// subnet.
|
||||
testIPv4OtherSubnet = netip.MustParseAddr(testAnotherGatewayIPv4Str)
|
||||
|
||||
// testIPRelayAgent is the test IP address of the relay agent.
|
||||
testIPRelayAgent = netip.MustParseAddr("10.0.0.1")
|
||||
// testIPv4RelayAgent is the test IP address of the relay agent.
|
||||
testIPv4RelayAgent = netip.MustParseAddr("10.0.0.1")
|
||||
)
|
||||
|
||||
// Time-related variables for test cases.
|
||||
|
|
@ -106,7 +94,7 @@ func TestDHCPServer_ServeEther4_discover(t *testing.T) {
|
|||
in: newDHCPDISCOVER(t, testHWUnknown),
|
||||
wantOpts: layers.DHCPOptions{
|
||||
newOptMessageType(t, layers.DHCPMsgTypeOffer),
|
||||
newOptServerID(t, testIfaceAddr),
|
||||
newOptServerID(t, testIfaceAddrV4),
|
||||
newOptLeaseTime(t, testLeaseTTL),
|
||||
},
|
||||
}, {
|
||||
|
|
@ -114,27 +102,27 @@ func TestDHCPServer_ServeEther4_discover(t *testing.T) {
|
|||
in: newDHCPDISCOVER(t, testHWStatic),
|
||||
wantOpts: layers.DHCPOptions{
|
||||
newOptMessageType(t, layers.DHCPMsgTypeOffer),
|
||||
newOptServerID(t, testIfaceAddr),
|
||||
newOptServerID(t, testIfaceAddrV4),
|
||||
newOptLeaseTime(t, testLeaseTTL),
|
||||
newOptHostname(t, testLeaseHostnameStatic),
|
||||
newOptHostname(t, testLease4HostnameStatic),
|
||||
},
|
||||
}, {
|
||||
name: "existing_dynamic",
|
||||
in: newDHCPDISCOVER(t, testHWDynamic),
|
||||
wantOpts: layers.DHCPOptions{
|
||||
newOptMessageType(t, layers.DHCPMsgTypeOffer),
|
||||
newOptServerID(t, testIfaceAddr),
|
||||
newOptServerID(t, testIfaceAddrV4),
|
||||
newOptLeaseTime(t, testTTLDynamicLease),
|
||||
newOptHostname(t, testLeaseHostnameDynamic),
|
||||
newOptHostname(t, testLease4HostnameDynamic),
|
||||
},
|
||||
}, {
|
||||
name: "existing_dynamic_expired",
|
||||
in: newDHCPDISCOVER(t, testHWExpired),
|
||||
wantOpts: layers.DHCPOptions{
|
||||
newOptMessageType(t, layers.DHCPMsgTypeOffer),
|
||||
newOptServerID(t, testIfaceAddr),
|
||||
newOptServerID(t, testIfaceAddrV4),
|
||||
newOptLeaseTime(t, testLeaseTTL),
|
||||
newOptHostname(t, testLeaseHostnameExpired),
|
||||
newOptHostname(t, testLease4HostnameExpired),
|
||||
},
|
||||
}}
|
||||
|
||||
|
|
@ -145,7 +133,7 @@ func TestDHCPServer_ServeEther4_discover(t *testing.T) {
|
|||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceName, testIfaceAddr)
|
||||
ndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceName, testIfaceAddrV4)
|
||||
startTestDHCPServer(t, &dhcpsvc.Config{
|
||||
Interfaces: testIPv4InterfacesConf,
|
||||
NetworkDeviceManager: ndMgr,
|
||||
|
|
@ -155,7 +143,7 @@ func TestDHCPServer_ServeEther4_discover(t *testing.T) {
|
|||
|
||||
testutil.RequireSend(t, inCh, tc.in, testTimeout)
|
||||
|
||||
assertValidResponse(t, req, outCh, tc.wantOpts)
|
||||
assertValidResponse4(t, req, outCh, tc.wantOpts)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -166,7 +154,7 @@ func TestDHCPServer_ServeEther4_discoverExpired(t *testing.T) {
|
|||
pkt := newDHCPDISCOVER(t, testHWUnknown)
|
||||
req := testutil.RequireTypeAssert[*layers.DHCPv4](t, pkt.Layer(layers.LayerTypeDHCPv4))
|
||||
|
||||
ndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceName, testIfaceAddr)
|
||||
ndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceName, testIfaceAddrV4)
|
||||
|
||||
startTestDHCPServer(t, &dhcpsvc.Config{
|
||||
Interfaces: testIPv4InterfacesConf,
|
||||
|
|
@ -177,9 +165,9 @@ func TestDHCPServer_ServeEther4_discoverExpired(t *testing.T) {
|
|||
|
||||
testutil.RequireSend(t, inCh, pkt, testTimeout)
|
||||
|
||||
assertValidResponse(t, req, outCh, layers.DHCPOptions{
|
||||
assertValidResponse4(t, req, outCh, layers.DHCPOptions{
|
||||
newOptMessageType(t, layers.DHCPMsgTypeOffer),
|
||||
newOptServerID(t, testIfaceAddr),
|
||||
newOptServerID(t, testIfaceAddrV4),
|
||||
newOptLeaseTime(t, testLeaseTTL),
|
||||
})
|
||||
}
|
||||
|
|
@ -187,18 +175,18 @@ func TestDHCPServer_ServeEther4_discoverExpired(t *testing.T) {
|
|||
func TestDHCPServer_ServeEther4_release(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ipMismatch := testIPDynamic.Next().Next()
|
||||
ipMismatch := testIPv4Dynamic.Next().Next()
|
||||
|
||||
testCases := []struct {
|
||||
req gopacket.Packet
|
||||
name string
|
||||
wantChange bool
|
||||
}{{
|
||||
req: newDHCPRELEASE(t, testHWDynamic, testIPDynamic),
|
||||
req: newDHCPRELEASE(t, testHWDynamic, testIPv4Dynamic),
|
||||
name: "success",
|
||||
wantChange: true,
|
||||
}, {
|
||||
req: newDHCPRELEASE(t, testHWUnknown, testIPDynamic),
|
||||
req: newDHCPRELEASE(t, testHWUnknown, testIPv4Dynamic),
|
||||
name: "not_found",
|
||||
wantChange: false,
|
||||
}, {
|
||||
|
|
@ -206,7 +194,7 @@ func TestDHCPServer_ServeEther4_release(t *testing.T) {
|
|||
name: "mismatch_ip",
|
||||
wantChange: false,
|
||||
}, {
|
||||
req: newDHCPRELEASE(t, testHWDynamic, testIPOtherSubnet),
|
||||
req: newDHCPRELEASE(t, testHWDynamic, testIPv4OtherSubnet),
|
||||
name: "bad_subnet",
|
||||
wantChange: false,
|
||||
}}
|
||||
|
|
@ -217,7 +205,7 @@ func TestDHCPServer_ServeEther4_release(t *testing.T) {
|
|||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ndMgr, inCh, _ := newTestNetworkDeviceManager(t, testIfaceName, testIfaceAddr)
|
||||
ndMgr, inCh, _ := newTestNetworkDeviceManager(t, testIfaceName, testIfaceAddrV4)
|
||||
srv := newTestDHCPServer(t, &dhcpsvc.Config{
|
||||
Interfaces: testIPv4InterfacesConf,
|
||||
NetworkDeviceManager: ndMgr,
|
||||
|
|
@ -259,7 +247,7 @@ func TestDHCPServer_ServeEther4_requestSelecting(t *testing.T) {
|
|||
request: newDHCPREQUEST(t, &dhcpRequestConfig{
|
||||
options: layers.DHCPOptions{
|
||||
newOptRequestIP(t, testIPv4Conf.RangeStart),
|
||||
newOptServerID(t, testIfaceAddr),
|
||||
newOptServerID(t, testIfaceAddrV4),
|
||||
},
|
||||
clientHWAddr: testHWUnknown,
|
||||
flags: dhcpsvc.FlagsBroadcast,
|
||||
|
|
@ -267,15 +255,15 @@ func TestDHCPServer_ServeEther4_requestSelecting(t *testing.T) {
|
|||
name: "success",
|
||||
wantOpts: layers.DHCPOptions{
|
||||
newOptMessageType(t, layers.DHCPMsgTypeAck),
|
||||
newOptServerID(t, testIfaceAddr),
|
||||
newOptServerID(t, testIfaceAddrV4),
|
||||
newOptLeaseTime(t, testLeaseTTL),
|
||||
},
|
||||
}, {
|
||||
discover: newDHCPDISCOVER(t, testHWStatic),
|
||||
request: newDHCPREQUEST(t, &dhcpRequestConfig{
|
||||
options: layers.DHCPOptions{
|
||||
newOptRequestIP(t, testIPStatic),
|
||||
newOptServerID(t, testIPOtherSubnet),
|
||||
newOptRequestIP(t, testIPv4Static),
|
||||
newOptServerID(t, testIPv4OtherSubnet),
|
||||
},
|
||||
clientHWAddr: testHWStatic,
|
||||
flags: dhcpsvc.FlagsBroadcast,
|
||||
|
|
@ -287,7 +275,7 @@ func TestDHCPServer_ServeEther4_requestSelecting(t *testing.T) {
|
|||
request: newDHCPREQUEST(t, &dhcpRequestConfig{
|
||||
options: layers.DHCPOptions{
|
||||
newOptRequestIP(t, testIPv4Conf.RangeEnd.Next()),
|
||||
newOptServerID(t, testIfaceAddr),
|
||||
newOptServerID(t, testIfaceAddrV4),
|
||||
},
|
||||
clientHWAddr: testHWUnknown,
|
||||
flags: dhcpsvc.FlagsBroadcast,
|
||||
|
|
@ -295,14 +283,14 @@ func TestDHCPServer_ServeEther4_requestSelecting(t *testing.T) {
|
|||
name: "no_lease",
|
||||
wantOpts: layers.DHCPOptions{
|
||||
newOptMessageType(t, layers.DHCPMsgTypeNak),
|
||||
newOptServerID(t, testIfaceAddr),
|
||||
newOptServerID(t, testIfaceAddrV4),
|
||||
},
|
||||
}, {
|
||||
discover: newDHCPDISCOVER(t, testHWStatic),
|
||||
request: newDHCPREQUEST(t, &dhcpRequestConfig{
|
||||
options: layers.DHCPOptions{
|
||||
newOptRequestIP(t, testIPv4Conf.RangeEnd.Next()),
|
||||
newOptServerID(t, testIfaceAddr),
|
||||
newOptServerID(t, testIfaceAddrV4),
|
||||
},
|
||||
clientHWAddr: testHWStatic,
|
||||
flags: dhcpsvc.FlagsBroadcast,
|
||||
|
|
@ -310,17 +298,17 @@ func TestDHCPServer_ServeEther4_requestSelecting(t *testing.T) {
|
|||
name: "wrong_ip",
|
||||
wantOpts: layers.DHCPOptions{
|
||||
newOptMessageType(t, layers.DHCPMsgTypeNak),
|
||||
newOptServerID(t, testIfaceAddr),
|
||||
newOptServerID(t, testIfaceAddrV4),
|
||||
},
|
||||
}, {
|
||||
discover: newDHCPDISCOVER(t, testHWStatic),
|
||||
request: newDHCPREQUEST(t, &dhcpRequestConfig{
|
||||
options: layers.DHCPOptions{
|
||||
newOptRequestIP(t, testIPStatic),
|
||||
newOptServerID(t, testIfaceAddr),
|
||||
newOptRequestIP(t, testIPv4Static),
|
||||
newOptServerID(t, testIfaceAddrV4),
|
||||
},
|
||||
clientHWAddr: testHWStatic,
|
||||
clientIP: testIPStatic,
|
||||
clientIP: testIPv4Static,
|
||||
flags: dhcpsvc.FlagsBroadcast,
|
||||
}),
|
||||
name: "nonzero_ciaddr",
|
||||
|
|
@ -333,7 +321,7 @@ func TestDHCPServer_ServeEther4_requestSelecting(t *testing.T) {
|
|||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceName, testIfaceAddr)
|
||||
ndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceName, testIfaceAddrV4)
|
||||
startTestDHCPServer(t, &dhcpsvc.Config{
|
||||
Logger: slogutil.NewDiscardLogger(),
|
||||
Interfaces: testIPv4InterfacesConf,
|
||||
|
|
@ -351,7 +339,7 @@ func TestDHCPServer_ServeEther4_requestSelecting(t *testing.T) {
|
|||
|
||||
testutil.RequireSend(t, inCh, tc.request, testTimeout)
|
||||
|
||||
assertValidResponse(t, dhcpv4FromPacket(t, tc.request), outCh, tc.wantOpts)
|
||||
assertValidResponse4(t, dhcpv4FromPacket(t, tc.request), outCh, tc.wantOpts)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -366,31 +354,31 @@ func TestDHCPServer_ServeEther4_requestInitReboot(t *testing.T) {
|
|||
}{{
|
||||
name: "success",
|
||||
req: newDHCPREQUEST(t, &dhcpRequestConfig{
|
||||
options: layers.DHCPOptions{newOptRequestIP(t, testIPStatic)},
|
||||
options: layers.DHCPOptions{newOptRequestIP(t, testIPv4Static)},
|
||||
clientHWAddr: testHWStatic,
|
||||
flags: dhcpsvc.FlagsBroadcast,
|
||||
}),
|
||||
wantOpts: layers.DHCPOptions{
|
||||
newOptMessageType(t, layers.DHCPMsgTypeAck),
|
||||
newOptServerID(t, testIfaceAddr),
|
||||
newOptServerID(t, testIfaceAddrV4),
|
||||
newOptLeaseTime(t, testLeaseTTL),
|
||||
newOptHostname(t, testLeaseHostnameStatic),
|
||||
newOptHostname(t, testLease4HostnameStatic),
|
||||
},
|
||||
}, {
|
||||
name: "wrong_subnet",
|
||||
req: newDHCPREQUEST(t, &dhcpRequestConfig{
|
||||
options: layers.DHCPOptions{newOptRequestIP(t, testIPOtherSubnet)},
|
||||
options: layers.DHCPOptions{newOptRequestIP(t, testIPv4OtherSubnet)},
|
||||
clientHWAddr: testHWStatic,
|
||||
flags: dhcpsvc.FlagsBroadcast,
|
||||
}),
|
||||
wantOpts: layers.DHCPOptions{
|
||||
newOptMessageType(t, layers.DHCPMsgTypeNak),
|
||||
newOptServerID(t, testIfaceAddr),
|
||||
newOptServerID(t, testIfaceAddrV4),
|
||||
},
|
||||
}, {
|
||||
name: "no_lease",
|
||||
req: newDHCPREQUEST(t, &dhcpRequestConfig{
|
||||
options: layers.DHCPOptions{newOptRequestIP(t, testIPStatic)},
|
||||
options: layers.DHCPOptions{newOptRequestIP(t, testIPv4Static)},
|
||||
clientHWAddr: testHWUnknown,
|
||||
flags: dhcpsvc.FlagsBroadcast,
|
||||
}),
|
||||
|
|
@ -398,30 +386,30 @@ func TestDHCPServer_ServeEther4_requestInitReboot(t *testing.T) {
|
|||
}, {
|
||||
name: "wrong_ip",
|
||||
req: newDHCPREQUEST(t, &dhcpRequestConfig{
|
||||
options: layers.DHCPOptions{newOptRequestIP(t, testIPDynamic)},
|
||||
options: layers.DHCPOptions{newOptRequestIP(t, testIPv4Dynamic)},
|
||||
clientHWAddr: testHWStatic,
|
||||
flags: dhcpsvc.FlagsBroadcast,
|
||||
}),
|
||||
wantOpts: layers.DHCPOptions{
|
||||
newOptMessageType(t, layers.DHCPMsgTypeNak),
|
||||
newOptServerID(t, testIfaceAddr),
|
||||
newOptServerID(t, testIfaceAddrV4),
|
||||
},
|
||||
}, {
|
||||
name: "wrong_ip_no_broadcast",
|
||||
req: newDHCPREQUEST(t, &dhcpRequestConfig{
|
||||
options: layers.DHCPOptions{newOptRequestIP(t, testIPDynamic)},
|
||||
options: layers.DHCPOptions{newOptRequestIP(t, testIPv4Dynamic)},
|
||||
clientHWAddr: testHWStatic,
|
||||
}),
|
||||
wantOpts: layers.DHCPOptions{
|
||||
newOptMessageType(t, layers.DHCPMsgTypeNak),
|
||||
newOptServerID(t, testIfaceAddr),
|
||||
newOptServerID(t, testIfaceAddrV4),
|
||||
},
|
||||
}, {
|
||||
name: "nonzero_ciaddr",
|
||||
req: newDHCPREQUEST(t, &dhcpRequestConfig{
|
||||
options: layers.DHCPOptions{newOptRequestIP(t, testIPStatic)},
|
||||
options: layers.DHCPOptions{newOptRequestIP(t, testIPv4Static)},
|
||||
clientHWAddr: testHWStatic,
|
||||
clientIP: testIPStatic,
|
||||
clientIP: testIPv4Static,
|
||||
flags: dhcpsvc.FlagsBroadcast,
|
||||
}),
|
||||
wantOpts: nil,
|
||||
|
|
@ -433,7 +421,7 @@ func TestDHCPServer_ServeEther4_requestInitReboot(t *testing.T) {
|
|||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceName, testIfaceAddr)
|
||||
ndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceName, testIfaceAddrV4)
|
||||
startTestDHCPServer(t, &dhcpsvc.Config{
|
||||
Interfaces: testIPv4InterfacesConf,
|
||||
NetworkDeviceManager: ndMgr,
|
||||
|
|
@ -443,7 +431,7 @@ func TestDHCPServer_ServeEther4_requestInitReboot(t *testing.T) {
|
|||
|
||||
testutil.RequireSend(t, inCh, tc.req, testTimeout)
|
||||
|
||||
assertValidResponse(t, dhcpv4FromPacket(t, tc.req), outCh, tc.wantOpts)
|
||||
assertValidResponse4(t, dhcpv4FromPacket(t, tc.req), outCh, tc.wantOpts)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -459,52 +447,52 @@ func TestDHCPServer_ServeEther4_requestRenewSuccess(t *testing.T) {
|
|||
name: "success",
|
||||
req: newDHCPREQUEST(t, &dhcpRequestConfig{
|
||||
clientHWAddr: testHWDynamic,
|
||||
clientIP: testIPDynamic,
|
||||
clientIP: testIPv4Dynamic,
|
||||
flags: dhcpsvc.FlagsBroadcast,
|
||||
}),
|
||||
wantOpts: layers.DHCPOptions{
|
||||
newOptMessageType(t, layers.DHCPMsgTypeAck),
|
||||
newOptServerID(t, testIfaceAddr),
|
||||
newOptServerID(t, testIfaceAddrV4),
|
||||
newOptLeaseTime(t, testTTLDynamicLease),
|
||||
newOptHostname(t, testLeaseHostnameDynamic),
|
||||
newOptHostname(t, testLease4HostnameDynamic),
|
||||
},
|
||||
}, {
|
||||
name: "static",
|
||||
req: newDHCPREQUEST(t, &dhcpRequestConfig{
|
||||
clientHWAddr: testHWStatic,
|
||||
clientIP: testIPStatic,
|
||||
clientIP: testIPv4Static,
|
||||
flags: dhcpsvc.FlagsBroadcast,
|
||||
}),
|
||||
wantOpts: layers.DHCPOptions{
|
||||
newOptMessageType(t, layers.DHCPMsgTypeAck),
|
||||
newOptServerID(t, testIfaceAddr),
|
||||
newOptServerID(t, testIfaceAddrV4),
|
||||
newOptLeaseTime(t, testLeaseTTL),
|
||||
newOptHostname(t, testLeaseHostnameStatic),
|
||||
newOptHostname(t, testLease4HostnameStatic),
|
||||
},
|
||||
}, {
|
||||
name: "relay_agent",
|
||||
req: newDHCPREQUEST(t, &dhcpRequestConfig{
|
||||
clientHWAddr: testHWDynamic,
|
||||
clientIP: testIPDynamic,
|
||||
relayAgentIP: testIPRelayAgent,
|
||||
clientIP: testIPv4Dynamic,
|
||||
relayAgentIP: testIPv4RelayAgent,
|
||||
}),
|
||||
wantOpts: layers.DHCPOptions{
|
||||
newOptMessageType(t, layers.DHCPMsgTypeAck),
|
||||
newOptServerID(t, testIfaceAddr),
|
||||
newOptServerID(t, testIfaceAddrV4),
|
||||
newOptLeaseTime(t, testTTLDynamicLease),
|
||||
newOptHostname(t, testLeaseHostnameDynamic),
|
||||
newOptHostname(t, testLease4HostnameDynamic),
|
||||
},
|
||||
}, {
|
||||
name: "ciaddr_unicast",
|
||||
req: newDHCPREQUEST(t, &dhcpRequestConfig{
|
||||
clientHWAddr: testHWDynamic,
|
||||
clientIP: testIPDynamic,
|
||||
clientIP: testIPv4Dynamic,
|
||||
}),
|
||||
wantOpts: layers.DHCPOptions{
|
||||
newOptMessageType(t, layers.DHCPMsgTypeAck),
|
||||
newOptServerID(t, testIfaceAddr),
|
||||
newOptServerID(t, testIfaceAddrV4),
|
||||
newOptLeaseTime(t, testTTLDynamicLease),
|
||||
newOptHostname(t, testLeaseHostnameDynamic),
|
||||
newOptHostname(t, testLease4HostnameDynamic),
|
||||
},
|
||||
}}
|
||||
|
||||
|
|
@ -514,7 +502,7 @@ func TestDHCPServer_ServeEther4_requestRenewSuccess(t *testing.T) {
|
|||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceName, testIfaceAddr)
|
||||
ndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceName, testIfaceAddrV4)
|
||||
startTestDHCPServer(t, &dhcpsvc.Config{
|
||||
Interfaces: testIPv4InterfacesConf,
|
||||
NetworkDeviceManager: ndMgr,
|
||||
|
|
@ -524,7 +512,7 @@ func TestDHCPServer_ServeEther4_requestRenewSuccess(t *testing.T) {
|
|||
|
||||
testutil.RequireSend(t, inCh, tc.req, testTimeout)
|
||||
|
||||
assertValidResponse(t, dhcpv4FromPacket(t, tc.req), outCh, tc.wantOpts)
|
||||
assertValidResponse4(t, dhcpv4FromPacket(t, tc.req), outCh, tc.wantOpts)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -540,7 +528,7 @@ func TestDHCPServer_ServeEther4_requestRenewFail(t *testing.T) {
|
|||
name: "wrong_subnet",
|
||||
req: newDHCPREQUEST(t, &dhcpRequestConfig{
|
||||
clientHWAddr: testHWStatic,
|
||||
clientIP: testIPOtherSubnet,
|
||||
clientIP: testIPv4OtherSubnet,
|
||||
flags: dhcpsvc.FlagsBroadcast,
|
||||
}),
|
||||
wantOpts: nil,
|
||||
|
|
@ -548,7 +536,7 @@ func TestDHCPServer_ServeEther4_requestRenewFail(t *testing.T) {
|
|||
name: "no_lease",
|
||||
req: newDHCPREQUEST(t, &dhcpRequestConfig{
|
||||
clientHWAddr: testHWUnknown,
|
||||
clientIP: testIPStatic,
|
||||
clientIP: testIPv4Static,
|
||||
flags: dhcpsvc.FlagsBroadcast,
|
||||
}),
|
||||
wantOpts: nil,
|
||||
|
|
@ -556,12 +544,12 @@ func TestDHCPServer_ServeEther4_requestRenewFail(t *testing.T) {
|
|||
name: "wrong_ip",
|
||||
req: newDHCPREQUEST(t, &dhcpRequestConfig{
|
||||
clientHWAddr: testHWStatic,
|
||||
clientIP: testIPDynamic,
|
||||
clientIP: testIPv4Dynamic,
|
||||
flags: dhcpsvc.FlagsBroadcast,
|
||||
}),
|
||||
wantOpts: layers.DHCPOptions{
|
||||
newOptMessageType(t, layers.DHCPMsgTypeNak),
|
||||
newOptServerID(t, testIfaceAddr),
|
||||
newOptServerID(t, testIfaceAddrV4),
|
||||
},
|
||||
}}
|
||||
|
||||
|
|
@ -571,7 +559,7 @@ func TestDHCPServer_ServeEther4_requestRenewFail(t *testing.T) {
|
|||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceName, testIfaceAddr)
|
||||
ndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceName, testIfaceAddrV4)
|
||||
startTestDHCPServer(t, &dhcpsvc.Config{
|
||||
Interfaces: testIPv4InterfacesConf,
|
||||
NetworkDeviceManager: ndMgr,
|
||||
|
|
@ -581,7 +569,7 @@ func TestDHCPServer_ServeEther4_requestRenewFail(t *testing.T) {
|
|||
|
||||
testutil.RequireSend(t, inCh, tc.req, testTimeout)
|
||||
|
||||
assertValidResponse(t, dhcpv4FromPacket(t, tc.req), outCh, tc.wantOpts)
|
||||
assertValidResponse4(t, dhcpv4FromPacket(t, tc.req), outCh, tc.wantOpts)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -594,19 +582,19 @@ func TestDHCPServer_ServeEther4_decline(t *testing.T) {
|
|||
name string
|
||||
wantChange bool
|
||||
}{{
|
||||
req: newDHCPDECLINE(t, testHWDynamic, testIPDynamic),
|
||||
req: newDHCPDECLINE(t, testHWDynamic, testIPv4Dynamic),
|
||||
name: "success",
|
||||
wantChange: true,
|
||||
}, {
|
||||
req: newDHCPDECLINE(t, testHWUnknown, testIPDynamic),
|
||||
req: newDHCPDECLINE(t, testHWUnknown, testIPv4Dynamic),
|
||||
name: "not_found",
|
||||
wantChange: false,
|
||||
}, {
|
||||
req: newDHCPDECLINE(t, testHWAnother, testIPUnknown),
|
||||
req: newDHCPDECLINE(t, testHWAnother, testIPv4Unknown),
|
||||
name: "mismatch_ip",
|
||||
wantChange: false,
|
||||
}, {
|
||||
req: newDHCPDECLINE(t, testHWDynamic, testIPOtherSubnet),
|
||||
req: newDHCPDECLINE(t, testHWDynamic, testIPv4OtherSubnet),
|
||||
name: "bad_subnet",
|
||||
wantChange: false,
|
||||
}, {
|
||||
|
|
@ -621,7 +609,7 @@ func TestDHCPServer_ServeEther4_decline(t *testing.T) {
|
|||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ndMgr, inCh, _ := newTestNetworkDeviceManager(t, testIfaceName, testIfaceAddr)
|
||||
ndMgr, inCh, _ := newTestNetworkDeviceManager(t, testIfaceName, testIfaceAddrV4)
|
||||
srv := newTestDHCPServer(t, &dhcpsvc.Config{
|
||||
Interfaces: testIPv4InterfacesConf,
|
||||
NetworkDeviceManager: ndMgr,
|
||||
|
|
@ -675,7 +663,7 @@ type dhcpRequestConfig struct {
|
|||
func newDHCPREQUEST(tb testing.TB, conf *dhcpRequestConfig) (pkt gopacket.Packet) {
|
||||
tb.Helper()
|
||||
|
||||
eth := newEthernet4Layer(tb, conf.clientHWAddr, nil)
|
||||
eth := newEthernetLayer(tb, conf.clientHWAddr, nil, layers.EthernetTypeIPv4)
|
||||
|
||||
ip, udp := newIPv4UDPLayer(
|
||||
tb,
|
||||
|
|
@ -711,7 +699,7 @@ func newDHCPREQUEST(tb testing.TB, conf *dhcpRequestConfig) (pkt gopacket.Packet
|
|||
func newDHCPDISCOVER(tb testing.TB, clientHWAddr net.HardwareAddr) (pkt gopacket.Packet) {
|
||||
tb.Helper()
|
||||
|
||||
eth := newEthernet4Layer(tb, clientHWAddr, nil)
|
||||
eth := newEthernetLayer(tb, clientHWAddr, nil, layers.EthernetTypeIPv4)
|
||||
|
||||
ip, udp := newIPv4UDPLayer(tb, netip.AddrPort{}, netip.AddrPort{})
|
||||
|
||||
|
|
@ -737,12 +725,12 @@ func newDHCPRELEASE(
|
|||
) (pkt gopacket.Packet) {
|
||||
tb.Helper()
|
||||
|
||||
eth := newEthernet4Layer(tb, clientHWAddr, testIfaceHWAddr)
|
||||
eth := newEthernetLayer(tb, clientHWAddr, testIfaceHWAddr, layers.EthernetTypeIPv4)
|
||||
|
||||
ip, udp := newIPv4UDPLayer(
|
||||
tb,
|
||||
netip.AddrPortFrom(clientIP, uint16(dhcpsvc.ClientPortV4)),
|
||||
netip.AddrPortFrom(testIfaceAddr, uint16(dhcpsvc.ServerPortV4)),
|
||||
netip.AddrPortFrom(testIfaceAddrV4, uint16(dhcpsvc.ServerPortV4)),
|
||||
)
|
||||
|
||||
dhcp := &layers.DHCPv4{
|
||||
|
|
@ -768,7 +756,7 @@ func newDHCPDECLINE(
|
|||
) (pkt gopacket.Packet) {
|
||||
tb.Helper()
|
||||
|
||||
eth := newEthernet4Layer(tb, clientHWAddr, nil)
|
||||
eth := newEthernetLayer(tb, clientHWAddr, nil, layers.EthernetTypeIPv4)
|
||||
|
||||
ip, udp := newIPv4UDPLayer(tb, netip.AddrPort{}, netip.AddrPort{})
|
||||
|
||||
|
|
@ -828,47 +816,6 @@ func newIPv4UDPLayer(tb testing.TB, src, dst netip.AddrPort) (ip *layers.IPv4, u
|
|||
return ip, udp
|
||||
}
|
||||
|
||||
// newEthernet4Layer creates a new Ethernet layer for IPv4 packets. Nil src is
|
||||
// replaced with an unspecified MAC address, nil dst is replaced with a
|
||||
// broadcast MAC address.
|
||||
func newEthernet4Layer(tb testing.TB, src, dst net.HardwareAddr) (eth *layers.Ethernet) {
|
||||
tb.Helper()
|
||||
|
||||
if src == nil {
|
||||
src = net.HardwareAddr{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||
}
|
||||
if dst == nil {
|
||||
dst = net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
|
||||
}
|
||||
|
||||
return &layers.Ethernet{
|
||||
SrcMAC: src,
|
||||
DstMAC: dst,
|
||||
EthernetType: layers.EthernetTypeIPv4,
|
||||
}
|
||||
}
|
||||
|
||||
// newTestPacket creates a valid packet from ls using first as first layer
|
||||
// decoder.
|
||||
func newTestPacket(
|
||||
tb testing.TB,
|
||||
first gopacket.Decoder,
|
||||
ls ...gopacket.SerializableLayer,
|
||||
) (pkg gopacket.Packet) {
|
||||
tb.Helper()
|
||||
|
||||
buf := gopacket.NewSerializeBuffer()
|
||||
|
||||
opts := gopacket.SerializeOptions{
|
||||
FixLengths: true,
|
||||
ComputeChecksums: true,
|
||||
}
|
||||
err := gopacket.SerializeLayers(buf, opts, ls...)
|
||||
require.NoError(tb, err)
|
||||
|
||||
return gopacket.NewPacket(buf.Bytes(), first, gopacket.Default)
|
||||
}
|
||||
|
||||
// requireEthernet requires data to contain an Ethernet layer and all layers
|
||||
// from ls. First of ls must be of type [layers.LayerTypeEthernet].
|
||||
func requireEthernet(
|
||||
|
|
@ -886,10 +833,10 @@ func requireEthernet(
|
|||
return types
|
||||
}
|
||||
|
||||
// assertValidResponse asserts that recvCh eventually gets the response with
|
||||
// assertValidResponse4 asserts that recvCh eventually gets the response with
|
||||
// wantOpts for request. If wantOpts is nil, asserts that no response is sent.
|
||||
// request and recvCh must not be nil.
|
||||
func assertValidResponse(
|
||||
func assertValidResponse4(
|
||||
tb testing.TB,
|
||||
request *layers.DHCPv4,
|
||||
recvCh <-chan []byte,
|
||||
|
|
@ -910,7 +857,7 @@ func assertValidResponse(
|
|||
udp := &layers.UDP{}
|
||||
resp := &layers.DHCPv4{}
|
||||
types := requireEthernet(tb, respData, &layers.Ethernet{}, ip, udp, resp)
|
||||
require.Equal(tb, fullLayersStack, types)
|
||||
require.Equal(tb, fullLayersStack4, types)
|
||||
|
||||
assertValidDHCPv4(tb, request, resp, ip, udp)
|
||||
|
||||
|
|
@ -927,6 +874,8 @@ func assertValidResponse(
|
|||
// assertValidDHCPv4 asserts that the response is valid for the given request
|
||||
// according to RFC 2131.
|
||||
func assertValidDHCPv4(tb testing.TB, req, resp *layers.DHCPv4, ip *layers.IPv4, udp *layers.UDP) {
|
||||
tb.Helper()
|
||||
|
||||
switch {
|
||||
case !req.RelayAgentIP.IsUnspecified():
|
||||
assert.Equal(tb, req.RelayAgentIP.To4(), ip.DstIP)
|
||||
|
|
@ -945,21 +894,6 @@ func assertValidDHCPv4(tb testing.TB, req, resp *layers.DHCPv4, ip *layers.IPv4,
|
|||
}
|
||||
}
|
||||
|
||||
// assertNoResponse asserts that no response is received on the channel within
|
||||
// the timeout.
|
||||
//
|
||||
// TODO(e.burkov): Improve the helper to not rely on timeout.
|
||||
func assertNoResponse(tb testing.TB, outCh <-chan []byte, timeout time.Duration) {
|
||||
tb.Helper()
|
||||
|
||||
var resp []byte
|
||||
require.Panics(tb, func() {
|
||||
resp, _ = testutil.RequireReceive(testutil.NewPanicT(tb), outCh, timeout)
|
||||
})
|
||||
|
||||
require.Nil(tb, resp)
|
||||
}
|
||||
|
||||
// dhcpv4FromPacket extracts the DHCPv4 layer from pkt, which is required to
|
||||
// contain one.
|
||||
func dhcpv4FromPacket(tb testing.TB, pkt gopacket.Packet) (msg *layers.DHCPv4) {
|
||||
|
|
|
|||
|
|
@ -92,7 +92,6 @@ func (iface *dhcpInterfaceV6) handleSolicit(
|
|||
}
|
||||
|
||||
if lease == nil {
|
||||
l.DebugContext(ctx, "no ia_na in solicit or no addresses available")
|
||||
resp.Options = iface.newSolicitRespOpts(fd, req, cliID, iaid, nil, false)
|
||||
|
||||
return respond6(fd, resp)
|
||||
|
|
@ -110,6 +109,12 @@ func (iface *dhcpInterfaceV6) handleSolicit(
|
|||
if err != nil {
|
||||
l.WarnContext(ctx, "committing rapid leases", slogutil.KeyError, err)
|
||||
isRapidCommit = false
|
||||
} else {
|
||||
// The server will also send a Reply in response to a Solicit with a
|
||||
// Rapid Commit option.
|
||||
//
|
||||
// See RFC 9915 Section 18.3.
|
||||
resp.MsgType = layers.DHCPv6MsgTypeReply
|
||||
}
|
||||
|
||||
resp.Options = iface.newSolicitRespOpts(fd, req, cliID, iaid, lease, isRapidCommit)
|
||||
|
|
|
|||
337
internal/dhcpsvc/handler6_test.go
Normal file
337
internal/dhcpsvc/handler6_test.go
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
package dhcpsvc_test
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TODO(e.burkov): Add tests for wrong packets.
|
||||
|
||||
// testIPv6InterfacesConf is the test interfaces configuration for the DHCPv6
|
||||
// part of the [DHCPServer].
|
||||
var testIPv6InterfacesConf = map[string]*dhcpsvc.InterfaceConfig{
|
||||
testIfaceName: {
|
||||
IPv4: disabledIPv4Conf,
|
||||
IPv6: testIPv6Conf,
|
||||
},
|
||||
}
|
||||
|
||||
// testIAID is a common IAID for IANA options in tests.
|
||||
const testIAID = 1
|
||||
|
||||
// testTransactionID is a sample transaction ID for testing.
|
||||
//
|
||||
// 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")
|
||||
)
|
||||
|
||||
func TestDHCPServer_ServeEther6_solicit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
in gopacket.Packet
|
||||
wantOpts layers.DHCPv6Options
|
||||
}{{
|
||||
name: "new",
|
||||
in: newDHCPv6SOLICIT(t, testHWUnknown, testIPv6Unknown, false),
|
||||
wantOpts: layers.DHCPv6Options{
|
||||
newOptServerDUID(t, testIfaceHWAddr),
|
||||
newOptClientDUID(t, testHWUnknown),
|
||||
newOptIANA(t, testIAID, testIPv6Conf.RangeStart),
|
||||
newOptPreference(t, 0),
|
||||
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
|
||||
},
|
||||
}, {
|
||||
name: "existing_static",
|
||||
in: newDHCPv6SOLICIT(t, testHWStatic, testIPv6Static, false),
|
||||
wantOpts: layers.DHCPv6Options{
|
||||
newOptServerDUID(t, testIfaceHWAddr),
|
||||
newOptClientDUID(t, testHWStatic),
|
||||
newOptIANA(t, testIAID, testIPv6Static),
|
||||
newOptPreference(t, 0),
|
||||
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
|
||||
},
|
||||
}, {
|
||||
name: "existing_dynamic",
|
||||
in: newDHCPv6SOLICIT(t, testHWDynamic, testIPv6Dynamic, false),
|
||||
wantOpts: layers.DHCPv6Options{
|
||||
newOptServerDUID(t, testIfaceHWAddr),
|
||||
newOptClientDUID(t, testHWDynamic),
|
||||
newOptIANA(t, testIAID, testIPv6Dynamic),
|
||||
newOptPreference(t, 0),
|
||||
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
|
||||
},
|
||||
}, {
|
||||
name: "existing_expired",
|
||||
in: newDHCPv6SOLICIT(t, testHWExpired, testIPv6Expired, false),
|
||||
wantOpts: layers.DHCPv6Options{
|
||||
newOptServerDUID(t, testIfaceHWAddr),
|
||||
newOptClientDUID(t, testHWExpired),
|
||||
newOptIANA(t, testIAID, testIPv6Expired),
|
||||
newOptPreference(t, 0),
|
||||
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
|
||||
},
|
||||
}, {
|
||||
name: "new_rapid_commit",
|
||||
in: newDHCPv6SOLICIT(t, testHWUnknown, testIPv6Unknown, true),
|
||||
wantOpts: layers.DHCPv6Options{
|
||||
newOptServerDUID(t, testIfaceHWAddr),
|
||||
newOptClientDUID(t, testHWUnknown),
|
||||
newOptIANA(t, testIAID, testIPv6Conf.RangeStart),
|
||||
newOptPreference(t, 0),
|
||||
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
|
||||
layers.NewDHCPv6Option(layers.DHCPv6OptRapidCommit, []byte{}),
|
||||
},
|
||||
}, {
|
||||
name: "existing_rapid_commit",
|
||||
in: newDHCPv6SOLICIT(t, testHWStatic, testIPv6Static, true),
|
||||
wantOpts: layers.DHCPv6Options{
|
||||
newOptServerDUID(t, testIfaceHWAddr),
|
||||
newOptClientDUID(t, testHWStatic),
|
||||
newOptIANA(t, testIAID, testIPv6Static),
|
||||
newOptPreference(t, 0),
|
||||
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
|
||||
layers.NewDHCPv6Option(layers.DHCPv6OptRapidCommit, []byte{}),
|
||||
},
|
||||
}, {
|
||||
name: "existing_dynamic_rapid_commit",
|
||||
in: newDHCPv6SOLICIT(t, testHWDynamic, testIPv6Dynamic, true),
|
||||
wantOpts: layers.DHCPv6Options{
|
||||
newOptServerDUID(t, testIfaceHWAddr),
|
||||
newOptClientDUID(t, testHWDynamic),
|
||||
newOptIANA(t, testIAID, testIPv6Dynamic),
|
||||
newOptPreference(t, 0),
|
||||
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
|
||||
layers.NewDHCPv6Option(layers.DHCPv6OptRapidCommit, []byte{}),
|
||||
},
|
||||
}, {
|
||||
name: "existing_expired_rapid_commit",
|
||||
in: newDHCPv6SOLICIT(t, testHWExpired, testIPv6Expired, true),
|
||||
wantOpts: layers.DHCPv6Options{
|
||||
newOptServerDUID(t, testIfaceHWAddr),
|
||||
newOptClientDUID(t, testHWExpired),
|
||||
newOptIANA(t, testIAID, testIPv6Expired),
|
||||
newOptPreference(t, 0),
|
||||
newOptSolMaxRT(t, dhcpsvc.DefaultSolMaxRT),
|
||||
layers.NewDHCPv6Option(layers.DHCPv6OptRapidCommit, []byte{}),
|
||||
},
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
req := testutil.RequireTypeAssert[*layers.DHCPv6](t, tc.in.Layer(layers.LayerTypeDHCPv6))
|
||||
dbFilePath := newTempDB(t)
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceName, testIfaceAddrV6)
|
||||
startTestDHCPServer(t, &dhcpsvc.Config{
|
||||
Interfaces: testIPv6InterfacesConf,
|
||||
NetworkDeviceManager: ndMgr,
|
||||
Logger: testLogger,
|
||||
DBFilePath: dbFilePath,
|
||||
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,
|
||||
hwAddr net.HardwareAddr,
|
||||
reqIP netip.Addr,
|
||||
rapidCommit bool,
|
||||
) (pkt gopacket.Packet) {
|
||||
tb.Helper()
|
||||
|
||||
eth := newEthernetLayer(tb, hwAddr, nil, layers.EthernetTypeIPv6)
|
||||
|
||||
ip, udp := newIPv6UDPLayer(tb, netip.AddrPort{}, netip.AddrPort{})
|
||||
|
||||
dhcp := &layers.DHCPv6{
|
||||
MsgType: layers.DHCPv6MsgTypeSolicit,
|
||||
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, hwAddr),
|
||||
},
|
||||
}
|
||||
|
||||
if reqIP.IsValid() && reqIP.Is6() {
|
||||
dhcp.Options = append(dhcp.Options, newOptIANA(tb, testIAID, reqIP))
|
||||
}
|
||||
|
||||
if rapidCommit {
|
||||
o := layers.NewDHCPv6Option(layers.DHCPv6OptRapidCommit, nil)
|
||||
dhcp.Options = append(dhcp.Options, o)
|
||||
}
|
||||
|
||||
return newTestPacket(tb, layers.LinkTypeEthernet, eth, ip, udp, dhcp)
|
||||
}
|
||||
|
||||
// newIPv6UDPLayer creates IPv6 and UDP layers for testing. Invalid src is
|
||||
// replaced with an unspecified address and client DHCPv6 port, invalid dst is
|
||||
// replaced with the broadcast address and server DHCPv6 port.
|
||||
func newIPv6UDPLayer(tb testing.TB, src, dst netip.AddrPort) (ip *layers.IPv6, udp *layers.UDP) {
|
||||
tb.Helper()
|
||||
|
||||
if !src.IsValid() {
|
||||
src = netip.AddrPortFrom(netip.IPv6Unspecified(), uint16(dhcpsvc.ClientPortV6))
|
||||
}
|
||||
|
||||
if !dst.IsValid() {
|
||||
bcastAddr, ok := netip.AddrFromSlice(net.IPv6linklocalallnodes)
|
||||
require.True(tb, ok)
|
||||
|
||||
dst = netip.AddrPortFrom(bcastAddr, uint16(dhcpsvc.ServerPortV6))
|
||||
}
|
||||
|
||||
ip = &layers.IPv6{
|
||||
Version: 6,
|
||||
HopLimit: dhcpsvc.IPv6DefaultHopLimit,
|
||||
SrcIP: src.Addr().AsSlice(),
|
||||
DstIP: dst.Addr().AsSlice(),
|
||||
NextHeader: layers.IPProtocolUDP,
|
||||
}
|
||||
udp = &layers.UDP{
|
||||
SrcPort: layers.UDPPort(src.Port()),
|
||||
DstPort: layers.UDPPort(dst.Port()),
|
||||
}
|
||||
require.NoError(tb, udp.SetNetworkLayerForChecksum(ip))
|
||||
|
||||
return ip, udp
|
||||
}
|
||||
|
||||
// newEthernetLayer creates a new Ethernet layer for IP packets of the specified
|
||||
// type. Nil src is replaced with an unspecified MAC address, nil dst is
|
||||
// replaced with a broadcast MAC address, typ must be [layers.EthernetTypeIPv4]
|
||||
// or [layers.EthernetTypeIPv6].
|
||||
func newEthernetLayer(
|
||||
tb testing.TB,
|
||||
src net.HardwareAddr,
|
||||
dst net.HardwareAddr,
|
||||
typ layers.EthernetType,
|
||||
) (eth *layers.Ethernet) {
|
||||
tb.Helper()
|
||||
|
||||
if src == nil {
|
||||
src = net.HardwareAddr{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||
}
|
||||
if dst == nil {
|
||||
dst = net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
|
||||
}
|
||||
|
||||
return &layers.Ethernet{
|
||||
SrcMAC: src,
|
||||
DstMAC: dst,
|
||||
EthernetType: typ,
|
||||
}
|
||||
}
|
||||
|
||||
// assertValidResponse6 asserts that the response received on recvCh is a valid
|
||||
// DHCPv6 response for the given request and contains the expected options. If
|
||||
// wantOpts is nil, it asserts that no response is received.
|
||||
func assertValidResponse6(
|
||||
tb testing.TB,
|
||||
req *layers.DHCPv6,
|
||||
recvCh <-chan []byte,
|
||||
wantOpts layers.DHCPv6Options,
|
||||
) {
|
||||
tb.Helper()
|
||||
|
||||
if wantOpts == nil {
|
||||
assertNoResponse(tb, recvCh, testTimeout/10)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
respData, ok := testutil.RequireReceive(tb, recvCh, testTimeout)
|
||||
require.True(tb, ok)
|
||||
|
||||
ip := &layers.IPv6{}
|
||||
udp := &layers.UDP{}
|
||||
resp := &layers.DHCPv6{}
|
||||
types := requireEthernet(tb, respData, &layers.Ethernet{}, ip, udp, resp)
|
||||
require.Equal(tb, fullLayersStack6, types)
|
||||
|
||||
assertValidDHCPv6(tb, req, resp)
|
||||
|
||||
// TODO(e.burkov): Consider comparing the whole message instead of separate
|
||||
// fields.
|
||||
assert.Equal(tb, req.LinkAddr, resp.LinkAddr, "link address")
|
||||
assert.Equal(tb, req.PeerAddr, resp.PeerAddr, "peer address")
|
||||
assert.Equal(tb, req.TransactionID, resp.TransactionID, "transaction id")
|
||||
assert.Equal(tb, wantOpts, resp.Options, "options")
|
||||
}
|
||||
|
||||
// assertValidDHCPv6 asserts that the response is valid for the given request
|
||||
// according to RFC 9915.
|
||||
//
|
||||
// TODO(e.burkov): Add more checks involving other network layers.
|
||||
func assertValidDHCPv6(
|
||||
tb testing.TB,
|
||||
req *layers.DHCPv6,
|
||||
resp *layers.DHCPv6,
|
||||
) {
|
||||
tb.Helper()
|
||||
|
||||
switch req.MsgType {
|
||||
case
|
||||
layers.DHCPv6MsgTypeRequest,
|
||||
layers.DHCPv6MsgTypeConfirm,
|
||||
layers.DHCPv6MsgTypeRenew,
|
||||
layers.DHCPv6MsgTypeRebind,
|
||||
layers.DHCPv6MsgTypeRelease,
|
||||
layers.DHCPv6MsgTypeDecline,
|
||||
layers.DHCPv6MsgTypeInformationRequest:
|
||||
assert.Equal(tb, layers.DHCPv6MsgTypeReply, resp.MsgType)
|
||||
case layers.DHCPv6MsgTypeSolicit:
|
||||
isRapidCommit := slices.ContainsFunc(resp.Options, func(o layers.DHCPv6Option) (ok bool) {
|
||||
return o.Code == layers.DHCPv6OptRapidCommit
|
||||
})
|
||||
|
||||
if isRapidCommit {
|
||||
assert.Equal(tb, layers.DHCPv6MsgTypeReply, resp.MsgType)
|
||||
} else {
|
||||
assert.Equal(tb, layers.DHCPv6MsgTypeAdverstise, resp.MsgType)
|
||||
}
|
||||
default:
|
||||
tb.Errorf("request message type: %v: %s", errors.ErrUnexpectedValue, req.MsgType)
|
||||
}
|
||||
}
|
||||
|
|
@ -226,6 +226,8 @@ func (iface *netInterface) allocateLease(
|
|||
// reserveLease reserves a lease for a client by its MAC-address. lease is nil
|
||||
// if a new lease can't be allocated. mac must be a valid according to
|
||||
// [netutil.ValidateMAC]. iface.indexMu mutex must be locked.
|
||||
//
|
||||
// TODO(e.burkov): Pass the time moment instead of clock.
|
||||
func (iface *netInterface) reserveLease(
|
||||
ctx context.Context,
|
||||
mac net.HardwareAddr,
|
||||
|
|
|
|||
|
|
@ -18,34 +18,34 @@ import (
|
|||
// See RFC 9915 Section 21.4.
|
||||
const iaNAMinLen = 12
|
||||
|
||||
// iaNAOption represents a parsed IA_NA (Identity Association for Non-temporary
|
||||
// IANAOption represents a parsed IA_NA (Identity Association for Non-temporary
|
||||
// Addresses) option.
|
||||
//
|
||||
// See RFC 9915 Section 21.4.
|
||||
type iaNAOption struct {
|
||||
// nested are the IA Address options nested within this IA_NA.
|
||||
nested []iaAddrOption
|
||||
type IANAOption struct {
|
||||
// Nested are the IA Address options Nested within this IA_NA.
|
||||
Nested []IAAddrOption
|
||||
|
||||
// iaid is the Identity Association IDentifier, a 4-octet value uniquely
|
||||
// ID is the Identity Association Identifier, a 4-octet value uniquely
|
||||
// identifying this IA within the client.
|
||||
//
|
||||
// TODO(e.burkov): Add new type.
|
||||
iaid uint32
|
||||
ID uint32
|
||||
|
||||
// t1 is the time after which the client must contact the same server to
|
||||
// T1 is the time after which the client must contact the same server to
|
||||
// extend the lifetimes of the addresses in this IA.
|
||||
t1 time.Duration
|
||||
T1 time.Duration
|
||||
|
||||
// t2 is the time after which the client may contact any available server to
|
||||
// T2 is the time after which the client may contact any available server to
|
||||
// extend the lifetimes.
|
||||
t2 time.Duration
|
||||
T2 time.Duration
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ encoding.BinaryUnmarshaler = (*iaNAOption)(nil)
|
||||
var _ encoding.BinaryUnmarshaler = (*IANAOption)(nil)
|
||||
|
||||
// UnmarshalBinary implements the [encoding.BinaryUnmarshaler] interface for
|
||||
// *iaNAOption. data should have the following format:
|
||||
// *IANAOption. data should have the following format:
|
||||
//
|
||||
// 0 1 2 3
|
||||
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
|
|
@ -60,16 +60,16 @@ var _ encoding.BinaryUnmarshaler = (*iaNAOption)(nil)
|
|||
// . IA_NA-options .
|
||||
// . .
|
||||
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
func (opt *iaNAOption) UnmarshalBinary(data []byte) (err error) {
|
||||
func (opt *IANAOption) UnmarshalBinary(data []byte) (err error) {
|
||||
err = validate.NoLessThan("data length", len(data), iaNAMinLen)
|
||||
if err != nil {
|
||||
// Don't wrap the error, since it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
opt.iaid = binary.BigEndian.Uint32(data[0:4])
|
||||
opt.t1 = time.Duration(binary.BigEndian.Uint32(data[4:8])) * time.Second
|
||||
opt.t2 = time.Duration(binary.BigEndian.Uint32(data[8:12])) * time.Second
|
||||
opt.ID = binary.BigEndian.Uint32(data[0:4])
|
||||
opt.T1 = time.Duration(binary.BigEndian.Uint32(data[4:8])) * time.Second
|
||||
opt.T2 = time.Duration(binary.BigEndian.Uint32(data[8:12])) * time.Second
|
||||
|
||||
// Parse the nested options that follow the fixed fields.
|
||||
nested := data[iaNAMinLen:]
|
||||
|
|
@ -83,13 +83,13 @@ func (opt *iaNAOption) UnmarshalBinary(data []byte) (err error) {
|
|||
}
|
||||
|
||||
if code == layers.DHCPv6OptIAAddr {
|
||||
addr := iaAddrOption{}
|
||||
addr := IAAddrOption{}
|
||||
err = addr.UnmarshalBinary(nested[4 : 4+l])
|
||||
if err != nil {
|
||||
return fmt.Errorf("nested ia_addr at index %d: %w", i, err)
|
||||
}
|
||||
|
||||
opt.nested = append(opt.nested, addr)
|
||||
opt.Nested = append(opt.Nested, addr)
|
||||
}
|
||||
|
||||
nested = nested[4+l:]
|
||||
|
|
@ -98,21 +98,21 @@ func (opt *iaNAOption) UnmarshalBinary(data []byte) (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Encode serializes ia into a DHCPv6 IA_NA option. Each contained
|
||||
// [iaAddrOption] is encoded as a nested IA Address option.
|
||||
// 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) {
|
||||
func (opt IANAOption) Encode() (iaOpt layers.DHCPv6Option) {
|
||||
// Each nested IA Address option: code (2) + length (2) + data (24).
|
||||
const nestedAddrSize = 2 + 2 + iaAddrDataLen
|
||||
|
||||
data := make([]byte, 0, iaNAMinLen+len(opt.nested)*nestedAddrSize)
|
||||
data := make([]byte, 0, iaNAMinLen+len(opt.Nested)*nestedAddrSize)
|
||||
|
||||
data = binary.BigEndian.AppendUint32(data, opt.iaid)
|
||||
data = binary.BigEndian.AppendUint32(data, uint32(opt.t1.Seconds()))
|
||||
data = binary.BigEndian.AppendUint32(data, uint32(opt.t2.Seconds()))
|
||||
data = binary.BigEndian.AppendUint32(data, opt.ID)
|
||||
data = binary.BigEndian.AppendUint32(data, uint32(opt.T1.Seconds()))
|
||||
data = binary.BigEndian.AppendUint32(data, uint32(opt.T2.Seconds()))
|
||||
|
||||
for _, addr := range opt.nested {
|
||||
for _, addr := range opt.Nested {
|
||||
data = addr.appendTo(data)
|
||||
}
|
||||
|
||||
|
|
@ -125,27 +125,27 @@ func (opt iaNAOption) Encode() (iaOpt layers.DHCPv6Option) {
|
|||
// (4 bytes each).
|
||||
const iaAddrDataLen = 24
|
||||
|
||||
// iaAddrOption represents a parsed IA Address option.
|
||||
// IAAddrOption represents a parsed IA Address option.
|
||||
//
|
||||
// See RFC 9915 Section 21.6.
|
||||
type iaAddrOption struct {
|
||||
// addr is the IPv6 address.
|
||||
addr netip.Addr
|
||||
type IAAddrOption struct {
|
||||
// Addr is the IPv6 address.
|
||||
Addr netip.Addr
|
||||
|
||||
// preferredLifetime is the preferred lifetime of the address. When it is
|
||||
// PreferredLifetime is the preferred lifetime of the address. When it is
|
||||
// zero, the address is deprecated.
|
||||
preferredLifetime time.Duration
|
||||
PreferredLifetime time.Duration
|
||||
|
||||
// validLifetime is the valid lifetime of the address. When it is zero, the
|
||||
// ValidLifetime is the valid lifetime of the address. When it is zero, the
|
||||
// address is no longer valid.
|
||||
validLifetime time.Duration
|
||||
ValidLifetime time.Duration
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ encoding.BinaryUnmarshaler = (*iaAddrOption)(nil)
|
||||
var _ encoding.BinaryUnmarshaler = (*IAAddrOption)(nil)
|
||||
|
||||
// UnmarshalBinary implements the [encoding.BinaryUnmarshaler] interface for
|
||||
// *iaAddrOption. Nested options within IA Address, if any, are
|
||||
// *IAAddrOption. Nested options within IA Address, if any, are
|
||||
// ignored. data should have the following format:
|
||||
//
|
||||
// 0 1 2 3
|
||||
|
|
@ -164,7 +164,7 @@ var _ encoding.BinaryUnmarshaler = (*iaAddrOption)(nil)
|
|||
// . IAaddr-options .
|
||||
// . .
|
||||
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
func (ia *iaAddrOption) UnmarshalBinary(data []byte) (err error) {
|
||||
func (ia *IAAddrOption) UnmarshalBinary(data []byte) (err error) {
|
||||
err = validate.NoLessThan("data length", len(data), iaAddrDataLen)
|
||||
if err != nil {
|
||||
// Don't wrap the error, since it's informative enough as is.
|
||||
|
|
@ -172,30 +172,30 @@ func (ia *iaAddrOption) UnmarshalBinary(data []byte) (err error) {
|
|||
}
|
||||
|
||||
var ok bool
|
||||
ia.addr, ok = netip.AddrFromSlice(data[0:16])
|
||||
ia.Addr, ok = netip.AddrFromSlice(data[0:16])
|
||||
if !ok {
|
||||
return fmt.Errorf("ia_addr: invalid ipv6 address bytes")
|
||||
}
|
||||
|
||||
ia.preferredLifetime = time.Duration(binary.BigEndian.Uint32(data[16:20])) * time.Second
|
||||
ia.validLifetime = time.Duration(binary.BigEndian.Uint32(data[20:24])) * time.Second
|
||||
ia.PreferredLifetime = time.Duration(binary.BigEndian.Uint32(data[16:20])) * time.Second
|
||||
ia.ValidLifetime = time.Duration(binary.BigEndian.Uint32(data[20:24])) * time.Second
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// appendTo returns the data portion of the IA Address option encoding, suitable
|
||||
// for use as a nested option inside an IA_NA.
|
||||
func (ia iaAddrOption) appendTo(orig []byte) (data []byte) {
|
||||
func (ia IAAddrOption) appendTo(orig []byte) (data []byte) {
|
||||
data = orig
|
||||
|
||||
data = binary.BigEndian.AppendUint16(data, uint16(layers.DHCPv6OptIAAddr))
|
||||
data = binary.BigEndian.AppendUint16(data, uint16(iaAddrDataLen))
|
||||
|
||||
// [netip.Addr.AppendBinary] never returns errors.
|
||||
data, _ = ia.addr.AppendBinary(data)
|
||||
data, _ = ia.Addr.AppendBinary(data)
|
||||
|
||||
data = binary.BigEndian.AppendUint32(data, uint32(ia.preferredLifetime.Seconds()))
|
||||
data = binary.BigEndian.AppendUint32(data, uint32(ia.validLifetime.Seconds()))
|
||||
data = binary.BigEndian.AppendUint32(data, uint32(ia.PreferredLifetime.Seconds()))
|
||||
data = binary.BigEndian.AppendUint32(data, uint32(ia.ValidLifetime.Seconds()))
|
||||
|
||||
return data
|
||||
}
|
||||
|
|
@ -236,11 +236,11 @@ func serverDUID6(opts layers.DHCPv6Options) (duid []byte, ok bool) {
|
|||
return findOption6(opts, layers.DHCPv6OptServerID)
|
||||
}
|
||||
|
||||
// solMaxRT is the recommended SOL_MAX_RT value sent to clients. It caps the
|
||||
// client's solicit retransmission interval.
|
||||
// DefaultSolMaxRT is the recommended SOL_MAX_RT value sent to clients. It caps
|
||||
// the client's solicit retransmission interval.
|
||||
//
|
||||
// See RFC 9915 Section 21.24.
|
||||
const solMaxRT = 1 * time.Hour
|
||||
const DefaultSolMaxRT = 1 * time.Hour
|
||||
|
||||
// newPreferenceOption returns a DHCPv6 Preference option with the given value.
|
||||
//
|
||||
|
|
|
|||
88
internal/dhcpsvc/options6_test.go
Normal file
88
internal/dhcpsvc/options6_test.go
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
package dhcpsvc_test
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
|
||||
"github.com/google/gopacket/layers"
|
||||
)
|
||||
|
||||
// newOptIANA creates a DHCPv6 Identity Association for Non-temporary Address
|
||||
// (3) option containing an IA Address with the specified IAID and requested IP
|
||||
// address. reqIP must be a valid IPv6 address. The option will have the T1
|
||||
// and T2 values set to the recommended values based on the [testLeaseTTL]
|
||||
// constant, see the RFC reference in the
|
||||
// [dhcpsvc.DHCPServer.newDHCPInterfaceV6].
|
||||
func newOptIANA(tb testing.TB, iaid uint32, reqIP netip.Addr) (opt layers.DHCPv6Option) {
|
||||
tb.Helper()
|
||||
|
||||
iana := &dhcpsvc.IANAOption{
|
||||
ID: iaid,
|
||||
Nested: []dhcpsvc.IAAddrOption{{
|
||||
PreferredLifetime: testLeaseTTL,
|
||||
ValidLifetime: testLeaseTTL,
|
||||
Addr: reqIP,
|
||||
}},
|
||||
T1: testLeaseTTL / 2,
|
||||
T2: testLeaseTTL * 4 / 5,
|
||||
}
|
||||
|
||||
return iana.Encode()
|
||||
}
|
||||
|
||||
// newOptPreference creates a DHCPv6 Preference (7) option with the specified
|
||||
// preference value.
|
||||
func newOptPreference(tb testing.TB, pref uint8) (opt layers.DHCPv6Option) {
|
||||
tb.Helper()
|
||||
|
||||
return layers.NewDHCPv6Option(layers.DHCPv6OptPreference, []byte{pref})
|
||||
}
|
||||
|
||||
// newOptSolMaxRT creates a DHCPv6 Solicit Message Maximum Retransmission Time
|
||||
// (80) option with the specified maxRT value.
|
||||
func newOptSolMaxRT(tb testing.TB, maxRT time.Duration) (opt layers.DHCPv6Option) {
|
||||
tb.Helper()
|
||||
|
||||
return layers.NewDHCPv6Option(
|
||||
layers.DHCPv6OptSolMaxRt,
|
||||
binary.BigEndian.AppendUint32(nil, uint32(maxRT.Seconds())),
|
||||
)
|
||||
}
|
||||
|
||||
// newOptClientDUID creates a DHCPv6 Client Identifier (1) option containing a
|
||||
// DUID-LL made of cliHWAddr.
|
||||
func newOptClientDUID(tb testing.TB, cliHWAddr net.HardwareAddr) (opt layers.DHCPv6Option) {
|
||||
tb.Helper()
|
||||
|
||||
return newOptDUIDLL(tb, layers.DHCPv6OptClientID, cliHWAddr)
|
||||
}
|
||||
|
||||
// newOptServerID creates a DHCPv6 Server Identifier (2) option containing a
|
||||
// DUID-LL made of srvHWAddr.
|
||||
func newOptServerDUID(tb testing.TB, srvHWAddr net.HardwareAddr) (opt layers.DHCPv6Option) {
|
||||
tb.Helper()
|
||||
|
||||
return newOptDUIDLL(tb, layers.DHCPv6OptServerID, srvHWAddr)
|
||||
}
|
||||
|
||||
// newOptDUIDLL creates a DHCPv6 option with the specified code containing a
|
||||
// DUID-LL made of hwAddr and Ethernet hardware type.
|
||||
func newOptDUIDLL(
|
||||
tb testing.TB,
|
||||
code layers.DHCPv6Opt,
|
||||
hwAddr net.HardwareAddr,
|
||||
) (opt layers.DHCPv6Option) {
|
||||
tb.Helper()
|
||||
|
||||
duid := &layers.DHCPv6DUID{
|
||||
Type: layers.DHCPv6DUIDTypeLL,
|
||||
HardwareType: binary.BigEndian.AppendUint16(nil, uint16(layers.LinkTypeEthernet)),
|
||||
LinkLayerAddress: hwAddr,
|
||||
}
|
||||
|
||||
return layers.NewDHCPv6Option(code, duid.Encode())
|
||||
}
|
||||
|
|
@ -144,6 +144,9 @@ func (srv *DHCPServer) newInterfaces(
|
|||
func (srv *DHCPServer) Start(ctx context.Context) (err error) {
|
||||
srv.logger.DebugContext(ctx, "starting dhcp server")
|
||||
|
||||
// TODO(e.burkov): Create a single device for each network interface with
|
||||
// dual-stack support when possible.
|
||||
|
||||
var errs []error
|
||||
for _, iface := range srv.interfaces4 {
|
||||
netDevName := iface.common.name
|
||||
|
|
@ -153,7 +156,7 @@ func (srv *DHCPServer) Start(ctx context.Context) (err error) {
|
|||
Name: netDevName,
|
||||
})
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
errs = append(errs, fmt.Errorf("opening ipv4 device %q: %w", netDevName, err))
|
||||
|
||||
continue
|
||||
}
|
||||
|
|
@ -166,7 +169,26 @@ func (srv *DHCPServer) Start(ctx context.Context) (err error) {
|
|||
go srv.serveEther4(context.WithoutCancel(ctx), iface, netDev)
|
||||
}
|
||||
|
||||
// TODO(e.burkov): Serve EthernetTypeIPv6.
|
||||
for _, iface := range srv.interfaces6 {
|
||||
netDevName := iface.common.name
|
||||
|
||||
var netDev NetworkDevice
|
||||
netDev, err = srv.deviceManager.Open(ctx, &NetworkDeviceConfig{
|
||||
Name: netDevName,
|
||||
})
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("opening ipv6 device %q: %w", netDevName, err))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
srv.devices = append(srv.devices, container.KeyValue[string, NetworkDevice]{
|
||||
Key: netDevName,
|
||||
Value: netDev,
|
||||
})
|
||||
|
||||
go srv.serveEther6(context.WithoutCancel(ctx), iface, netDev)
|
||||
}
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
|
|
|||
26
internal/dhcpsvc/testdata/TestDHCPServer_ServeEther6_solicit/leases.json
vendored
Normal file
26
internal/dhcpsvc/testdata/TestDHCPServer_ServeEther6_solicit/leases.json
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -210,6 +210,13 @@ func (srv *DHCPServer) newDHCPInterfaceV6(
|
|||
// TODO(e.burkov): Use an ICMP implementation.
|
||||
addrChecker: noopAddressChecker{},
|
||||
subnetPrefix: netip.PrefixFrom(conf.RangeStart, v6PrefLen),
|
||||
// Recommended values for T1 and T2 are 0.5 and 0.8 times the shortest
|
||||
// preferred lifetime of the addresses in the IA that the server is
|
||||
// willing to extend, respectively.
|
||||
//
|
||||
// See RFC 9915 Section 21.4.
|
||||
//
|
||||
// TODO(e.burkov): Consider making configurable.
|
||||
t1: conf.LeaseDuration / 2,
|
||||
t2: conf.LeaseDuration * 4 / 5,
|
||||
raSLAACOnly: conf.RASLAACOnly,
|
||||
|
|
@ -360,11 +367,11 @@ func clientIDMatchingServer(
|
|||
return cliID, nil
|
||||
}
|
||||
|
||||
// defaultHopLimit is the default hop limit for relaying DHCPv6 response
|
||||
// IPv6DefaultHopLimit is the default hop limit for relaying DHCPv6 response
|
||||
// packets.
|
||||
//
|
||||
// See RFC 9915 Section 7.6.
|
||||
const defaultHopLimit = 8
|
||||
const IPv6DefaultHopLimit = 8
|
||||
|
||||
// respond6 constructs and sends a DHCPv6 response to the client.
|
||||
func respond6(fd *frameData6, resp *layers.DHCPv6) (err error) {
|
||||
|
|
@ -377,7 +384,7 @@ func respond6(fd *frameData6, resp *layers.DHCPv6) (err error) {
|
|||
ip := &layers.IPv6{
|
||||
Version: 6,
|
||||
NextHeader: layers.IPProtocolUDP,
|
||||
HopLimit: defaultHopLimit,
|
||||
HopLimit: IPv6DefaultHopLimit,
|
||||
SrcIP: fd.localAddr.AsSlice(),
|
||||
// If the original message was received directly by the server, the
|
||||
// server unicasts the Advertise or Reply message directly to the client
|
||||
|
|
@ -412,7 +419,7 @@ func respond6(fd *frameData6, resp *layers.DHCPv6) (err error) {
|
|||
// option is malformed. lease is nil if there is no address available for
|
||||
// leasing. mac must be a valid MAC address according to [netutil.ValidateMAC],
|
||||
// req must be a valid DHCPv6 message of SOLICIT type, iface.common.indexMu
|
||||
// mutex must be locked.
|
||||
// must be locked.
|
||||
//
|
||||
// TODO(e.burkov): Support allocating several leases at a time when the
|
||||
// database will migrate, see the BUG at [Lease]'s documentation.
|
||||
|
|
@ -422,13 +429,14 @@ func (iface *dhcpInterfaceV6) allocateForSolicit(
|
|||
req *layers.DHCPv6,
|
||||
) (lease *Lease, iaid uint32) {
|
||||
l := iface.common.logger
|
||||
key := macToKey(mac)
|
||||
|
||||
for _, reqOpt := range req.Options {
|
||||
if reqOpt.Code != layers.DHCPv6OptIANA {
|
||||
continue
|
||||
}
|
||||
|
||||
var iana iaNAOption
|
||||
var iana IANAOption
|
||||
err := iana.UnmarshalBinary(reqOpt.Data)
|
||||
if err != nil {
|
||||
// TODO(e.burkov): Recheck the logic on malformed IA_NA options.
|
||||
|
|
@ -437,21 +445,25 @@ func (iface *dhcpInterfaceV6) allocateForSolicit(
|
|||
continue
|
||||
}
|
||||
|
||||
// TODO(e.burkov): Test the case, where the lease exists and is
|
||||
// expired.
|
||||
//
|
||||
var ok bool
|
||||
if lease, ok = iface.common.leases[key]; ok {
|
||||
return lease, iana.ID
|
||||
}
|
||||
|
||||
// TODO(e.burkov): Support allocating the exact requested address if it
|
||||
// is available.
|
||||
lease, err = iface.common.allocateLease(ctx, mac, iface.addrChecker, iface.clock)
|
||||
if err != nil {
|
||||
l.DebugContext(ctx, "no address available", "iaid", iana.iaid, slogutil.KeyError, err)
|
||||
l.DebugContext(ctx, "no address available", "iaid", iana.ID, slogutil.KeyError, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
return lease, iana.iaid
|
||||
return lease, iana.ID
|
||||
}
|
||||
|
||||
l.DebugContext(ctx, "no valid ia_na in solicit")
|
||||
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
|
|
@ -483,7 +495,7 @@ func (iface *dhcpInterfaceV6) newSolicitRespOpts(
|
|||
//
|
||||
// See RFC 9915 Section 18.3.9.
|
||||
opts = append(opts, newPreferenceOption(0))
|
||||
opts = append(opts, newSOLMaxRTOption(solMaxRT))
|
||||
opts = append(opts, newSOLMaxRTOption(DefaultSolMaxRT))
|
||||
|
||||
if rapidCommit {
|
||||
opts = append(opts, layers.NewDHCPv6Option(layers.DHCPv6OptRapidCommit, nil))
|
||||
|
|
@ -501,15 +513,15 @@ func (iface *dhcpInterfaceV6) iaNAFromLease(lease *Lease, iaid uint32) (iana lay
|
|||
return newIANAWithStatus(iaid, layers.DHCPv6StatusCodeNoAddrsAvail)
|
||||
}
|
||||
|
||||
return iaNAOption{
|
||||
nested: []iaAddrOption{{
|
||||
addr: lease.IP,
|
||||
preferredLifetime: iface.common.leaseTTL,
|
||||
validLifetime: iface.common.leaseTTL,
|
||||
return IANAOption{
|
||||
Nested: []IAAddrOption{{
|
||||
Addr: lease.IP,
|
||||
PreferredLifetime: iface.common.leaseTTL,
|
||||
ValidLifetime: iface.common.leaseTTL,
|
||||
}},
|
||||
iaid: iaid,
|
||||
t1: iface.t1,
|
||||
t2: iface.t2,
|
||||
ID: iaid,
|
||||
T1: iface.t1,
|
||||
T2: iface.t2,
|
||||
}.Encode()
|
||||
}
|
||||
|
||||
|
|
@ -532,6 +544,11 @@ func (iface *dhcpInterfaceV6) commit(
|
|||
lease.Hostname = aghnet.GenerateHostname(lease.IP)
|
||||
}
|
||||
|
||||
// TODO(e.burkov): Add the Lease.isExpired. method.
|
||||
if lease.Expiry.Before(iface.clock.Now()) {
|
||||
lease.updateExpiry(iface.clock, iface.common.leaseTTL)
|
||||
}
|
||||
|
||||
err = iface.common.index.update(ctx, iface.common.logger, lease, iface.common)
|
||||
if err != nil {
|
||||
rmErr := iface.common.removeLease(lease)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue