From bd99e3e09d00b7e6a66eee568c0133b6fce0fdc6 Mon Sep 17 00:00:00 2001 From: Eugene Burkov Date: Fri, 16 Feb 2024 19:19:00 +0300 Subject: [PATCH 1/2] Pull request 2150: AG-28455 rc versions Squashed commit of the following: commit 9b80bf2da8676c7a80982b88b547b35760afd4dd Merge: 2c184158c fede29794 Author: Eugene Burkov Date: Fri Feb 16 15:49:23 2024 +0300 Merge branch 'master' into AG-28455-rc-versions commit 2c184158c052dc1ddc57f4bdf53ad31a50410659 Author: Eugene Burkov Date: Wed Feb 14 13:01:30 2024 +0300 scripts: imp code commit f0965058ad2231b342cf406a8434d76cc3f546c2 Author: Eugene Burkov Date: Tue Feb 13 14:52:10 2024 +0300 scripts: imp code commit a5bed23c7077ea3655ae88528c1dfc5ea9d061a7 Author: Eugene Burkov Date: Mon Feb 12 18:35:52 2024 +0300 scripts: fix typo commit 36e9ea1ac3403a53452fcc16d35d32ec7663fd1e Author: Eugene Burkov Date: Mon Feb 12 15:41:57 2024 +0300 scripts: revert changes, imp docs commit d74c85d4ec77b12ceaae28db5a8e7961896d688a Author: Eugene Burkov Date: Mon Feb 5 18:12:38 2024 +0300 all: mark rc versions separately --- bamboo-specs/test.yaml | 16 +++++++++++++++- scripts/make/go-build.sh | 4 ++-- scripts/make/version.sh | 31 ++++++++++++++++++++++++++++--- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/bamboo-specs/test.yaml b/bamboo-specs/test.yaml index 239fe8a7..532e7f4b 100644 --- a/bamboo-specs/test.yaml +++ b/bamboo-specs/test.yaml @@ -6,6 +6,7 @@ 'name': 'AdGuard Home - Build and run tests' 'variables': 'dockerGo': 'adguard/golang-ubuntu:8.0' + 'channel': 'development' 'stages': - 'Tests': @@ -73,7 +74,7 @@ make\ ARCH="amd64"\ OS="windows darwin linux"\ - CHANNEL="development"\ + CHANNEL=${bamboo.channel}\ SIGN=0\ PARALLELISM=1\ VERBOSE=2\ @@ -115,3 +116,16 @@ 'labels': [] 'other': 'concurrent-build-plugin': 'system-default' + +'branch-overrides': + # rc-vX.Y.Z branches are the release candidate branches. They are created + # from the release branch and are used to build the release candidate + # images. + - '^rc-v[0-9]+\.[0-9]+\.[0-9]+': + # Build betas on release branches manually. + 'triggers': [] + # Set the default release channel on the release branch to beta, as we + # may need to build a few of these. + 'variables': + 'dockerGo': 'adguard/golang-ubuntu:8.0' + 'channel': 'candidate' diff --git a/scripts/make/go-build.sh b/scripts/make/go-build.sh index 7f629cd8..48d68c00 100644 --- a/scripts/make/go-build.sh +++ b/scripts/make/go-build.sh @@ -51,12 +51,12 @@ readonly channel case "$channel" in -('development'|'edge'|'beta'|'release') +('development'|'edge'|'beta'|'release'|'candidate') # All is well, go on. ;; (*) echo "invalid channel '$channel', supported values are\ - 'development', 'edge', 'beta', and 'release'" 1>&2 + 'development', 'edge', 'beta', 'release', and 'candidate'" 1>&2 exit 1 ;; esac diff --git a/scripts/make/version.sh b/scripts/make/version.sh index 42eee5fc..c5715191 100644 --- a/scripts/make/version.sh +++ b/scripts/make/version.sh @@ -43,7 +43,7 @@ bump_minor='/^v[0-9]+\.[0-9]+\.0$/ { } { - printf("invalid release version: \"%s\"\n", $0); + printf("invalid minor release version: \"%s\"\n", $0); exit 1; }' @@ -128,15 +128,40 @@ in version="$last_tag" ;; +('candidate') + # This pseudo-channel is used to set a proper versions into release + # candidate builds. + + # last_tag is expected to be the latest release tag. + last_tag="$( git describe --abbrev=0 )" + readonly last_tag + + # current_branch is the name of the branch currently checked out. + current_branch="$( git rev-parse --abbrev-ref HEAD )" + readonly current_branch + + # The branch should be named like: + # + # rc-v12.34.56 + # + if ! echo "$current_branch" | grep -E -e '^rc-v[0-9]+\.[0-9]+\.[0-9]+$' -q + then + echo "invalid release candidate branch name '$current_branch'" 1>&2 + + exit 1 + fi + + version="${current_branch#rc-}-rc.$( git rev-list --count "$last_tag"..HEAD )" + ;; (*) echo "invalid channel '$channel', supported values are\ - 'development', 'edge', 'beta', and 'release'" 1>&2 + 'development', 'edge', 'beta', 'release' and 'candidate'" 1>&2 exit 1 ;; esac # Finally, make sure that we don't output invalid versions. -if ! echo "$version" | grep -E -e '^v[0-9]+\.[0-9]+\.[0-9]+(-(a|b|dev)\.[0-9]+)?(\+[[:xdigit:]]+)?$' -q +if ! echo "$version" | grep -E -e '^v[0-9]+\.[0-9]+\.[0-9]+(-(a|b|dev|rc)\.[0-9]+)?(\+[[:xdigit:]]+)?$' -q then echo "generated an invalid version '$version'" 1>&2 From 6fd0a624ca372ee659f22589fe0a9200edea5399 Mon Sep 17 00:00:00 2001 From: Eugene Burkov Date: Tue, 20 Feb 2024 14:52:38 +0300 Subject: [PATCH 2/2] Pull request 2152: 4923 gopacket DHCP vol.7 Updates #4923. Squashed commit of the following: commit 0f90eb3596fcbca0d87cb4eb857d45aea26f3854 Merge: 38b3165b6 bd99e3e09 Author: Eugene Burkov Date: Mon Feb 19 20:11:38 2024 +0300 Merge branch 'master' into 4923-gopacket-dhcp-vol.7 commit 38b3165b696c9a3f69484d8e4c2c847340ed9363 Author: Eugene Burkov Date: Mon Feb 19 14:52:01 2024 +0300 dhcpsvc: imp docs commit 0a078920a20de9fb2d864c90d2311800c6f3bc3f Author: Eugene Burkov Date: Mon Feb 19 14:48:19 2024 +0300 dhcpsvc: imp code commit 30691f0d989c48b2f0dff8079952615dbfbdaea1 Author: Eugene Burkov Date: Thu Feb 15 19:57:41 2024 +0300 dhcpsvc: imp code, dry commit 20f5ef80fb2d1cad869883da3684a01e5b8b3315 Author: Eugene Burkov Date: Thu Feb 15 15:57:09 2024 +0300 dhcpsvc: finish leases methods --- internal/dhcpsvc/dhcpsvc.go | 20 +- internal/dhcpsvc/interface.go | 26 ++ internal/dhcpsvc/leaseindex.go | 126 +++++++++ internal/dhcpsvc/server.go | 103 +++++--- internal/dhcpsvc/server_test.go | 445 +++++++++++++++++++++++++++++--- internal/dhcpsvc/v6.go | 2 +- 6 files changed, 639 insertions(+), 83 deletions(-) create mode 100644 internal/dhcpsvc/leaseindex.go diff --git a/internal/dhcpsvc/dhcpsvc.go b/internal/dhcpsvc/dhcpsvc.go index 8ab2cab7..41e0037e 100644 --- a/internal/dhcpsvc/dhcpsvc.go +++ b/internal/dhcpsvc/dhcpsvc.go @@ -14,7 +14,9 @@ import ( // Interface is a DHCP service. // // TODO(e.burkov): Separate HostByIP, MACByIP, IPByHost into a separate -// interface. This is also valid for Enabled method. +// interface. This is also applicable to Enabled method. +// +// TODO(e.burkov): Reconsider the requirements for the leases validity. type Interface interface { agh.ServiceWithConfig[*Config] @@ -29,6 +31,8 @@ type Interface interface { // MACByIP returns the MAC address for the given IP address leased. It // returns nil if there is no such client, due to an assumption that a DHCP // client must always have a MAC address. + // + // TODO(e.burkov): Think of a contract for the returned value. MACByIP(ip netip.Addr) (mac net.HardwareAddr) // IPByHost returns the IP address of the DHCP client with the given @@ -44,17 +48,17 @@ type Interface interface { // signatures instead of cloning the whole list. Leases() (ls []*Lease) - // AddLease adds a new DHCP lease. It returns an error if the lease is - // invalid or already exists. + // AddLease adds a new DHCP lease. l must be valid. It returns an error if + // l already exists. AddLease(l *Lease) (err error) - // UpdateStaticLease changes an existing DHCP lease. It returns an error if - // there is no lease with such hardware addressor if new values are invalid - // or already exist. + // UpdateStaticLease replaces an existing static DHCP lease. l must be + // valid. It returns an error if the lease with the given hardware address + // doesn't exist or if other values match another existing lease. UpdateStaticLease(l *Lease) (err error) - // RemoveLease removes an existing DHCP lease. It returns an error if there - // is no lease equal to l. + // RemoveLease removes an existing DHCP lease. l must be valid. It returns + // an error if there is no lease equal to l. RemoveLease(l *Lease) (err error) // Reset removes all the DHCP leases. diff --git a/internal/dhcpsvc/interface.go b/internal/dhcpsvc/interface.go index 5a96e261..ebb225e6 100644 --- a/internal/dhcpsvc/interface.go +++ b/internal/dhcpsvc/interface.go @@ -38,3 +38,29 @@ func (iface *netInterface) insertLease(l *Lease) (err error) { return nil } + +// updateLease replaces an existing lease within iface with the given one. It +// returns an error if there is no lease with such hardware address. +func (iface *netInterface) updateLease(l *Lease) (prev *Lease, err error) { + i, found := slices.BinarySearchFunc(iface.leases, l, compareLeaseMAC) + if !found { + return nil, fmt.Errorf("no lease for mac %s", l.HWAddr) + } + + prev, iface.leases[i] = iface.leases[i], l + + return prev, nil +} + +// removeLease removes an existing lease from iface. It returns an error if +// there is no lease equal to l. +func (iface *netInterface) removeLease(l *Lease) (err error) { + i, found := slices.BinarySearchFunc(iface.leases, l, compareLeaseMAC) + if !found { + return fmt.Errorf("no lease for mac %s", l.HWAddr) + } + + iface.leases = slices.Delete(iface.leases, i, i+1) + + return nil +} diff --git a/internal/dhcpsvc/leaseindex.go b/internal/dhcpsvc/leaseindex.go new file mode 100644 index 00000000..c9487b75 --- /dev/null +++ b/internal/dhcpsvc/leaseindex.go @@ -0,0 +1,126 @@ +package dhcpsvc + +import ( + "fmt" + "net/netip" + "slices" + "strings" +) + +// leaseIndex is the set of leases indexed by their identifiers for quick +// lookup. +type leaseIndex struct { + // byAddr is a lookup shortcut for leases by their IP addresses. + byAddr map[netip.Addr]*Lease + + // byName is a lookup shortcut for leases by their hostnames. + // + // TODO(e.burkov): Use a slice of leases with the same hostname? + byName map[string]*Lease +} + +// newLeaseIndex returns a new index for [Lease]s. +func newLeaseIndex() *leaseIndex { + return &leaseIndex{ + byAddr: map[netip.Addr]*Lease{}, + byName: map[string]*Lease{}, + } +} + +// leaseByAddr returns a lease by its IP address. +func (idx *leaseIndex) leaseByAddr(addr netip.Addr) (l *Lease, ok bool) { + l, ok = idx.byAddr[addr] + + return l, ok +} + +// leaseByName returns a lease by its hostname. +func (idx *leaseIndex) leaseByName(name string) (l *Lease, ok bool) { + // TODO(e.burkov): Probably, use a case-insensitive comparison and store in + // slice. This would require a benchmark. + l, ok = idx.byName[strings.ToLower(name)] + + return l, ok +} + +// clear removes all leases from idx. +func (idx *leaseIndex) clear() { + clear(idx.byAddr) + clear(idx.byName) +} + +// add adds l into idx and into iface. l must be valid, iface should be +// responsible for l's IP. It returns an error if l duplicates at least a +// single value of another lease. +func (idx *leaseIndex) add(l *Lease, iface *netInterface) (err error) { + loweredName := strings.ToLower(l.Hostname) + + if _, ok := idx.byAddr[l.IP]; ok { + return fmt.Errorf("lease for ip %s already exists", l.IP) + } else if _, ok = idx.byName[loweredName]; ok { + return fmt.Errorf("lease for hostname %s already exists", l.Hostname) + } + + err = iface.insertLease(l) + if err != nil { + return err + } + + idx.byAddr[l.IP] = l + idx.byName[loweredName] = l + + return nil +} + +// remove removes l from idx and from iface. l must be valid, iface should +// contain the same lease or the lease itself. It returns an error if the lease +// not found. +func (idx *leaseIndex) remove(l *Lease, iface *netInterface) (err error) { + loweredName := strings.ToLower(l.Hostname) + + if _, ok := idx.byAddr[l.IP]; !ok { + return fmt.Errorf("no lease for ip %s", l.IP) + } else if _, ok = idx.byName[loweredName]; !ok { + return fmt.Errorf("no lease for hostname %s", l.Hostname) + } + + err = iface.removeLease(l) + if err != nil { + return err + } + + delete(idx.byAddr, l.IP) + delete(idx.byName, loweredName) + + return nil +} + +// update updates l in idx and in iface. l must be valid, iface should be +// responsible for l's IP. It returns an error if l duplicates at least a +// single value of another lease, except for the updated lease itself. +func (idx *leaseIndex) update(l *Lease, iface *netInterface) (err error) { + loweredName := strings.ToLower(l.Hostname) + + existing, ok := idx.byAddr[l.IP] + if ok && !slices.Equal(l.HWAddr, existing.HWAddr) { + return fmt.Errorf("lease for ip %s already exists", l.IP) + } + + existing, ok = idx.byName[loweredName] + if ok && !slices.Equal(l.HWAddr, existing.HWAddr) { + return fmt.Errorf("lease for hostname %s already exists", l.Hostname) + } + + prev, err := iface.updateLease(l) + if err != nil { + return err + } + + delete(idx.byAddr, prev.IP) + delete(idx.byName, strings.ToLower(prev.Hostname)) + + idx.byAddr[l.IP] = l + idx.byName[loweredName] = l + + return nil +} diff --git a/internal/dhcpsvc/server.go b/internal/dhcpsvc/server.go index 1e07363d..bc354b00 100644 --- a/internal/dhcpsvc/server.go +++ b/internal/dhcpsvc/server.go @@ -5,11 +5,11 @@ import ( "net" "net/netip" "slices" - "strings" "sync" "sync/atomic" "time" + "github.com/AdguardTeam/golibs/errors" "golang.org/x/exp/maps" ) @@ -23,17 +23,11 @@ type DHCPServer struct { // hostnames. localTLD string - // leasesMu protects the ipIndex and nameIndex fields against concurrent - // access, as well as leaseHandlers within the interfaces. + // leasesMu protects the leases index as well as leases in the interfaces. leasesMu *sync.RWMutex - // leaseByIP is a lookup shortcut for leases by their IP addresses. - leaseByIP map[netip.Addr]*Lease - - // leaseByName is a lookup shortcut for leases by their hostnames. - // - // TODO(e.burkov): Use a slice of leases with the same hostname? - leaseByName map[string]*Lease + // leases stores the DHCP leases for quick lookups. + leases *leaseIndex // interfaces4 is the set of IPv4 interfaces sorted by interface name. interfaces4 netInterfacesV4 @@ -88,8 +82,7 @@ func New(conf *Config) (srv *DHCPServer, err error) { enabled: enabled, localTLD: conf.LocalDomainName, leasesMu: &sync.RWMutex{}, - leaseByIP: map[netip.Addr]*Lease{}, - leaseByName: map[string]*Lease{}, + leases: newLeaseIndex(), interfaces4: ifaces4, interfaces6: ifaces6, icmpTimeout: conf.ICMPTimeout, @@ -120,6 +113,11 @@ func (srv *DHCPServer) Leases() (leases []*Lease) { leases = append(leases, lease.Clone()) } } + for _, iface := range srv.interfaces6 { + for _, lease := range iface.leases { + leases = append(leases, lease.Clone()) + } + } return leases } @@ -129,7 +127,7 @@ func (srv *DHCPServer) HostByIP(ip netip.Addr) (host string) { srv.leasesMu.RLock() defer srv.leasesMu.RUnlock() - if l, ok := srv.leaseByIP[ip]; ok { + if l, ok := srv.leases.leaseByAddr(ip); ok { return l.Hostname } @@ -141,7 +139,7 @@ func (srv *DHCPServer) MACByIP(ip netip.Addr) (mac net.HardwareAddr) { srv.leasesMu.RLock() defer srv.leasesMu.RUnlock() - if l, ok := srv.leaseByIP[ip]; ok { + if l, ok := srv.leases.leaseByAddr(ip); ok { return l.HWAddr } @@ -150,12 +148,10 @@ func (srv *DHCPServer) MACByIP(ip netip.Addr) (mac net.HardwareAddr) { // IPByHost implements the [Interface] interface for *DHCPServer. func (srv *DHCPServer) IPByHost(host string) (ip netip.Addr) { - lowered := strings.ToLower(host) - srv.leasesMu.RLock() defer srv.leasesMu.RUnlock() - if l, ok := srv.leaseByName[lowered]; ok { + if l, ok := srv.leases.leaseByName(host); ok { return l.IP } @@ -173,39 +169,76 @@ func (srv *DHCPServer) Reset() (err error) { for _, iface := range srv.interfaces6 { iface.reset() } - - clear(srv.leaseByIP) - clear(srv.leaseByName) + srv.leases.clear() return nil } // AddLease implements the [Interface] interface for *DHCPServer. func (srv *DHCPServer) AddLease(l *Lease) (err error) { - var ok bool - var iface *netInterface + defer func() { err = errors.Annotate(err, "adding lease: %w") }() addr := l.IP + iface, err := srv.ifaceForAddr(addr) + if err != nil { + // Don't wrap the error since there is already an annotation deferred. + return err + } + srv.leasesMu.Lock() + defer srv.leasesMu.Unlock() + + return srv.leases.add(l, iface) +} + +// UpdateStaticLease implements the [Interface] interface for *DHCPServer. +// +// TODO(e.burkov): Support moving leases between interfaces. +func (srv *DHCPServer) UpdateStaticLease(l *Lease) (err error) { + defer func() { err = errors.Annotate(err, "updating static lease: %w") }() + + addr := l.IP + iface, err := srv.ifaceForAddr(addr) + if err != nil { + // Don't wrap the error since there is already an annotation deferred. + return err + } + + srv.leasesMu.Lock() + defer srv.leasesMu.Unlock() + + return srv.leases.update(l, iface) +} + +// RemoveLease implements the [Interface] interface for *DHCPServer. +func (srv *DHCPServer) RemoveLease(l *Lease) (err error) { + defer func() { err = errors.Annotate(err, "removing lease: %w") }() + + addr := l.IP + iface, err := srv.ifaceForAddr(addr) + if err != nil { + // Don't wrap the error since there is already an annotation deferred. + return err + } + + srv.leasesMu.Lock() + defer srv.leasesMu.Unlock() + + return srv.leases.remove(l, iface) +} + +// ifaceForAddr returns the handled network interface for the given IP address, +// or an error if no such interface exists. +func (srv *DHCPServer) ifaceForAddr(addr netip.Addr) (iface *netInterface, err error) { + var ok bool if addr.Is4() { iface, ok = srv.interfaces4.find(addr) } else { iface, ok = srv.interfaces6.find(addr) } if !ok { - return fmt.Errorf("no interface for IP address %s", addr) + return nil, fmt.Errorf("no interface for ip %s", addr) } - srv.leasesMu.Lock() - defer srv.leasesMu.Unlock() - - err = iface.insertLease(l) - if err != nil { - return err - } - - srv.leaseByIP[l.IP] = l - srv.leaseByName[strings.ToLower(l.Hostname)] = l - - return nil + return iface, nil } diff --git a/internal/dhcpsvc/server_test.go b/internal/dhcpsvc/server_test.go index 3db16000..6d5bc9d8 100644 --- a/internal/dhcpsvc/server_test.go +++ b/internal/dhcpsvc/server_test.go @@ -3,6 +3,7 @@ package dhcpsvc_test import ( "net" "net/netip" + "strings" "testing" "time" @@ -15,6 +16,52 @@ import ( // testLocalTLD is a common local TLD for tests. const testLocalTLD = "local" +// testInterfaceConf is a common set of interface configurations for tests. +var testInterfaceConf = map[string]*dhcpsvc.InterfaceConfig{ + "eth0": { + IPv4: &dhcpsvc.IPv4Config{ + Enabled: true, + GatewayIP: netip.MustParseAddr("192.168.0.1"), + SubnetMask: netip.MustParseAddr("255.255.255.0"), + RangeStart: netip.MustParseAddr("192.168.0.2"), + RangeEnd: netip.MustParseAddr("192.168.0.254"), + LeaseDuration: 1 * time.Hour, + }, + IPv6: &dhcpsvc.IPv6Config{ + Enabled: true, + RangeStart: netip.MustParseAddr("2001:db8::1"), + LeaseDuration: 1 * time.Hour, + RAAllowSLAAC: true, + RASLAACOnly: true, + }, + }, + "eth1": { + IPv4: &dhcpsvc.IPv4Config{ + Enabled: true, + GatewayIP: netip.MustParseAddr("172.16.0.1"), + SubnetMask: netip.MustParseAddr("255.255.255.0"), + RangeStart: netip.MustParseAddr("172.16.0.2"), + RangeEnd: netip.MustParseAddr("172.16.0.255"), + LeaseDuration: 1 * time.Hour, + }, + IPv6: &dhcpsvc.IPv6Config{ + Enabled: true, + RangeStart: netip.MustParseAddr("2001:db9::1"), + LeaseDuration: 1 * time.Hour, + RAAllowSLAAC: true, + RASLAACOnly: true, + }, + }, +} + +// mustParseMAC parses a hardware address from s and requires no errors. +func mustParseMAC(t require.TestingT, s string) (mac net.HardwareAddr) { + mac, err := net.ParseMAC(s) + require.NoError(t, err) + + return mac +} + func TestNew(t *testing.T) { validIPv4Conf := &dhcpsvc.IPv4Config{ Enabled: true, @@ -117,46 +164,113 @@ func TestNew(t *testing.T) { } } +func TestDHCPServer_AddLease(t *testing.T) { + srv, err := dhcpsvc.New(&dhcpsvc.Config{ + Enabled: true, + LocalDomainName: testLocalTLD, + Interfaces: testInterfaceConf, + }) + require.NoError(t, err) + + const ( + host1 = "host1" + host2 = "host2" + host3 = "host3" + ) + + ip1 := netip.MustParseAddr("192.168.0.2") + ip2 := netip.MustParseAddr("192.168.0.3") + ip3 := netip.MustParseAddr("2001:db8::2") + + mac1 := mustParseMAC(t, "01:02:03:04:05:06") + mac2 := mustParseMAC(t, "06:05:04:03:02:01") + mac3 := mustParseMAC(t, "02:03:04:05:06:07") + + require.NoError(t, srv.AddLease(&dhcpsvc.Lease{ + Hostname: host1, + IP: ip1, + HWAddr: mac1, + IsStatic: true, + })) + + testCases := []struct { + name string + lease *dhcpsvc.Lease + wantErrMsg string + }{{ + name: "outside_range", + lease: &dhcpsvc.Lease{ + Hostname: host2, + IP: netip.MustParseAddr("1.2.3.4"), + HWAddr: mac2, + }, + wantErrMsg: "adding lease: no interface for ip 1.2.3.4", + }, { + name: "duplicate_ip", + lease: &dhcpsvc.Lease{ + Hostname: host2, + IP: ip1, + HWAddr: mac2, + }, + wantErrMsg: "adding lease: lease for ip " + ip1.String() + + " already exists", + }, { + name: "duplicate_hostname", + lease: &dhcpsvc.Lease{ + Hostname: host1, + IP: ip2, + HWAddr: mac2, + }, + wantErrMsg: "adding lease: lease for hostname " + host1 + + " already exists", + }, { + name: "duplicate_hostname_case", + lease: &dhcpsvc.Lease{ + Hostname: strings.ToUpper(host1), + IP: ip2, + HWAddr: mac2, + }, + wantErrMsg: "adding lease: lease for hostname " + + strings.ToUpper(host1) + " already exists", + }, { + name: "duplicate_mac", + lease: &dhcpsvc.Lease{ + Hostname: host2, + IP: ip2, + HWAddr: mac1, + }, + wantErrMsg: "adding lease: lease for mac " + mac1.String() + + " already exists", + }, { + name: "valid", + lease: &dhcpsvc.Lease{ + Hostname: host2, + IP: ip2, + HWAddr: mac2, + }, + wantErrMsg: "", + }, { + name: "valid_v6", + lease: &dhcpsvc.Lease{ + Hostname: host3, + IP: ip3, + HWAddr: mac3, + }, + wantErrMsg: "", + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.AddLease(tc.lease)) + }) + } +} + func TestDHCPServer_index(t *testing.T) { srv, err := dhcpsvc.New(&dhcpsvc.Config{ Enabled: true, LocalDomainName: testLocalTLD, - Interfaces: map[string]*dhcpsvc.InterfaceConfig{ - "eth0": { - IPv4: &dhcpsvc.IPv4Config{ - Enabled: true, - GatewayIP: netip.MustParseAddr("192.168.0.1"), - SubnetMask: netip.MustParseAddr("255.255.255.0"), - RangeStart: netip.MustParseAddr("192.168.0.2"), - RangeEnd: netip.MustParseAddr("192.168.0.254"), - LeaseDuration: 1 * time.Hour, - }, - IPv6: &dhcpsvc.IPv6Config{ - Enabled: true, - RangeStart: netip.MustParseAddr("2001:db8::1"), - LeaseDuration: 1 * time.Hour, - RAAllowSLAAC: true, - RASLAACOnly: true, - }, - }, - "eth1": { - IPv4: &dhcpsvc.IPv4Config{ - Enabled: true, - GatewayIP: netip.MustParseAddr("172.16.0.1"), - SubnetMask: netip.MustParseAddr("255.255.255.0"), - RangeStart: netip.MustParseAddr("172.16.0.2"), - RangeEnd: netip.MustParseAddr("172.16.0.255"), - LeaseDuration: 1 * time.Hour, - }, - IPv6: &dhcpsvc.IPv6Config{ - Enabled: true, - RangeStart: netip.MustParseAddr("2001:db9::1"), - LeaseDuration: 1 * time.Hour, - RAAllowSLAAC: true, - RASLAACOnly: true, - }, - }, - }, + Interfaces: testInterfaceConf, }) require.NoError(t, err) @@ -173,9 +287,9 @@ func TestDHCPServer_index(t *testing.T) { ip3 := netip.MustParseAddr("172.16.0.3") ip4 := netip.MustParseAddr("172.16.0.4") - mac1 := net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x06} - mac2 := net.HardwareAddr{0x06, 0x05, 0x04, 0x03, 0x02, 0x01} - mac3 := net.HardwareAddr{0x05, 0x04, 0x03, 0x02, 0x01, 0x00} + mac1 := mustParseMAC(t, "01:02:03:04:05:06") + mac2 := mustParseMAC(t, "06:05:04:03:02:01") + mac3 := mustParseMAC(t, "02:03:04:05:06:07") leases := []*dhcpsvc.Lease{{ Hostname: host1, @@ -226,3 +340,256 @@ func TestDHCPServer_index(t *testing.T) { assert.Nil(t, srv.MACByIP(netip.Addr{})) }) } + +func TestDHCPServer_UpdateStaticLease(t *testing.T) { + srv, err := dhcpsvc.New(&dhcpsvc.Config{ + Enabled: true, + LocalDomainName: testLocalTLD, + Interfaces: testInterfaceConf, + }) + require.NoError(t, err) + + const ( + host1 = "host1" + host2 = "host2" + host3 = "host3" + host4 = "host4" + host5 = "host5" + host6 = "host6" + ) + + ip1 := netip.MustParseAddr("192.168.0.2") + ip2 := netip.MustParseAddr("192.168.0.3") + ip3 := netip.MustParseAddr("192.168.0.4") + ip4 := netip.MustParseAddr("2001:db8::2") + ip5 := netip.MustParseAddr("2001:db8::3") + + mac1 := mustParseMAC(t, "01:02:03:04:05:06") + mac2 := mustParseMAC(t, "01:02:03:04:05:07") + mac3 := mustParseMAC(t, "06:05:04:03:02:01") + mac4 := mustParseMAC(t, "06:05:04:03:02:02") + + leases := []*dhcpsvc.Lease{{ + Hostname: host1, + IP: ip1, + HWAddr: mac1, + IsStatic: true, + }, { + Hostname: host2, + IP: ip2, + HWAddr: mac2, + IsStatic: true, + }, { + Hostname: host4, + IP: ip4, + HWAddr: mac4, + IsStatic: true, + }} + for _, l := range leases { + require.NoError(t, srv.AddLease(l)) + } + + testCases := []struct { + name string + lease *dhcpsvc.Lease + wantErrMsg string + }{{ + name: "outside_range", + lease: &dhcpsvc.Lease{ + Hostname: host1, + IP: netip.MustParseAddr("1.2.3.4"), + HWAddr: mac1, + }, + wantErrMsg: "updating static lease: no interface for ip 1.2.3.4", + }, { + name: "not_found", + lease: &dhcpsvc.Lease{ + Hostname: host3, + IP: ip3, + HWAddr: mac3, + }, + wantErrMsg: "updating static lease: no lease for mac " + mac3.String(), + }, { + name: "duplicate_ip", + lease: &dhcpsvc.Lease{ + Hostname: host1, + IP: ip2, + HWAddr: mac1, + }, + wantErrMsg: "updating static lease: lease for ip " + ip2.String() + + " already exists", + }, { + name: "duplicate_hostname", + lease: &dhcpsvc.Lease{ + Hostname: host2, + IP: ip1, + HWAddr: mac1, + }, + wantErrMsg: "updating static lease: lease for hostname " + host2 + + " already exists", + }, { + name: "duplicate_hostname_case", + lease: &dhcpsvc.Lease{ + Hostname: strings.ToUpper(host2), + IP: ip1, + HWAddr: mac1, + }, + wantErrMsg: "updating static lease: lease for hostname " + + strings.ToUpper(host2) + " already exists", + }, { + name: "valid", + lease: &dhcpsvc.Lease{ + Hostname: host3, + IP: ip3, + HWAddr: mac1, + }, + wantErrMsg: "", + }, { + name: "valid_v6", + lease: &dhcpsvc.Lease{ + Hostname: host6, + IP: ip5, + HWAddr: mac4, + }, + wantErrMsg: "", + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.UpdateStaticLease(tc.lease)) + }) + } +} + +func TestDHCPServer_RemoveLease(t *testing.T) { + srv, err := dhcpsvc.New(&dhcpsvc.Config{ + Enabled: true, + LocalDomainName: testLocalTLD, + Interfaces: testInterfaceConf, + }) + require.NoError(t, err) + + const ( + host1 = "host1" + host2 = "host2" + host3 = "host3" + ) + + ip1 := netip.MustParseAddr("192.168.0.2") + ip2 := netip.MustParseAddr("192.168.0.3") + ip3 := netip.MustParseAddr("2001:db8::2") + + mac1 := mustParseMAC(t, "01:02:03:04:05:06") + mac2 := mustParseMAC(t, "02:03:04:05:06:07") + mac3 := mustParseMAC(t, "06:05:04:03:02:01") + + leases := []*dhcpsvc.Lease{{ + Hostname: host1, + IP: ip1, + HWAddr: mac1, + IsStatic: true, + }, { + Hostname: host3, + IP: ip3, + HWAddr: mac3, + IsStatic: true, + }} + for _, l := range leases { + require.NoError(t, srv.AddLease(l)) + } + + testCases := []struct { + name string + lease *dhcpsvc.Lease + wantErrMsg string + }{{ + name: "not_found_mac", + lease: &dhcpsvc.Lease{ + Hostname: host1, + IP: ip1, + HWAddr: mac2, + }, + wantErrMsg: "removing lease: no lease for mac " + mac2.String(), + }, { + name: "not_found_ip", + lease: &dhcpsvc.Lease{ + Hostname: host1, + IP: ip2, + HWAddr: mac1, + }, + wantErrMsg: "removing lease: no lease for ip " + ip2.String(), + }, { + name: "not_found_host", + lease: &dhcpsvc.Lease{ + Hostname: host2, + IP: ip1, + HWAddr: mac1, + }, + wantErrMsg: "removing lease: no lease for hostname " + host2, + }, { + name: "valid", + lease: &dhcpsvc.Lease{ + Hostname: host1, + IP: ip1, + HWAddr: mac1, + }, + wantErrMsg: "", + }, { + name: "valid_v6", + lease: &dhcpsvc.Lease{ + Hostname: host3, + IP: ip3, + HWAddr: mac3, + }, + wantErrMsg: "", + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.RemoveLease(tc.lease)) + }) + } + + assert.Empty(t, srv.Leases()) +} + +func TestDHCPServer_Reset(t *testing.T) { + srv, err := dhcpsvc.New(&dhcpsvc.Config{ + Enabled: true, + LocalDomainName: testLocalTLD, + Interfaces: testInterfaceConf, + }) + require.NoError(t, err) + + leases := []*dhcpsvc.Lease{{ + Hostname: "host1", + IP: netip.MustParseAddr("192.168.0.2"), + HWAddr: mustParseMAC(t, "01:02:03:04:05:06"), + IsStatic: true, + }, { + Hostname: "host2", + IP: netip.MustParseAddr("192.168.0.3"), + HWAddr: mustParseMAC(t, "06:05:04:03:02:01"), + IsStatic: true, + }, { + Hostname: "host3", + IP: netip.MustParseAddr("2001:db8::2"), + HWAddr: mustParseMAC(t, "02:03:04:05:06:07"), + IsStatic: true, + }, { + Hostname: "host4", + IP: netip.MustParseAddr("2001:db8::3"), + HWAddr: mustParseMAC(t, "06:05:04:03:02:02"), + IsStatic: true, + }} + + for _, l := range leases { + require.NoError(t, srv.AddLease(l)) + } + + require.Len(t, srv.Leases(), len(leases)) + + require.NoError(t, srv.Reset()) + + assert.Empty(t, srv.Leases()) +} diff --git a/internal/dhcpsvc/v6.go b/internal/dhcpsvc/v6.go index ea300e30..09342569 100644 --- a/internal/dhcpsvc/v6.go +++ b/internal/dhcpsvc/v6.go @@ -150,7 +150,7 @@ func (ifaces netInterfacesV6) find(ip netip.Addr) (iface6 *netInterface, ok bool const prefLen = netutil.IPv6BitLen - 8 i := slices.IndexFunc(ifaces, func(iface *netInterfaceV6) (contains bool) { - return !iface.rangeStart.Less(ip) && + return !ip.Less(iface.rangeStart) && netip.PrefixFrom(iface.rangeStart, prefLen).Contains(ip) }) if i < 0 {