From 8bb1aad7398899f3a6ad2249e665b07a848f6202 Mon Sep 17 00:00:00 2001 From: Eugene Burkov Date: Thu, 16 Nov 2023 14:14:40 +0300 Subject: [PATCH] Pull request 2070: 4923 gopacket DHCP vol.4 Merge in DNS/adguard-home from 4923-gopacket-dhcp-vol.4 to master Updates #4923. Squashed commit of the following: commit 4b87258c70ac98b2abb1ac95f7e916e244b3cd08 Merge: 61458864f 9b91a8740 Author: Eugene Burkov Date: Thu Nov 16 14:05:34 2023 +0300 Merge branch 'master' into 4923-gopacket-dhcp-vol.4 commit 61458864f3df7a027e65060a5f0fb516cc7911a7 Author: Eugene Burkov Date: Wed Nov 15 18:48:40 2023 +0300 all: imp code commit 506a0ab81e76beebb900f86580577563b471e4e2 Author: Eugene Burkov Date: Tue Nov 14 15:59:56 2023 +0300 all: cleanup moving lease commit 8d218b732662ac4308ed09d28c1bf9f65906d47c Author: Eugene Burkov Date: Mon Nov 13 18:13:39 2023 +0300 all: rm old leases type --- internal/dhcpd/config.go | 13 +-- internal/dhcpd/db.go | 102 +++++++++++++++++---- internal/dhcpd/dhcpd.go | 117 +----------------------- internal/dhcpd/dhcpd_unix_test.go | 3 +- internal/dhcpd/http_unix.go | 6 +- internal/dhcpd/migrate.go | 95 ++++++++++--------- internal/dhcpd/migrate_internal_test.go | 23 +++-- internal/dhcpd/v46_windows.go | 28 +++--- internal/dhcpd/v4_unix.go | 90 +++++++++--------- internal/dhcpd/v4_unix_test.go | 39 ++++---- internal/dhcpd/v6_unix.go | 41 +++++---- internal/dhcpd/v6_unix_test.go | 15 +-- internal/dhcpsvc/config.go | 6 +- internal/dhcpsvc/dhcpsvc.go | 30 ++++-- internal/dhcpsvc/server.go | 23 +++++ internal/dhcpsvc/v4.go | 10 +- internal/home/clients_internal_test.go | 2 +- 17 files changed, 332 insertions(+), 311 deletions(-) diff --git a/internal/dhcpd/config.go b/internal/dhcpd/config.go index 92179a59..d11d9342 100644 --- a/internal/dhcpd/config.go +++ b/internal/dhcpd/config.go @@ -8,6 +8,7 @@ import ( "github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghnet" + "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" "github.com/AdguardTeam/golibs/errors" ) @@ -49,16 +50,16 @@ type ServerConfig struct { // DHCPServer - DHCP server interface type DHCPServer interface { // ResetLeases resets leases. - ResetLeases(leases []*Lease) (err error) + ResetLeases(leases []*dhcpsvc.Lease) (err error) // GetLeases returns deep clones of the current leases. - GetLeases(flags GetLeasesFlags) (leases []*Lease) + GetLeases(flags GetLeasesFlags) (leases []*dhcpsvc.Lease) // AddStaticLease - add a static lease - AddStaticLease(l *Lease) (err error) + AddStaticLease(l *dhcpsvc.Lease) (err error) // RemoveStaticLease - remove a static lease - RemoveStaticLease(l *Lease) (err error) + RemoveStaticLease(l *dhcpsvc.Lease) (err error) // UpdateStaticLease updates IP, hostname of the lease. - UpdateStaticLease(l *Lease) (err error) + UpdateStaticLease(l *dhcpsvc.Lease) (err error) // FindMACbyIP returns a MAC address by the IP address of its lease, if // there is one. @@ -81,7 +82,7 @@ type DHCPServer interface { Start() (err error) // Stop - stop server Stop() (err error) - getLeasesRef() []*Lease + getLeasesRef() []*dhcpsvc.Lease } // V4ServerConf - server configuration diff --git a/internal/dhcpd/db.go b/internal/dhcpd/db.go index 8d8c90fa..9862d0e5 100644 --- a/internal/dhcpd/db.go +++ b/internal/dhcpd/db.go @@ -5,9 +5,13 @@ package dhcpd import ( "encoding/json" "fmt" + "net" + "net/netip" "os" "strings" + "time" + "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/log" "github.com/google/renameio/v2/maybe" @@ -28,7 +32,60 @@ type dataLeases struct { Version int `json:"version"` // Leases is the list containing stored DHCP leases. - Leases []*Lease `json:"leases"` + Leases []*dbLease `json:"leases"` +} + +// dbLease is the structure of stored lease. +type dbLease struct { + Expiry string `json:"expires"` + IP netip.Addr `json:"ip"` + Hostname string `json:"hostname"` + HWAddr string `json:"mac"` + IsStatic bool `json:"static"` +} + +// fromLease converts *dhcpsvc.Lease to *dbLease. +func fromLease(l *dhcpsvc.Lease) (dl *dbLease) { + var expiryStr string + if !l.IsStatic { + // The front-end is waiting for RFC 3999 format of the time value. It + // also shouldn't got an Expiry field for static leases. + // + // See https://github.com/AdguardTeam/AdGuardHome/issues/2692. + expiryStr = l.Expiry.Format(time.RFC3339) + } + + return &dbLease{ + Expiry: expiryStr, + Hostname: l.Hostname, + HWAddr: l.HWAddr.String(), + IP: l.IP, + IsStatic: l.IsStatic, + } +} + +// toLease converts *dbLease to *dhcpsvc.Lease. +func (dl *dbLease) toLease() (l *dhcpsvc.Lease, err error) { + mac, err := net.ParseMAC(dl.HWAddr) + if err != nil { + return nil, fmt.Errorf("parsing hardware address: %w", err) + } + + expiry := time.Time{} + if !dl.IsStatic { + expiry, err = time.Parse(time.RFC3339, dl.Expiry) + if err != nil { + return nil, fmt.Errorf("parsing expiry time: %w", err) + } + } + + return &dhcpsvc.Lease{ + Expiry: expiry, + IP: dl.IP, + Hostname: dl.Hostname, + HWAddr: mac, + IsStatic: dl.IsStatic, + }, nil } // dbLoad loads stored leases. @@ -49,15 +106,22 @@ func (s *server) dbLoad() (err error) { } leases := dl.Leases - - leases4 := []*Lease{} - leases6 := []*Lease{} + leases4 := []*dhcpsvc.Lease{} + leases6 := []*dhcpsvc.Lease{} for _, l := range leases { - if l.IP.Is4() { - leases4 = append(leases4, l) + var lease *dhcpsvc.Lease + lease, err = l.toLease() + if err != nil { + log.Info("dhcp: invalid lease: %s", err) + + continue + } + + if lease.IP.Is4() { + leases4 = append(leases4, lease) } else { - leases6 = append(leases6, l) + leases6 = append(leases6, lease) } } @@ -73,8 +137,12 @@ func (s *server) dbLoad() (err error) { } } - log.Info("dhcp: loaded leases v4:%d v6:%d total-read:%d from DB", - len(leases4), len(leases6), len(leases)) + log.Info( + "dhcp: loaded leases v4:%d v6:%d total-read:%d from DB", + len(leases4), + len(leases6), + len(leases), + ) return nil } @@ -83,24 +151,26 @@ func (s *server) dbLoad() (err error) { func (s *server) dbStore() (err error) { // Use an empty slice here as opposed to nil so that it doesn't write // "null" into the database file if leases are empty. - leases := []*Lease{} + leases := []*dbLease{} - leases4 := s.srv4.getLeasesRef() - leases = append(leases, leases4...) + for _, l := range s.srv4.getLeasesRef() { + leases = append(leases, fromLease(l)) + } if s.srv6 != nil { - leases6 := s.srv6.getLeasesRef() - leases = append(leases, leases6...) + for _, l := range s.srv6.getLeasesRef() { + leases = append(leases, fromLease(l)) + } } return writeDB(s.conf.dbFilePath, leases) } // writeDB writes leases to file at path. -func writeDB(path string, leases []*Lease) (err error) { +func writeDB(path string, leases []*dbLease) (err error) { defer func() { err = errors.Annotate(err, "writing db: %w") }() - slices.SortFunc(leases, func(a, b *Lease) (res int) { + slices.SortFunc(leases, func(a, b *dbLease) (res int) { return strings.Compare(a.Hostname, b.Hostname) }) diff --git a/internal/dhcpd/dhcpd.go b/internal/dhcpd/dhcpd.go index e4ee14a9..edc3d3a4 100644 --- a/internal/dhcpd/dhcpd.go +++ b/internal/dhcpd/dhcpd.go @@ -2,7 +2,6 @@ package dhcpd import ( - "encoding/json" "fmt" "net" "net/netip" @@ -12,7 +11,6 @@ import ( "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" "github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/timeutil" - "golang.org/x/exp/slices" ) const ( @@ -29,105 +27,6 @@ const ( defaultBackoff time.Duration = 500 * time.Millisecond ) -// Lease contains the necessary information about a DHCP lease. It's used as is -// in the database, so don't change it until it's absolutely necessary, see -// [dataVersion]. -// -// TODO(e.burkov): Unexport it and use [dhcpsvc.Lease]. -type Lease struct { - // Expiry is the expiration time of the lease. - Expiry time.Time `json:"expires"` - - // Hostname of the client. - Hostname string `json:"hostname"` - - // HWAddr is the physical hardware address (MAC address). - HWAddr net.HardwareAddr `json:"mac"` - - // IP is the IP address leased to the client. - IP netip.Addr `json:"ip"` - - // IsStatic defines if the lease is static. - IsStatic bool `json:"static"` -} - -// Clone returns a deep copy of l. -func (l *Lease) Clone() (clone *Lease) { - if l == nil { - return nil - } - - return &Lease{ - Expiry: l.Expiry, - Hostname: l.Hostname, - HWAddr: slices.Clone(l.HWAddr), - IP: l.IP, - IsStatic: l.IsStatic, - } -} - -// IsBlocklisted returns true if the lease is blocklisted. -// -// TODO(a.garipov): Just make it a boolean field. -func (l *Lease) IsBlocklisted() (ok bool) { - if len(l.HWAddr) == 0 { - return false - } - - for _, b := range l.HWAddr { - if b != 0 { - return false - } - } - - return true -} - -// MarshalJSON implements the json.Marshaler interface for Lease. -func (l Lease) MarshalJSON() ([]byte, error) { - var expiryStr string - if !l.IsStatic { - // The front-end is waiting for RFC 3999 format of the time - // value. It also shouldn't got an Expiry field for static - // leases. - // - // See https://github.com/AdguardTeam/AdGuardHome/issues/2692. - expiryStr = l.Expiry.Format(time.RFC3339) - } - - type lease Lease - return json.Marshal(&struct { - HWAddr string `json:"mac"` - Expiry string `json:"expires,omitempty"` - lease - }{ - HWAddr: l.HWAddr.String(), - Expiry: expiryStr, - lease: lease(l), - }) -} - -// UnmarshalJSON implements the json.Unmarshaler interface for *Lease. -func (l *Lease) UnmarshalJSON(data []byte) (err error) { - type lease Lease - aux := struct { - *lease - HWAddr string `json:"mac"` - }{ - lease: (*lease)(l), - } - if err = json.Unmarshal(data, &aux); err != nil { - return err - } - - l.HWAddr, err = net.ParseMAC(aux.HWAddr) - if err != nil { - return fmt.Errorf("couldn't parse MAC address: %w", err) - } - - return nil -} - // OnLeaseChangedT is a callback for lease changes. type OnLeaseChangedT func(flags int) @@ -370,19 +269,7 @@ func (s *server) Stop() (err error) { // Leases returns the list of active DHCP leases. func (s *server) Leases() (leases []*dhcpsvc.Lease) { - ls := append(s.srv4.GetLeases(LeasesAll), s.srv6.GetLeases(LeasesAll)...) - leases = make([]*dhcpsvc.Lease, len(ls)) - for i, l := range ls { - leases[i] = &dhcpsvc.Lease{ - Expiry: l.Expiry, - Hostname: l.Hostname, - HWAddr: l.HWAddr, - IP: l.IP, - IsStatic: l.IsStatic, - } - } - - return leases + return append(s.srv4.GetLeases(LeasesAll), s.srv6.GetLeases(LeasesAll)...) } // MACByIP returns a MAC address by the IP address of its lease, if there is @@ -414,6 +301,6 @@ func (s *server) IPByHost(host string) (ip netip.Addr) { } // AddStaticLease - add static v4 lease -func (s *server) AddStaticLease(l *Lease) error { +func (s *server) AddStaticLease(l *dhcpsvc.Lease) error { return s.srv4.AddStaticLease(l) } diff --git a/internal/dhcpd/dhcpd_unix_test.go b/internal/dhcpd/dhcpd_unix_test.go index 7eced536..3aea125a 100644 --- a/internal/dhcpd/dhcpd_unix_test.go +++ b/internal/dhcpd/dhcpd_unix_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" "github.com/AdguardTeam/golibs/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -44,7 +45,7 @@ func TestDB(t *testing.T) { s.srv6, err = v6Create(V6ServerConf{}) require.NoError(t, err) - leases := []*Lease{{ + leases := []*dhcpsvc.Lease{{ Expiry: time.Now().Add(time.Hour), Hostname: "static-1.local", HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, diff --git a/internal/dhcpd/http_unix.go b/internal/dhcpd/http_unix.go index 622bf1e7..199d1113 100644 --- a/internal/dhcpd/http_unix.go +++ b/internal/dhcpd/http_unix.go @@ -93,13 +93,13 @@ func leasesToStatic(leases []*dhcpsvc.Lease) (static []*leaseStatic) { } // toLease converts leaseStatic to Lease or returns error. -func (l *leaseStatic) toLease() (lease *Lease, err error) { +func (l *leaseStatic) toLease() (lease *dhcpsvc.Lease, err error) { addr, err := net.ParseMAC(l.HWAddr) if err != nil { return nil, fmt.Errorf("couldn't parse MAC address: %w", err) } - return &Lease{ + return &dhcpsvc.Lease{ HWAddr: addr, IP: l.IP, Hostname: l.Hostname, @@ -593,7 +593,7 @@ func setOtherDHCPResult(ifaceName string, result *dhcpSearchResult) { // parseLease parses a lease from r. If there is no error returns DHCPServer // and *Lease. r must be non-nil. -func (s *server) parseLease(r io.Reader) (srv DHCPServer, lease *Lease, err error) { +func (s *server) parseLease(r io.Reader) (srv DHCPServer, lease *dhcpsvc.Lease, err error) { l := &leaseStatic{} err = json.NewDecoder(r).Decode(l) if err != nil { diff --git a/internal/dhcpd/migrate.go b/internal/dhcpd/migrate.go index 4fa9339f..436ba2dc 100644 --- a/internal/dhcpd/migrate.go +++ b/internal/dhcpd/migrate.go @@ -2,6 +2,7 @@ package dhcpd import ( "encoding/json" + "fmt" "net" "net/netip" "os" @@ -25,9 +26,9 @@ const ( dbFilename = "leases.db" ) -// leaseJSON is the structure of stored lease. +// leaseJSON is the structure of stored lease in a legacy database. // -// Deprecated: Use [Lease]. +// Deprecated: Use [dbLease]. type leaseJSON struct { HWAddr []byte `json:"mac"` IP []byte `json:"ip"` @@ -35,13 +36,28 @@ type leaseJSON struct { Expiry int64 `json:"exp"` } -func normalizeIP(ip net.IP) net.IP { - ip4 := ip.To4() - if ip4 != nil { - return ip4 +// readOldDB reads the old database from the given path. +func readOldDB(path string) (leases []*leaseJSON, err error) { + // #nosec G304 -- Trust this path, since it's taken from the old file name + // relative to the working directory and should generally be considered + // safe. + file, err := os.Open(path) + if errors.Is(err, os.ErrNotExist) { + // Nothing to migrate. + return nil, nil + } else if err != nil { + // Don't wrap the error since it's informative enough as is. + return nil, err + } + defer func() { err = errors.WithDeferred(err, file.Close()) }() + + leases = []*leaseJSON{} + err = json.NewDecoder(file).Decode(&leases) + if err != nil { + return nil, fmt.Errorf("decoding old db: %w", err) } - return ip + return leases, nil } // migrateDB migrates stored leases if necessary. @@ -51,59 +67,50 @@ func migrateDB(conf *ServerConfig) (err error) { oldLeasesPath := filepath.Join(conf.WorkDir, dbFilename) dataDirPath := filepath.Join(conf.DataDir, dataFilename) - // #nosec G304 -- Trust this path, since it's taken from the old file name - // relative to the working directory and should generally be considered - // safe. - file, err := os.Open(oldLeasesPath) - if errors.Is(err, os.ErrNotExist) { + oldLeases, err := readOldDB(oldLeasesPath) + if err != nil { + // Don't wrap the error since it's informative enough as is. + return err + } else if oldLeases == nil { // Nothing to migrate. return nil - } else if err != nil { - // Don't wrap the error since it's informative enough as is. - return err } - ljs := []leaseJSON{} - err = json.NewDecoder(file).Decode(&ljs) - if err != nil { - // Don't wrap the error since it's informative enough as is. - return err - } - - err = file.Close() - if err != nil { - // Don't wrap the error since it's informative enough as is. - return err - } - - leases := []*Lease{} - - for _, lj := range ljs { - lj.IP = normalizeIP(lj.IP) - - ip, ok := netip.AddrFromSlice(lj.IP) + leases := make([]*dbLease, 0, len(oldLeases)) + for _, l := range oldLeases { + l.IP = normalizeIP(l.IP) + ip, ok := netip.AddrFromSlice(l.IP) if !ok { - log.Info("dhcp: invalid IP: %s", lj.IP) + log.Info("dhcp: invalid IP: %s", l.IP) continue } - lease := &Lease{ - Expiry: time.Unix(lj.Expiry, 0), - Hostname: lj.Hostname, - HWAddr: lj.HWAddr, + leases = append(leases, &dbLease{ + Expiry: time.Unix(l.Expiry, 0).Format(time.RFC3339), + Hostname: l.Hostname, + HWAddr: net.HardwareAddr(l.HWAddr).String(), IP: ip, - IsStatic: lj.Expiry == leaseExpireStatic, - } - - leases = append(leases, lease) + IsStatic: l.Expiry == leaseExpireStatic, + }) } err = writeDB(dataDirPath, leases) if err != nil { - // Don't wrap the error since it's informative enough as is. + // Don't wrap the error since an annotation deferred already. return err } return os.Remove(oldLeasesPath) } + +// normalizeIP converts the given IP address to IPv4 if it's IPv4-mapped IPv6, +// or leaves it as is otherwise. +func normalizeIP(ip net.IP) (normalized net.IP) { + normalized = ip.To4() + if normalized != nil { + return normalized + } + + return ip +} diff --git a/internal/dhcpd/migrate_internal_test.go b/internal/dhcpd/migrate_internal_test.go index 2c0e6ecd..4ed99361 100644 --- a/internal/dhcpd/migrate_internal_test.go +++ b/internal/dhcpd/migrate_internal_test.go @@ -2,7 +2,6 @@ package dhcpd import ( "encoding/json" - "net" "net/netip" "os" "path/filepath" @@ -27,16 +26,16 @@ func TestMigrateDB(t *testing.T) { err := os.WriteFile(oldLeasesPath, []byte(testData), 0o644) require.NoError(t, err) - wantLeases := []*Lease{{ - Expiry: time.Time{}, + wantLeases := []*dbLease{{ + Expiry: time.Unix(1, 0).Format(time.RFC3339), Hostname: "test1", - HWAddr: net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}, + HWAddr: "11:22:33:44:55:66", IP: netip.MustParseAddr("1.2.3.4"), IsStatic: true, }, { - Expiry: time.Unix(1231231231, 0), + Expiry: time.Unix(1231231231, 0).Format(time.RFC3339), Hostname: "test2", - HWAddr: net.HardwareAddr{0x66, 0x55, 0x44, 0x33, 0x22, 0x11}, + HWAddr: "66:55:44:33:22:11", IP: netip.MustParseAddr("4.3.2.1"), IsStatic: false, }} @@ -62,12 +61,12 @@ func TestMigrateDB(t *testing.T) { leases := dl.Leases - for i, wl := range wantLeases { - assert.Equal(t, wl.Hostname, leases[i].Hostname) - assert.Equal(t, wl.HWAddr, leases[i].HWAddr) - assert.Equal(t, wl.IP, leases[i].IP) - assert.Equal(t, wl.IsStatic, leases[i].IsStatic) + for i, wantLease := range wantLeases { + assert.Equal(t, wantLease.Hostname, leases[i].Hostname) + assert.Equal(t, wantLease.HWAddr, leases[i].HWAddr) + assert.Equal(t, wantLease.IP, leases[i].IP) + assert.Equal(t, wantLease.IsStatic, leases[i].IsStatic) - require.True(t, wl.Expiry.Equal(leases[i].Expiry)) + require.Equal(t, wantLease.Expiry, leases[i].Expiry) } } diff --git a/internal/dhcpd/v46_windows.go b/internal/dhcpd/v46_windows.go index 2dbe302e..241429c6 100644 --- a/internal/dhcpd/v46_windows.go +++ b/internal/dhcpd/v46_windows.go @@ -7,6 +7,8 @@ package dhcpd import ( "net" "net/netip" + + "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" ) type winServer struct{} @@ -14,19 +16,19 @@ type winServer struct{} // type check var _ DHCPServer = winServer{} -func (winServer) ResetLeases(_ []*Lease) (err error) { return nil } -func (winServer) GetLeases(_ GetLeasesFlags) (leases []*Lease) { return nil } -func (winServer) getLeasesRef() []*Lease { return nil } -func (winServer) AddStaticLease(_ *Lease) (err error) { return nil } -func (winServer) RemoveStaticLease(_ *Lease) (err error) { return nil } -func (winServer) UpdateStaticLease(_ *Lease) (err error) { return nil } -func (winServer) FindMACbyIP(_ netip.Addr) (mac net.HardwareAddr) { return nil } -func (winServer) WriteDiskConfig4(_ *V4ServerConf) {} -func (winServer) WriteDiskConfig6(_ *V6ServerConf) {} -func (winServer) Start() (err error) { return nil } -func (winServer) Stop() (err error) { return nil } -func (winServer) HostByIP(_ netip.Addr) (host string) { return "" } -func (winServer) IPByHost(_ string) (ip netip.Addr) { return netip.Addr{} } +func (winServer) ResetLeases(_ []*dhcpsvc.Lease) (err error) { return nil } +func (winServer) GetLeases(_ GetLeasesFlags) (leases []*dhcpsvc.Lease) { return nil } +func (winServer) getLeasesRef() []*dhcpsvc.Lease { return nil } +func (winServer) AddStaticLease(_ *dhcpsvc.Lease) (err error) { return nil } +func (winServer) RemoveStaticLease(_ *dhcpsvc.Lease) (err error) { return nil } +func (winServer) UpdateStaticLease(_ *dhcpsvc.Lease) (err error) { return nil } +func (winServer) FindMACbyIP(_ netip.Addr) (mac net.HardwareAddr) { return nil } +func (winServer) WriteDiskConfig4(_ *V4ServerConf) {} +func (winServer) WriteDiskConfig6(_ *V6ServerConf) {} +func (winServer) Start() (err error) { return nil } +func (winServer) Stop() (err error) { return nil } +func (winServer) HostByIP(_ netip.Addr) (host string) { return "" } +func (winServer) IPByHost(_ string) (ip netip.Addr) { return netip.Addr{} } func v4Create(_ *V4ServerConf) (s DHCPServer, err error) { return winServer{}, nil } func v6Create(_ V6ServerConf) (s DHCPServer, err error) { return winServer{}, nil } diff --git a/internal/dhcpd/v4_unix.go b/internal/dhcpd/v4_unix.go index 2b78e1cf..3685c2e8 100644 --- a/internal/dhcpd/v4_unix.go +++ b/internal/dhcpd/v4_unix.go @@ -12,6 +12,7 @@ import ( "time" "github.com/AdguardTeam/AdGuardHome/internal/aghnet" + "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/netutil" @@ -38,7 +39,7 @@ type v4Server struct { // have intersections with [implicitOpts]. explicitOpts dhcpv4.Options - // leasesLock protects leases, leaseHosts, and leasedOffsets. + // leasesLock protects leases, hostsIndex, ipIndex, and leasedOffsets. leasesLock sync.Mutex // leasedOffsets contains offsets from conf.ipRange.start that have been @@ -46,13 +47,13 @@ type v4Server struct { leasedOffsets *bitSet // leases contains all dynamic and static leases. - leases []*Lease + leases []*dhcpsvc.Lease // hostsIndex is the set of all hostnames of all known DHCP clients. - hostsIndex map[string]*Lease + hostsIndex map[string]*dhcpsvc.Lease // ipIndex is an index of leases by their IP addresses. - ipIndex map[netip.Addr]*Lease + ipIndex map[netip.Addr]*dhcpsvc.Lease } func (s *v4Server) enabled() (ok bool) { @@ -141,7 +142,7 @@ func (s *v4Server) IPByHost(host string) (ip netip.Addr) { } // ResetLeases resets leases. -func (s *v4Server) ResetLeases(leases []*Lease) (err error) { +func (s *v4Server) ResetLeases(leases []*dhcpsvc.Lease) (err error) { defer func() { err = errors.Annotate(err, "dhcpv4: %w") }() if s.conf == nil { @@ -152,8 +153,8 @@ func (s *v4Server) ResetLeases(leases []*Lease) (err error) { defer s.leasesLock.Unlock() s.leasedOffsets = newBitSet() - s.hostsIndex = make(map[string]*Lease, len(leases)) - s.ipIndex = make(map[netip.Addr]*Lease, len(leases)) + s.hostsIndex = make(map[string]*dhcpsvc.Lease, len(leases)) + s.ipIndex = make(map[netip.Addr]*dhcpsvc.Lease, len(leases)) s.leases = nil for _, l := range leases { @@ -173,14 +174,14 @@ func (s *v4Server) ResetLeases(leases []*Lease) (err error) { } // getLeasesRef returns the actual leases slice. For internal use only. -func (s *v4Server) getLeasesRef() []*Lease { +func (s *v4Server) getLeasesRef() []*dhcpsvc.Lease { return s.leases } // isBlocklisted returns true if this lease holds a blocklisted IP. // // TODO(a.garipov): Make a method of *Lease? -func (s *v4Server) isBlocklisted(l *Lease) (ok bool) { +func (s *v4Server) isBlocklisted(l *dhcpsvc.Lease) (ok bool) { if len(l.HWAddr) == 0 { return false } @@ -196,11 +197,11 @@ func (s *v4Server) isBlocklisted(l *Lease) (ok bool) { // GetLeases returns the list of current DHCP leases. It is safe for concurrent // use. -func (s *v4Server) GetLeases(flags GetLeasesFlags) (leases []*Lease) { +func (s *v4Server) GetLeases(flags GetLeasesFlags) (leases []*dhcpsvc.Lease) { // The function shouldn't return nil, because zero-length slice behaves // differently in cases like marshalling. Our front-end also requires // a non-nil value in the response. - leases = []*Lease{} + leases = []*dhcpsvc.Lease{} getDynamic := flags&LeasesDynamic != 0 getStatic := flags&LeasesStatic != 0 @@ -248,7 +249,7 @@ func (s *v4Server) FindMACbyIP(ip netip.Addr) (mac net.HardwareAddr) { const defaultHwAddrLen = 6 // Add the specified IP to the black list for a time period -func (s *v4Server) blocklistLease(l *Lease) { +func (s *v4Server) blocklistLease(l *dhcpsvc.Lease) { l.HWAddr = make(net.HardwareAddr, defaultHwAddrLen) l.Hostname = "" l.Expiry = time.Now().Add(s.conf.leaseTime) @@ -284,7 +285,7 @@ func (s *v4Server) rmLeaseByIndex(i int) { // Return error if a static lease is found // // TODO(s.chzhen): Refactor the code. -func (s *v4Server) rmDynamicLease(lease *Lease) (err error) { +func (s *v4Server) rmDynamicLease(lease *dhcpsvc.Lease) (err error) { for i, l := range s.leases { isStatic := l.IsStatic @@ -320,7 +321,7 @@ const ( ) // addLease adds a dynamic or static lease. -func (s *v4Server) addLease(l *Lease) (err error) { +func (s *v4Server) addLease(l *dhcpsvc.Lease) (err error) { r := s.conf.ipRange leaseIP := net.IP(l.IP.AsSlice()) offset, inOffset := r.offset(leaseIP) @@ -352,7 +353,7 @@ func (s *v4Server) addLease(l *Lease) (err error) { } // rmLease removes a lease with the same properties. -func (s *v4Server) rmLease(lease *Lease) (err error) { +func (s *v4Server) rmLease(lease *dhcpsvc.Lease) (err error) { if len(s.leases) == 0 { return nil } @@ -378,7 +379,7 @@ const ErrUnconfigured errors.Error = "server is unconfigured" // AddStaticLease implements the DHCPServer interface for *v4Server. It is // safe for concurrent use. -func (s *v4Server) AddStaticLease(l *Lease) (err error) { +func (s *v4Server) AddStaticLease(l *dhcpsvc.Lease) (err error) { defer func() { err = errors.Annotate(err, "dhcpv4: adding static lease: %w") }() if s.conf == nil { @@ -435,7 +436,7 @@ func (s *v4Server) AddStaticLease(l *Lease) (err error) { } // UpdateStaticLease updates IP, hostname of the static lease. -func (s *v4Server) UpdateStaticLease(l *Lease) (err error) { +func (s *v4Server) UpdateStaticLease(l *dhcpsvc.Lease) (err error) { defer func() { if err != nil { err = errors.Annotate(err, "dhcpv4: updating static lease: %w") @@ -474,7 +475,7 @@ func (s *v4Server) UpdateStaticLease(l *Lease) (err error) { } // validateStaticLease returns an error if the static lease is invalid. -func (s *v4Server) validateStaticLease(l *Lease) (err error) { +func (s *v4Server) validateStaticLease(l *dhcpsvc.Lease) (err error) { hostname, err := normalizeHostname(l.Hostname) if err != nil { // Don't wrap the error, because it's informative enough as is. @@ -511,7 +512,7 @@ func (s *v4Server) validateStaticLease(l *Lease) (err error) { // updateStaticLease safe removes dynamic lease with the same properties and // then adds a static lease l. -func (s *v4Server) updateStaticLease(l *Lease) (err error) { +func (s *v4Server) updateStaticLease(l *dhcpsvc.Lease) (err error) { s.leasesLock.Lock() defer s.leasesLock.Unlock() @@ -529,7 +530,7 @@ func (s *v4Server) updateStaticLease(l *Lease) (err error) { } // RemoveStaticLease removes a static lease. It is safe for concurrent use. -func (s *v4Server) RemoveStaticLease(l *Lease) (err error) { +func (s *v4Server) RemoveStaticLease(l *dhcpsvc.Lease) (err error) { defer func() { err = errors.Annotate(err, "dhcpv4: %w") }() if s.conf == nil { @@ -606,7 +607,7 @@ func (s *v4Server) addrAvailable(target net.IP) (avail bool) { } // findLease finds a lease by its MAC-address. -func (s *v4Server) findLease(mac net.HardwareAddr) (l *Lease) { +func (s *v4Server) findLease(mac net.HardwareAddr) (l *dhcpsvc.Lease) { for _, l = range s.leases { if bytes.Equal(mac, l.HWAddr) { return l @@ -646,8 +647,8 @@ func (s *v4Server) findExpiredLease() int { // reserveLease reserves a lease for a client by its MAC-address. It returns // nil if it couldn't allocate a new lease. -func (s *v4Server) reserveLease(mac net.HardwareAddr) (l *Lease, err error) { - l = &Lease{HWAddr: slices.Clone(mac)} +func (s *v4Server) reserveLease(mac net.HardwareAddr) (l *dhcpsvc.Lease, err error) { + l = &dhcpsvc.Lease{HWAddr: slices.Clone(mac)} nextIP := s.nextIP() if nextIP == nil { @@ -679,7 +680,7 @@ func (s *v4Server) reserveLease(mac net.HardwareAddr) (l *Lease, err error) { // commitLease refreshes l's values. It takes the desired hostname into account // when setting it into the lease, but generates a unique one if the provided // can't be used. -func (s *v4Server) commitLease(l *Lease, hostname string) { +func (s *v4Server) commitLease(l *dhcpsvc.Lease, hostname string) { prev := l.Hostname hostname = s.validHostnameForClient(hostname, l.IP) @@ -709,7 +710,7 @@ func (s *v4Server) commitLease(l *Lease, hostname string) { // allocateLease allocates a new lease for the MAC address. If there are no IP // addresses left, both l and err are nil. -func (s *v4Server) allocateLease(mac net.HardwareAddr) (l *Lease, err error) { +func (s *v4Server) allocateLease(mac net.HardwareAddr) (l *dhcpsvc.Lease, err error) { for { l, err = s.reserveLease(mac) if err != nil { @@ -728,7 +729,7 @@ func (s *v4Server) allocateLease(mac net.HardwareAddr) (l *Lease, err error) { } // handleDiscover is the handler for the DHCP Discover request. -func (s *v4Server) handleDiscover(req, resp *dhcpv4.DHCPv4) (l *Lease, err error) { +func (s *v4Server) handleDiscover(req, resp *dhcpv4.DHCPv4) (l *dhcpsvc.Lease, err error) { mac := req.ClientHWAddr defer s.conf.notify(LeaseChangedDBStore) @@ -787,7 +788,7 @@ func OptionFQDN(fqdn string) (opt dhcpv4.Option) { // checkLease checks if the pair of mac and ip is already leased. The mismatch // is true when the existing lease has the same hardware address but differs in // its IP address. -func (s *v4Server) checkLease(mac net.HardwareAddr, ip net.IP) (lease *Lease, mismatch bool) { +func (s *v4Server) checkLease(mac net.HardwareAddr, ip net.IP) (l *dhcpsvc.Lease, mismatch bool) { s.leasesLock.Lock() defer s.leasesLock.Unlock() @@ -798,7 +799,7 @@ func (s *v4Server) checkLease(mac net.HardwareAddr, ip net.IP) (lease *Lease, mi return nil, false } - for _, l := range s.leases { + for _, l = range s.leases { if !bytes.Equal(l.HWAddr, mac) { continue } @@ -823,7 +824,7 @@ func (s *v4Server) handleSelecting( req *dhcpv4.DHCPv4, reqIP net.IP, sid net.IP, -) (l *Lease, needsReply bool) { +) (l *dhcpsvc.Lease, needsReply bool) { // Client inserts the address of the selected server in server identifier, // ciaddr MUST be zero. mac := req.ClientHWAddr @@ -857,7 +858,10 @@ func (s *v4Server) handleSelecting( } // handleInitReboot handles the DHCPREQUEST generated during INIT-REBOOT state. -func (s *v4Server) handleInitReboot(req *dhcpv4.DHCPv4, reqIP net.IP) (l *Lease, needsReply bool) { +func (s *v4Server) handleInitReboot( + req *dhcpv4.DHCPv4, + reqIP net.IP, +) (l *dhcpsvc.Lease, needsReply bool) { mac := req.ClientHWAddr ip4 := reqIP.To4() @@ -899,7 +903,7 @@ func (s *v4Server) handleInitReboot(req *dhcpv4.DHCPv4, reqIP net.IP) (l *Lease, // handleRenew handles the DHCPREQUEST generated during RENEWING or REBINDING // state. -func (s *v4Server) handleRenew(req *dhcpv4.DHCPv4) (l *Lease, needsReply bool) { +func (s *v4Server) handleRenew(req *dhcpv4.DHCPv4) (l *dhcpsvc.Lease, needsReply bool) { mac := req.ClientHWAddr // ciaddr MUST be filled in with client's IP address. @@ -926,7 +930,7 @@ func (s *v4Server) handleRenew(req *dhcpv4.DHCPv4) (l *Lease, needsReply bool) { // handleByRequestType handles the DHCPREQUEST according to the state during // which it's generated by client. -func (s *v4Server) handleByRequestType(req *dhcpv4.DHCPv4) (lease *Lease, needsReply bool) { +func (s *v4Server) handleByRequestType(req *dhcpv4.DHCPv4) (lease *dhcpsvc.Lease, needsReply bool) { reqIP, sid := req.RequestedIPAddress(), req.ServerIdentifier() if sid != nil && !sid.IsUnspecified() { @@ -950,7 +954,7 @@ func (s *v4Server) handleByRequestType(req *dhcpv4.DHCPv4) (lease *Lease, needsR // handleRequest is the handler for a DHCPREQUEST message. // // See https://datatracker.ietf.org/doc/html/rfc2131#section-4.3.2. -func (s *v4Server) handleRequest(req, resp *dhcpv4.DHCPv4) (lease *Lease, needsReply bool) { +func (s *v4Server) handleRequest(req, resp *dhcpv4.DHCPv4) (lease *dhcpsvc.Lease, needsReply bool) { lease, needsReply = s.handleByRequestType(req) if lease == nil { return nil, needsReply @@ -1043,7 +1047,7 @@ func (s *v4Server) handleDecline(req, resp *dhcpv4.DHCPv4) (err error) { } // findLeaseForIP returns a lease for provided ip and mac. -func (s *v4Server) findLeaseForIP(ip net.IP, mac net.HardwareAddr) (l *Lease) { +func (s *v4Server) findLeaseForIP(ip net.IP, mac net.HardwareAddr) (l *dhcpsvc.Lease) { netIP, ok := netip.AddrFromSlice(ip) if !ok { log.Info("dhcpv4: invalid IP: %s", ip) @@ -1106,7 +1110,11 @@ func (s *v4Server) handleRelease(req, resp *dhcpv4.DHCPv4) (err error) { } // messageHandler describes a DHCPv4 message handler function. -type messageHandler func(s *v4Server, req, resp *dhcpv4.DHCPv4) (rCode int, l *Lease, err error) +type messageHandler func( + s *v4Server, + req *dhcpv4.DHCPv4, + resp *dhcpv4.DHCPv4, +) (rCode int, l *dhcpsvc.Lease, err error) // messageHandlers is a map of handlers for various messages with message types // keys. @@ -1115,7 +1123,7 @@ var messageHandlers = map[dhcpv4.MessageType]messageHandler{ s *v4Server, req *dhcpv4.DHCPv4, resp *dhcpv4.DHCPv4, - ) (rCode int, l *Lease, err error) { + ) (rCode int, l *dhcpsvc.Lease, err error) { l, err = s.handleDiscover(req, resp) if err != nil { return 0, nil, fmt.Errorf("handling discover: %s", err) @@ -1131,7 +1139,7 @@ var messageHandlers = map[dhcpv4.MessageType]messageHandler{ s *v4Server, req *dhcpv4.DHCPv4, resp *dhcpv4.DHCPv4, - ) (rCode int, l *Lease, err error) { + ) (rCode int, l *dhcpsvc.Lease, err error) { var toReply bool l, toReply = s.handleRequest(req, resp) if l == nil { @@ -1149,7 +1157,7 @@ var messageHandlers = map[dhcpv4.MessageType]messageHandler{ s *v4Server, req *dhcpv4.DHCPv4, resp *dhcpv4.DHCPv4, - ) (rCode int, l *Lease, err error) { + ) (rCode int, l *dhcpsvc.Lease, err error) { err = s.handleDecline(req, resp) if err != nil { return 0, nil, fmt.Errorf("handling decline: %s", err) @@ -1161,7 +1169,7 @@ var messageHandlers = map[dhcpv4.MessageType]messageHandler{ s *v4Server, req *dhcpv4.DHCPv4, resp *dhcpv4.DHCPv4, - ) (rCode int, l *Lease, err error) { + ) (rCode int, l *dhcpsvc.Lease, err error) { err = s.handleRelease(req, resp) if err != nil { return 0, nil, fmt.Errorf("handling release: %s", err) @@ -1402,8 +1410,8 @@ func (s *v4Server) Stop() (err error) { // Create DHCPv4 server func v4Create(conf *V4ServerConf) (srv *v4Server, err error) { s := &v4Server{ - hostsIndex: map[string]*Lease{}, - ipIndex: map[netip.Addr]*Lease{}, + hostsIndex: map[string]*dhcpsvc.Lease{}, + ipIndex: map[netip.Addr]*dhcpsvc.Lease{}, } err = conf.Validate() diff --git a/internal/dhcpd/v4_unix_test.go b/internal/dhcpd/v4_unix_test.go index 3ad43b74..4e0df75b 100644 --- a/internal/dhcpd/v4_unix_test.go +++ b/internal/dhcpd/v4_unix_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/AdguardTeam/AdGuardHome/internal/aghnet" + "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" "github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/stringutil" "github.com/AdguardTeam/golibs/testutil" @@ -67,7 +68,7 @@ func TestV4Server_leasing(t *testing.T) { s := defaultSrv(t) t.Run("add_static", func(t *testing.T) { - err := s.AddStaticLease(&Lease{ + err := s.AddStaticLease(&dhcpsvc.Lease{ Hostname: staticName, HWAddr: staticMAC, IP: staticIP, @@ -76,7 +77,7 @@ func TestV4Server_leasing(t *testing.T) { require.NoError(t, err) t.Run("same_name", func(t *testing.T) { - err = s.AddStaticLease(&Lease{ + err = s.AddStaticLease(&dhcpsvc.Lease{ Hostname: staticName, HWAddr: anotherMAC, IP: anotherIP, @@ -90,7 +91,7 @@ func TestV4Server_leasing(t *testing.T) { "dynamic leases for " + anotherIP.String() + " (" + staticMAC.String() + "): static lease already exists" - err = s.AddStaticLease(&Lease{ + err = s.AddStaticLease(&dhcpsvc.Lease{ Hostname: anotherName, HWAddr: staticMAC, IP: anotherIP, @@ -104,7 +105,7 @@ func TestV4Server_leasing(t *testing.T) { "dynamic leases for " + staticIP.String() + " (" + anotherMAC.String() + "): static lease already exists" - err = s.AddStaticLease(&Lease{ + err = s.AddStaticLease(&dhcpsvc.Lease{ Hostname: anotherName, HWAddr: anotherMAC, IP: staticIP, @@ -208,11 +209,11 @@ func TestV4Server_AddRemove_static(t *testing.T) { require.Empty(t, ls) testCases := []struct { - lease *Lease + lease *dhcpsvc.Lease name string wantErrMsg string }{{ - lease: &Lease{ + lease: &dhcpsvc.Lease{ Hostname: "success.local", HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, IP: netip.MustParseAddr("192.168.10.150"), @@ -220,7 +221,7 @@ func TestV4Server_AddRemove_static(t *testing.T) { name: "success", wantErrMsg: "", }, { - lease: &Lease{ + lease: &dhcpsvc.Lease{ Hostname: "probably-router.local", HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, IP: DefaultGatewayIP, @@ -229,7 +230,7 @@ func TestV4Server_AddRemove_static(t *testing.T) { wantErrMsg: "dhcpv4: adding static lease: " + `can't assign the gateway IP "192.168.10.1" to the lease`, }, { - lease: &Lease{ + lease: &dhcpsvc.Lease{ Hostname: "ip6.local", HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, IP: netip.MustParseAddr("ffff::1"), @@ -238,7 +239,7 @@ func TestV4Server_AddRemove_static(t *testing.T) { wantErrMsg: `dhcpv4: adding static lease: ` + `invalid IP "ffff::1": only IPv4 is supported`, }, { - lease: &Lease{ + lease: &dhcpsvc.Lease{ Hostname: "bad-mac.local", HWAddr: net.HardwareAddr{0xAA, 0xAA}, IP: netip.MustParseAddr("192.168.10.150"), @@ -247,7 +248,7 @@ func TestV4Server_AddRemove_static(t *testing.T) { wantErrMsg: `dhcpv4: adding static lease: bad mac address "aa:aa": ` + `bad mac address length 2, allowed: [6 8 20]`, }, { - lease: &Lease{ + lease: &dhcpsvc.Lease{ Hostname: "bad-lbl-.local", HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, IP: netip.MustParseAddr("192.168.10.150"), @@ -266,7 +267,7 @@ func TestV4Server_AddRemove_static(t *testing.T) { return } - err = s.RemoveStaticLease(&Lease{ + err = s.RemoveStaticLease(&dhcpsvc.Lease{ IP: tc.lease.IP, HWAddr: tc.lease.HWAddr, }) @@ -289,7 +290,7 @@ func TestV4_AddReplace(t *testing.T) { s, ok := sIface.(*v4Server) require.True(t, ok) - dynLeases := []Lease{{ + dynLeases := []dhcpsvc.Lease{{ Hostname: "dynamic-1.local", HWAddr: net.HardwareAddr{0x11, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, IP: netip.MustParseAddr("192.168.10.150"), @@ -304,7 +305,7 @@ func TestV4_AddReplace(t *testing.T) { require.NoError(t, err) } - stLeases := []*Lease{{ + stLeases := []*dhcpsvc.Lease{{ Hostname: "static-1.local", HWAddr: net.HardwareAddr{0x33, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, IP: netip.MustParseAddr("192.168.10.150"), @@ -513,7 +514,7 @@ func TestV4StaticLease_Get(t *testing.T) { s.conf.dnsIPAddrs = []netip.Addr{dnsAddr} s.implicitOpts.Update(dhcpv4.OptDNS(dnsAddr.AsSlice())) - l := &Lease{ + l := &dhcpsvc.Lease{ Hostname: "static-1.local", HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, IP: netip.MustParseAddr("192.168.10.150"), @@ -779,7 +780,7 @@ func TestV4Server_FindMACbyIP(t *testing.T) { anotherMAC := net.HardwareAddr{0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB} s := &v4Server{ - leases: []*Lease{{ + leases: []*dhcpsvc.Lease{{ Hostname: staticName, HWAddr: staticMAC, IP: staticIP, @@ -791,11 +792,11 @@ func TestV4Server_FindMACbyIP(t *testing.T) { IP: anotherIP, }}, } - s.ipIndex = map[netip.Addr]*Lease{ + s.ipIndex = map[netip.Addr]*dhcpsvc.Lease{ staticIP: s.leases[0], anotherIP: s.leases[1], } - s.hostsIndex = map[string]*Lease{ + s.hostsIndex = map[string]*dhcpsvc.Lease{ staticName: s.leases[0], anotherName: s.leases[1], } @@ -845,7 +846,7 @@ func TestV4Server_handleDecline(t *testing.T) { s4, ok := s.(*v4Server) require.True(t, ok) - s4.leases = []*Lease{{ + s4.leases = []*dhcpsvc.Lease{{ Hostname: dynamicName, HWAddr: dynamicMAC, IP: dynamicIP, @@ -887,7 +888,7 @@ func TestV4Server_handleRelease(t *testing.T) { s4, ok := s.(*v4Server) require.True(t, ok) - s4.leases = []*Lease{{ + s4.leases = []*dhcpsvc.Lease{{ Hostname: dynamicName, HWAddr: dynamicMAC, IP: dynamicIP, diff --git a/internal/dhcpd/v6_unix.go b/internal/dhcpd/v6_unix.go index 4101830e..aeaed266 100644 --- a/internal/dhcpd/v6_unix.go +++ b/internal/dhcpd/v6_unix.go @@ -11,6 +11,7 @@ import ( "time" "github.com/AdguardTeam/AdGuardHome/internal/aghnet" + "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/netutil" @@ -31,7 +32,7 @@ type v6Server struct { sid dhcpv6.DUID srv *server6.Server - leases []*Lease + leases []*dhcpsvc.Lease leasesLock sync.Mutex ipAddrs [256]byte } @@ -87,7 +88,7 @@ func (s *v6Server) IPByHost(host string) (ip netip.Addr) { } // ResetLeases resets leases. -func (s *v6Server) ResetLeases(leases []*Lease) (err error) { +func (s *v6Server) ResetLeases(leases []*dhcpsvc.Lease) (err error) { defer func() { err = errors.Annotate(err, "dhcpv6: %w") }() s.leasesLock.Lock() @@ -111,12 +112,14 @@ func (s *v6Server) ResetLeases(leases []*Lease) (err error) { // GetLeases returns the list of current DHCP leases. It is safe for concurrent // use. -func (s *v6Server) GetLeases(flags GetLeasesFlags) (leases []*Lease) { +func (s *v6Server) GetLeases(flags GetLeasesFlags) (leases []*dhcpsvc.Lease) { // The function shouldn't return nil value because zero-length slice // behaves differently in cases like marshalling. Our front-end also // requires non-nil value in the response. - leases = []*Lease{} + leases = []*dhcpsvc.Lease{} s.leasesLock.Lock() + defer s.leasesLock.Unlock() + for _, l := range s.leases { if l.IsStatic { if (flags & LeasesStatic) != 0 { @@ -128,12 +131,12 @@ func (s *v6Server) GetLeases(flags GetLeasesFlags) (leases []*Lease) { } } } - s.leasesLock.Unlock() + return leases } // getLeasesRef returns the actual leases slice. For internal use only. -func (s *v6Server) getLeasesRef() []*Lease { +func (s *v6Server) getLeasesRef() []*dhcpsvc.Lease { return s.leases } @@ -174,7 +177,7 @@ func (s *v6Server) leaseRemoveSwapByIndex(i int) { // Remove a dynamic lease with the same properties // Return error if a static lease is found -func (s *v6Server) rmDynamicLease(lease *Lease) (err error) { +func (s *v6Server) rmDynamicLease(lease *dhcpsvc.Lease) (err error) { for i := 0; i < len(s.leases); i++ { l := s.leases[i] @@ -204,7 +207,7 @@ func (s *v6Server) rmDynamicLease(lease *Lease) (err error) { } // AddStaticLease adds a static lease. It is safe for concurrent use. -func (s *v6Server) AddStaticLease(l *Lease) (err error) { +func (s *v6Server) AddStaticLease(l *dhcpsvc.Lease) (err error) { defer func() { err = errors.Annotate(err, "dhcpv6: %w") }() if !l.IP.Is6() { @@ -236,7 +239,7 @@ func (s *v6Server) AddStaticLease(l *Lease) (err error) { } // UpdateStaticLease updates IP, hostname of the static lease. -func (s *v6Server) UpdateStaticLease(l *Lease) (err error) { +func (s *v6Server) UpdateStaticLease(l *dhcpsvc.Lease) (err error) { defer func() { if err != nil { err = errors.Annotate(err, "dhcpv6: updating static lease: %w") @@ -267,7 +270,7 @@ func (s *v6Server) UpdateStaticLease(l *Lease) (err error) { } // RemoveStaticLease removes a static lease. It is safe for concurrent use. -func (s *v6Server) RemoveStaticLease(l *Lease) (err error) { +func (s *v6Server) RemoveStaticLease(l *dhcpsvc.Lease) (err error) { defer func() { err = errors.Annotate(err, "dhcpv6: %w") }() if !l.IP.Is6() { @@ -292,7 +295,7 @@ func (s *v6Server) RemoveStaticLease(l *Lease) (err error) { } // Add a lease -func (s *v6Server) addLease(l *Lease) { +func (s *v6Server) addLease(l *dhcpsvc.Lease) { s.leases = append(s.leases, l) ip := l.IP.As16() s.ipAddrs[ip[15]] = 1 @@ -300,7 +303,7 @@ func (s *v6Server) addLease(l *Lease) { } // Remove a lease with the same properties -func (s *v6Server) rmLease(lease *Lease) (err error) { +func (s *v6Server) rmLease(lease *dhcpsvc.Lease) (err error) { for i, l := range s.leases { if l.IP == lease.IP { if !bytes.Equal(l.HWAddr, lease.HWAddr) || @@ -318,7 +321,7 @@ func (s *v6Server) rmLease(lease *Lease) (err error) { } // Find lease by MAC. -func (s *v6Server) findLease(mac net.HardwareAddr) (lease *Lease) { +func (s *v6Server) findLease(mac net.HardwareAddr) (lease *dhcpsvc.Lease) { for i := range s.leases { if bytes.Equal(mac, s.leases[i].HWAddr) { return s.leases[i] @@ -356,8 +359,8 @@ func (s *v6Server) findFreeIP() net.IP { } // Reserve lease for MAC -func (s *v6Server) reserveLease(mac net.HardwareAddr) *Lease { - l := Lease{ +func (s *v6Server) reserveLease(mac net.HardwareAddr) *dhcpsvc.Lease { + l := dhcpsvc.Lease{ HWAddr: make([]byte, len(mac)), } @@ -390,7 +393,7 @@ func (s *v6Server) reserveLease(mac net.HardwareAddr) *Lease { return &l } -func (s *v6Server) commitDynamicLease(l *Lease) { +func (s *v6Server) commitDynamicLease(l *dhcpsvc.Lease) { l.Expiry = time.Now().Add(s.conf.leaseTime) s.leasesLock.Lock() @@ -438,7 +441,7 @@ func (s *v6Server) checkSID(msg *dhcpv6.Message) error { } // . IAAddress must be equal to the lease's IP -func (s *v6Server) checkIA(msg *dhcpv6.Message, lease *Lease) error { +func (s *v6Server) checkIA(msg *dhcpv6.Message, lease *dhcpsvc.Lease) error { switch msg.Type() { case dhcpv6.MessageTypeRequest, dhcpv6.MessageTypeConfirm, @@ -464,7 +467,7 @@ func (s *v6Server) checkIA(msg *dhcpv6.Message, lease *Lease) error { } // Store lease in DB (if necessary) and return lease life time -func (s *v6Server) commitLease(msg *dhcpv6.Message, lease *Lease) time.Duration { +func (s *v6Server) commitLease(msg *dhcpv6.Message, lease *dhcpsvc.Lease) time.Duration { lifetime := s.conf.leaseTime switch msg.Type() { @@ -506,7 +509,7 @@ func (s *v6Server) process(msg *dhcpv6.Message, req, resp dhcpv6.DHCPv6) bool { return false } - var lease *Lease + var lease *dhcpsvc.Lease func() { s.leasesLock.Lock() defer s.leasesLock.Unlock() diff --git a/internal/dhcpd/v6_unix_test.go b/internal/dhcpd/v6_unix_test.go index 3ed5221a..b642eed7 100644 --- a/internal/dhcpd/v6_unix_test.go +++ b/internal/dhcpd/v6_unix_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" "github.com/insomniacslk/dhcp/dhcpv6" "github.com/insomniacslk/dhcp/iana" "github.com/stretchr/testify/assert" @@ -28,7 +29,7 @@ func TestV6_AddRemove_static(t *testing.T) { require.Empty(t, s.GetLeases(LeasesStatic)) // Add static lease. - l := &Lease{ + l := &dhcpsvc.Lease{ IP: netip.MustParseAddr("2001::1"), HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, } @@ -47,7 +48,7 @@ func TestV6_AddRemove_static(t *testing.T) { assert.True(t, ls[0].IsStatic) // Try to remove non-existent static lease. - err = s.RemoveStaticLease(&Lease{ + err = s.RemoveStaticLease(&dhcpsvc.Lease{ IP: netip.MustParseAddr("2001::2"), HWAddr: l.HWAddr, }) @@ -72,7 +73,7 @@ func TestV6_AddReplace(t *testing.T) { require.True(t, ok) // Add dynamic leases. - dynLeases := []*Lease{{ + dynLeases := []*dhcpsvc.Lease{{ IP: netip.MustParseAddr("2001::1"), HWAddr: net.HardwareAddr{0x11, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, }, { @@ -84,7 +85,7 @@ func TestV6_AddReplace(t *testing.T) { s.addLease(l) } - stLeases := []*Lease{{ + stLeases := []*dhcpsvc.Lease{{ IP: netip.MustParseAddr("2001::1"), HWAddr: net.HardwareAddr{0x33, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, }, { @@ -126,7 +127,7 @@ func TestV6GetLease(t *testing.T) { LinkLayerAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, } - l := &Lease{ + l := &dhcpsvc.Lease{ IP: netip.MustParseAddr("2001::1"), HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, } @@ -324,7 +325,7 @@ func TestV6_FindMACbyIP(t *testing.T) { anotherMAC := net.HardwareAddr{0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB} s := &v6Server{ - leases: []*Lease{{ + leases: []*dhcpsvc.Lease{{ Hostname: staticName, HWAddr: staticMAC, IP: staticIP, @@ -337,7 +338,7 @@ func TestV6_FindMACbyIP(t *testing.T) { }}, } - s.leases = []*Lease{{ + s.leases = []*dhcpsvc.Lease{{ Hostname: staticName, HWAddr: staticMAC, IP: staticIP, diff --git a/internal/dhcpsvc/config.go b/internal/dhcpsvc/config.go index cd4bb148..52d28a33 100644 --- a/internal/dhcpsvc/config.go +++ b/internal/dhcpsvc/config.go @@ -43,7 +43,7 @@ func (conf *Config) Validate() (err error) { case !conf.Enabled: return nil case conf.ICMPTimeout < 0: - return fmt.Errorf("icmp timeout %s must be non-negative", conf.ICMPTimeout) + return newMustErr("icmp timeout", "be non-negative", conf.ICMPTimeout) } err = netutil.ValidateDomainName(conf.LocalDomainName) @@ -68,9 +68,9 @@ func (conf *Config) Validate() (err error) { return nil } -// mustBeErr returns an error that indicates that valName must be as must +// newMustErr returns an error that indicates that valName must be as must // describes. -func mustBeErr(valName, must string, val fmt.Stringer) (err error) { +func newMustErr(valName, must string, val fmt.Stringer) (err error) { return fmt.Errorf("%s %s must %s", valName, val, must) } diff --git a/internal/dhcpsvc/dhcpsvc.go b/internal/dhcpsvc/dhcpsvc.go index b0d83fb5..efa7404f 100644 --- a/internal/dhcpsvc/dhcpsvc.go +++ b/internal/dhcpsvc/dhcpsvc.go @@ -10,13 +10,13 @@ import ( "time" "github.com/AdguardTeam/AdGuardHome/internal/next/agh" + "golang.org/x/exp/slices" ) // Lease is a DHCP lease. // -// TODO(e.burkov): Consider it to [agh], since it also may be needed in -// [websvc]. Also think of implementing iterating methods with appropriate -// signatures. +// TODO(e.burkov): Consider moving it to [agh], since it also may be needed in +// [websvc]. type Lease struct { // IP is the IP address leased to the client. IP netip.Addr @@ -34,6 +34,21 @@ type Lease struct { IsStatic bool } +// Clone returns a deep copy of l. +func (l *Lease) Clone() (clone *Lease) { + if l == nil { + return nil + } + + return &Lease{ + Expiry: l.Expiry, + Hostname: l.Hostname, + HWAddr: slices.Clone(l.HWAddr), + IP: l.IP, + IsStatic: l.IsStatic, + } +} + type Interface interface { agh.ServiceWithConfig[*Config] @@ -57,6 +72,9 @@ type Interface interface { IPByHost(host string) (ip netip.Addr) // Leases returns all the active DHCP leases. + // + // TODO(e.burkov): Consider implementing iterating methods with appropriate + // signatures instead of cloning the whole list. Leases() (ls []*Lease) // AddLease adds a new DHCP lease. It returns an error if the lease is @@ -91,6 +109,9 @@ func (Empty) Shutdown(_ context.Context) (err error) { return nil } // Config implements the [ServiceWithConfig] interface for Empty. func (Empty) Config() (conf *Config) { return nil } +// type check +var _ Interface = Empty{} + // Enabled implements the [Interface] interface for Empty. func (Empty) Enabled() (ok bool) { return false } @@ -103,9 +124,6 @@ func (Empty) MACByIP(_ netip.Addr) (mac net.HardwareAddr) { return nil } // IPByHost implements the [Interface] interface for Empty. func (Empty) IPByHost(_ string) (ip netip.Addr) { return netip.Addr{} } -// type check -var _ Interface = Empty{} - // Leases implements the [Interface] interface for Empty. func (Empty) Leases() (leases []*Lease) { return nil } diff --git a/internal/dhcpsvc/server.go b/internal/dhcpsvc/server.go index bd0571fd..2d24ef27 100644 --- a/internal/dhcpsvc/server.go +++ b/internal/dhcpsvc/server.go @@ -25,6 +25,9 @@ type DHCPServer struct { // interfaces6 is the set of IPv6 interfaces sorted by interface name. interfaces6 []*iface6 + // leases is the set of active DHCP leases. + leases []*Lease + // icmpTimeout is the timeout for checking another DHCP server's presence. icmpTimeout time.Duration } @@ -75,3 +78,23 @@ func New(conf *Config) (srv *DHCPServer, err error) { icmpTimeout: conf.ICMPTimeout, }, nil } + +// type check +// +// TODO(e.burkov): Uncomment when the [Interface] interface is implemented. +// var _ Interface = (*DHCPServer)(nil) + +// Enabled implements the [Interface] interface for *DHCPServer. +func (srv *DHCPServer) Enabled() (ok bool) { + return srv.enabled.Load() +} + +// Leases implements the [Interface] interface for *DHCPServer. +func (srv *DHCPServer) Leases() (leases []*Lease) { + leases = make([]*Lease, 0, len(srv.leases)) + for _, lease := range srv.leases { + leases = append(leases, lease.Clone()) + } + + return leases +} diff --git a/internal/dhcpsvc/v4.go b/internal/dhcpsvc/v4.go index f53e7bba..44c4f840 100644 --- a/internal/dhcpsvc/v4.go +++ b/internal/dhcpsvc/v4.go @@ -45,15 +45,15 @@ func (conf *IPv4Config) validate() (err error) { case !conf.Enabled: return nil case !conf.GatewayIP.Is4(): - return mustBeErr("gateway ip", "be a valid ipv4", conf.GatewayIP) + return newMustErr("gateway ip", "be a valid ipv4", conf.GatewayIP) case !conf.SubnetMask.Is4(): - return mustBeErr("subnet mask", "be a valid ipv4 cidr mask", conf.SubnetMask) + return newMustErr("subnet mask", "be a valid ipv4 cidr mask", conf.SubnetMask) case !conf.RangeStart.Is4(): - return mustBeErr("range start", "be a valid ipv4", conf.RangeStart) + return newMustErr("range start", "be a valid ipv4", conf.RangeStart) case !conf.RangeEnd.Is4(): - return mustBeErr("range end", "be a valid ipv4", conf.RangeEnd) + return newMustErr("range end", "be a valid ipv4", conf.RangeEnd) case conf.LeaseDuration <= 0: - return mustBeErr("lease duration", "be less than %d", conf.LeaseDuration) + return newMustErr("lease duration", "be less than %d", conf.LeaseDuration) default: return nil } diff --git a/internal/home/clients_internal_test.go b/internal/home/clients_internal_test.go index 2efd2d4a..45e89cf3 100644 --- a/internal/home/clients_internal_test.go +++ b/internal/home/clients_internal_test.go @@ -314,7 +314,7 @@ func TestClientsAddExisting(t *testing.T) { clients.dhcp = dhcpServer - err = dhcpServer.AddStaticLease(&dhcpd.Lease{ + err = dhcpServer.AddStaticLease(&dhcpsvc.Lease{ HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, IP: ip, Hostname: "testhost",