From e269260fbe76c88853f64f0fdc69f2edd741726f Mon Sep 17 00:00:00 2001 From: Eugene Burkov Date: Tue, 9 Jul 2024 20:04:24 +0300 Subject: [PATCH] Pull request 2254: 4923 gopacket DHCP vol.9 Updates #4923. Squashed commit of the following: commit 05322419156d18502f3f937e789df02d78971b30 Author: Eugene Burkov Date: Tue Jul 9 18:18:35 2024 +0300 dhcpsvc: imp docs commit 083da3671320f7774db9c5b854e663162da9d214 Author: Eugene Burkov Date: Tue Jul 9 15:28:52 2024 +0300 dhcpsvc: imp code, tests commit 22e37e587e29c781abd4f2486f282dcfbffb4e6b Author: Eugene Burkov Date: Mon Jul 8 19:31:43 2024 +0300 dhcpsvc: imp tests commit 83ec7c54ef1e689a1f887e78e3055522539222d5 Author: Eugene Burkov Date: Mon Jul 8 18:56:21 2024 +0300 dhcpsvc: add db --- internal/dhcpsvc/config.go | 10 +- internal/dhcpsvc/config_test.go | 9 + internal/dhcpsvc/db.go | 192 +++++++++ internal/dhcpsvc/db_internal_test.go | 4 + internal/dhcpsvc/dhcpsvc_test.go | 66 +++ internal/dhcpsvc/leaseindex.go | 15 + internal/dhcpsvc/server.go | 59 ++- internal/dhcpsvc/server_test.go | 384 ++++++++---------- .../TestDHCPServer_RemoveLease/leases.json | 19 + .../testdata/TestDHCPServer_Reset/leases.json | 33 ++ .../leases.json | 26 ++ .../testdata/TestDHCPServer_index/leases.json | 33 ++ .../testdata/TestServer_Leases/leases.json | 15 + internal/dhcpsvc/v4.go | 1 + 14 files changed, 639 insertions(+), 227 deletions(-) create mode 100644 internal/dhcpsvc/db.go create mode 100644 internal/dhcpsvc/db_internal_test.go create mode 100644 internal/dhcpsvc/dhcpsvc_test.go create mode 100644 internal/dhcpsvc/testdata/TestDHCPServer_RemoveLease/leases.json create mode 100644 internal/dhcpsvc/testdata/TestDHCPServer_Reset/leases.json create mode 100644 internal/dhcpsvc/testdata/TestDHCPServer_UpdateStaticLease/leases.json create mode 100644 internal/dhcpsvc/testdata/TestDHCPServer_index/leases.json create mode 100644 internal/dhcpsvc/testdata/TestServer_Leases/leases.json diff --git a/internal/dhcpsvc/config.go b/internal/dhcpsvc/config.go index c1d7910d..464c497d 100644 --- a/internal/dhcpsvc/config.go +++ b/internal/dhcpsvc/config.go @@ -3,6 +3,7 @@ package dhcpsvc import ( "fmt" "log/slog" + "os" "time" "github.com/AdguardTeam/golibs/errors" @@ -23,7 +24,8 @@ type Config struct { // clients' hostnames. LocalDomainName string - // TODO(e.burkov): Add DB path. + // DBFilePath is the path to the database file containing the DHCP leases. + DBFilePath string // ICMPTimeout is the timeout for checking another DHCP server's presence. ICMPTimeout time.Duration @@ -64,6 +66,12 @@ func (conf *Config) Validate() (err error) { errs = append(errs, err) } + // This is a best-effort check for the file accessibility. The file will be + // checked again when it is opened later. + if _, err = os.Stat(conf.DBFilePath); err != nil && !errors.Is(err, os.ErrNotExist) { + errs = append(errs, fmt.Errorf("db file path %q: %w", conf.DBFilePath, err)) + } + if len(conf.Interfaces) == 0 { errs = append(errs, errNoInterfaces) diff --git a/internal/dhcpsvc/config_test.go b/internal/dhcpsvc/config_test.go index aa87b0d6..85dab4a9 100644 --- a/internal/dhcpsvc/config_test.go +++ b/internal/dhcpsvc/config_test.go @@ -1,6 +1,7 @@ package dhcpsvc_test import ( + "path/filepath" "testing" "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" @@ -8,6 +9,8 @@ import ( ) func TestConfig_Validate(t *testing.T) { + leasesPath := filepath.Join(t.TempDir(), "leases.json") + testCases := []struct { name string conf *dhcpsvc.Config @@ -25,6 +28,7 @@ func TestConfig_Validate(t *testing.T) { conf: &dhcpsvc.Config{ Enabled: true, Interfaces: testInterfaceConf, + DBFilePath: leasesPath, }, wantErrMsg: `bad domain name "": domain name is empty`, }, { @@ -32,6 +36,7 @@ func TestConfig_Validate(t *testing.T) { Enabled: true, LocalDomainName: testLocalTLD, Interfaces: nil, + DBFilePath: leasesPath, }, name: "no_interfaces", wantErrMsg: "no interfaces specified", @@ -40,6 +45,7 @@ func TestConfig_Validate(t *testing.T) { Enabled: true, LocalDomainName: testLocalTLD, Interfaces: nil, + DBFilePath: leasesPath, }, name: "no_interfaces", wantErrMsg: "no interfaces specified", @@ -50,6 +56,7 @@ func TestConfig_Validate(t *testing.T) { Interfaces: map[string]*dhcpsvc.InterfaceConfig{ "eth0": nil, }, + DBFilePath: leasesPath, }, name: "nil_interface", wantErrMsg: `interface "eth0": config is nil`, @@ -63,6 +70,7 @@ func TestConfig_Validate(t *testing.T) { IPv6: &dhcpsvc.IPv6Config{Enabled: false}, }, }, + DBFilePath: leasesPath, }, name: "nil_ipv4", wantErrMsg: `interface "eth0": ipv4: config is nil`, @@ -76,6 +84,7 @@ func TestConfig_Validate(t *testing.T) { IPv6: nil, }, }, + DBFilePath: leasesPath, }, name: "nil_ipv6", wantErrMsg: `interface "eth0": ipv6: config is nil`, diff --git a/internal/dhcpsvc/db.go b/internal/dhcpsvc/db.go new file mode 100644 index 00000000..b247e653 --- /dev/null +++ b/internal/dhcpsvc/db.go @@ -0,0 +1,192 @@ +package dhcpsvc + +import ( + "context" + "encoding/json" + "fmt" + "io/fs" + "net" + "net/netip" + "os" + "slices" + "strings" + "time" + + "github.com/AdguardTeam/golibs/errors" + "github.com/AdguardTeam/golibs/logutil/slogutil" + "github.com/google/renameio/v2/maybe" +) + +// dataVersion is the current version of the stored DHCP leases structure. +const dataVersion = 1 + +// databasePerm is the permissions for the database file. +const databasePerm fs.FileMode = 0o640 + +// dataLeases is the structure of the stored DHCP leases. +type dataLeases struct { + // Leases is the list containing stored DHCP leases. + Leases []*dbLease `json:"leases"` + + // Version is the current version of the structure. + Version int `json:"version"` +} + +// 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"` +} + +// compareNames returns the result of comparing the hostnames of dl and other +// lexicographically. +func (dl *dbLease) compareNames(other *dbLease) (res int) { + return strings.Compare(dl.Hostname, other.Hostname) +} + +// toDBLease converts *Lease to *dbLease. +func toDBLease(l *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, + } +} + +// toInternal converts dl to *Lease. +func (dl *dbLease) toInternal() (l *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 &Lease{ + Expiry: expiry, + IP: dl.IP, + Hostname: dl.Hostname, + HWAddr: mac, + IsStatic: dl.IsStatic, + }, nil +} + +// dbLoad loads stored leases. It must only be called before the service has +// been started. +func (srv *DHCPServer) dbLoad(ctx context.Context) (err error) { + defer func() { err = errors.Annotate(err, "loading db: %w") }() + + file, err := os.Open(srv.dbFilePath) + if err != nil { + if !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("reading db: %w", err) + } + + srv.logger.DebugContext(ctx, "no db file found") + + return nil + } + + dl := &dataLeases{} + err = json.NewDecoder(file).Decode(dl) + if err != nil { + return fmt.Errorf("decoding db: %w", err) + } + + srv.resetLeases() + srv.addDBLeases(ctx, dl.Leases) + + return nil +} + +// addDBLeases adds leases to the server. +func (srv *DHCPServer) addDBLeases(ctx context.Context, leases []*dbLease) { + var v4, v6 uint + for i, l := range leases { + lease, err := l.toInternal() + if err != nil { + srv.logger.WarnContext(ctx, "converting lease", "idx", i, slogutil.KeyError, err) + + continue + } + + iface, err := srv.ifaceForAddr(l.IP) + if err != nil { + srv.logger.WarnContext(ctx, "searching lease iface", "idx", i, slogutil.KeyError, err) + + continue + } + + err = srv.leases.add(lease, iface) + if err != nil { + srv.logger.WarnContext(ctx, "adding lease", "idx", i, slogutil.KeyError, err) + + continue + } + + if l.IP.Is4() { + v4++ + } else { + v6++ + } + } + + // TODO(e.burkov): Group by interface. + srv.logger.InfoContext(ctx, "loaded leases", "v4", v4, "v6", v6, "total", len(leases)) +} + +// writeDB writes leases to the database file. It expects the +// [DHCPServer.leasesMu] to be locked. +func (srv *DHCPServer) dbStore(ctx context.Context) (err error) { + defer func() { err = errors.Annotate(err, "writing db: %w") }() + + dl := &dataLeases{ + // Avoid writing null into the database file if there are no leases. + Leases: make([]*dbLease, 0, srv.leases.len()), + Version: dataVersion, + } + + srv.leases.rangeLeases(func(l *Lease) (cont bool) { + lease := toDBLease(l) + i, _ := slices.BinarySearchFunc(dl.Leases, lease, (*dbLease).compareNames) + dl.Leases = slices.Insert(dl.Leases, i, lease) + + return true + }) + + buf, err := json.Marshal(dl) + if err != nil { + // Don't wrap the error since it's informative enough as is. + return err + } + + err = maybe.WriteFile(srv.dbFilePath, buf, databasePerm) + if err != nil { + // Don't wrap the error since it's informative enough as is. + return err + } + + srv.logger.InfoContext(ctx, "stored leases", "num", len(dl.Leases), "file", srv.dbFilePath) + + return nil +} diff --git a/internal/dhcpsvc/db_internal_test.go b/internal/dhcpsvc/db_internal_test.go new file mode 100644 index 00000000..aa47c0da --- /dev/null +++ b/internal/dhcpsvc/db_internal_test.go @@ -0,0 +1,4 @@ +package dhcpsvc + +// DatabasePerm is the permissions for the test database file. +const DatabasePerm = databasePerm diff --git a/internal/dhcpsvc/dhcpsvc_test.go b/internal/dhcpsvc/dhcpsvc_test.go new file mode 100644 index 00000000..f8b993f6 --- /dev/null +++ b/internal/dhcpsvc/dhcpsvc_test.go @@ -0,0 +1,66 @@ +package dhcpsvc_test + +import ( + "net" + "net/netip" + "time" + + "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" + "github.com/AdguardTeam/golibs/logutil/slogutil" + "github.com/stretchr/testify/require" +) + +// testLocalTLD is a common local TLD for tests. +const testLocalTLD = "local" + +// testTimeout is a common timeout for tests and contexts. +const testTimeout time.Duration = 10 * time.Second + +// discardLog is a logger to discard test output. +var discardLog = slogutil.NewDiscardLogger() + +// 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 +} diff --git a/internal/dhcpsvc/leaseindex.go b/internal/dhcpsvc/leaseindex.go index c9487b75..855d6b84 100644 --- a/internal/dhcpsvc/leaseindex.go +++ b/internal/dhcpsvc/leaseindex.go @@ -124,3 +124,18 @@ func (idx *leaseIndex) update(l *Lease, iface *netInterface) (err error) { return nil } + +// rangeLeases calls f for each lease in idx in an unspecified order until f +// returns false. +func (idx *leaseIndex) rangeLeases(f func(l *Lease) (cont bool)) { + for _, l := range idx.byName { + if !f(l) { + break + } + } +} + +// len returns the number of leases in idx. +func (idx *leaseIndex) len() (l uint) { + return uint(len(idx.byAddr)) +} diff --git a/internal/dhcpsvc/server.go b/internal/dhcpsvc/server.go index cd1e93b2..745895ff 100644 --- a/internal/dhcpsvc/server.go +++ b/internal/dhcpsvc/server.go @@ -27,6 +27,13 @@ type DHCPServer struct { // hostnames. localTLD string + // dbFilePath is the path to the database file containing the DHCP leases. + // + // TODO(e.burkov): Consider extracting the database logic into a separate + // interface to prevent packages that only need lease data from depending on + // the entire server and to simplify testing. + dbFilePath string + // leasesMu protects the leases index as well as leases in the interfaces. leasesMu *sync.RWMutex @@ -93,9 +100,14 @@ func New(ctx context.Context, conf *Config) (srv *DHCPServer, err error) { interfaces4: ifaces4, interfaces6: ifaces6, icmpTimeout: conf.ICMPTimeout, + dbFilePath: conf.DBFilePath, } - // TODO(e.burkov): Load leases. + err = srv.dbLoad(ctx) + if err != nil { + // Don't wrap the error since it's informative enough as is. + return nil, err + } return srv, nil } @@ -167,9 +179,26 @@ func (srv *DHCPServer) IPByHost(host string) (ip netip.Addr) { // Reset implements the [Interface] interface for *DHCPServer. func (srv *DHCPServer) Reset(ctx context.Context) (err error) { + defer func() { err = errors.Annotate(err, "resetting leases: %w") }() + srv.leasesMu.Lock() defer srv.leasesMu.Unlock() + srv.resetLeases() + err = srv.dbStore(ctx) + if err != nil { + // Don't wrap the error since there is already an annotation deferred. + return err + } + + srv.logger.DebugContext(ctx, "reset leases") + + return nil +} + +// resetLeases resets the leases for all network interfaces of the server. It +// expects the DHCPServer.leasesMu to be locked. +func (srv *DHCPServer) resetLeases() { for _, iface := range srv.interfaces4 { iface.reset() } @@ -177,10 +206,6 @@ func (srv *DHCPServer) Reset(ctx context.Context) (err error) { iface.reset() } srv.leases.clear() - - srv.logger.DebugContext(ctx, "reset leases") - - return nil } // AddLease implements the [Interface] interface for *DHCPServer. @@ -190,7 +215,7 @@ func (srv *DHCPServer) AddLease(ctx context.Context, l *Lease) (err error) { addr := l.IP iface, err := srv.ifaceForAddr(addr) if err != nil { - // Don't wrap the error since there is already an annotation deferred. + // Don't wrap the error since it's already informative enough as is. return err } @@ -203,6 +228,12 @@ func (srv *DHCPServer) AddLease(ctx context.Context, l *Lease) (err error) { return err } + err = srv.dbStore(ctx) + if err != nil { + // Don't wrap the error since it's already informative enough as is. + return err + } + iface.logger.DebugContext( ctx, "added lease", "hostname", l.Hostname, @@ -223,7 +254,7 @@ func (srv *DHCPServer) UpdateStaticLease(ctx context.Context, l *Lease) (err err addr := l.IP iface, err := srv.ifaceForAddr(addr) if err != nil { - // Don't wrap the error since there is already an annotation deferred. + // Don't wrap the error since it's already informative enough as is. return err } @@ -236,6 +267,12 @@ func (srv *DHCPServer) UpdateStaticLease(ctx context.Context, l *Lease) (err err return err } + err = srv.dbStore(ctx) + if err != nil { + // Don't wrap the error since it's already informative enough as is. + return err + } + iface.logger.DebugContext( ctx, "updated lease", "hostname", l.Hostname, @@ -254,7 +291,7 @@ func (srv *DHCPServer) RemoveLease(ctx context.Context, l *Lease) (err error) { addr := l.IP iface, err := srv.ifaceForAddr(addr) if err != nil { - // Don't wrap the error since there is already an annotation deferred. + // Don't wrap the error since it's already informative enough as is. return err } @@ -267,6 +304,12 @@ func (srv *DHCPServer) RemoveLease(ctx context.Context, l *Lease) (err error) { return err } + err = srv.dbStore(ctx) + if err != nil { + // Don't wrap the error since it's already informative enough as is. + return err + } + iface.logger.DebugContext( ctx, "removed lease", "hostname", l.Hostname, diff --git a/internal/dhcpsvc/server_test.go b/internal/dhcpsvc/server_test.go index 5f6f002f..0166a9b7 100644 --- a/internal/dhcpsvc/server_test.go +++ b/internal/dhcpsvc/server_test.go @@ -1,72 +1,40 @@ package dhcpsvc_test import ( - "net" + "io/fs" "net/netip" + "os" + "path/filepath" "strings" "testing" "time" "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" - "github.com/AdguardTeam/golibs/logutil/slogutil" "github.com/AdguardTeam/golibs/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -// testLocalTLD is a common local TLD for tests. -const testLocalTLD = "local" +// testdata is a filesystem containing data for tests. +var testdata = os.DirFS("testdata") -// testTimeout is a common timeout for tests and contexts. -const testTimeout time.Duration = 10 * time.Second +// newTempDB copies the leases database file located in the testdata FS, under +// tb.Name()/leases.db, to a temporary directory and returns the path to the +// copied file. +func newTempDB(tb testing.TB) (dst string) { + tb.Helper() -// discardLog is a logger to discard test output. -var discardLog = slogutil.NewDiscardLogger() + const filename = "leases.json" -// 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, - }, - }, -} + data, err := fs.ReadFile(testdata, filepath.Join(tb.Name(), filename)) + require.NoError(tb, err) -// 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) + dst = filepath.Join(tb.TempDir(), filename) - return mac + err = os.WriteFile(dst, data, dhcpsvc.DatabasePerm) + require.NoError(tb, err) + + return dst } func TestNew(t *testing.T) { @@ -103,6 +71,8 @@ func TestNew(t *testing.T) { RASLAACOnly: true, } + leasesPath := filepath.Join(t.TempDir(), "leases.json") + testCases := []struct { conf *dhcpsvc.Config name string @@ -118,6 +88,7 @@ func TestNew(t *testing.T) { IPv6: validIPv6Conf, }, }, + DBFilePath: leasesPath, }, name: "valid", wantErrMsg: "", @@ -132,6 +103,7 @@ func TestNew(t *testing.T) { IPv6: &dhcpsvc.IPv6Config{Enabled: false}, }, }, + DBFilePath: leasesPath, }, name: "disabled_interfaces", wantErrMsg: "", @@ -146,6 +118,7 @@ func TestNew(t *testing.T) { IPv6: validIPv6Conf, }, }, + DBFilePath: leasesPath, }, name: "gateway_within_range", wantErrMsg: `interface "eth0": ipv4: ` + @@ -161,6 +134,7 @@ func TestNew(t *testing.T) { IPv6: validIPv6Conf, }, }, + DBFilePath: leasesPath, }, name: "bad_start", wantErrMsg: `interface "eth0": ipv4: ` + @@ -180,32 +154,36 @@ func TestNew(t *testing.T) { func TestDHCPServer_AddLease(t *testing.T) { ctx := testutil.ContextWithTimeout(t, testTimeout) + leasesPath := filepath.Join(t.TempDir(), "leases.json") srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{ Enabled: true, Logger: discardLog, LocalDomainName: testLocalTLD, Interfaces: testInterfaceConf, + DBFilePath: leasesPath, }) require.NoError(t, err) const ( - host1 = "host1" - host2 = "host2" - host3 = "host3" + existHost = "host1" + newHost = "host2" + ipv6Host = "host3" ) - ip1 := netip.MustParseAddr("192.168.0.2") - ip2 := netip.MustParseAddr("192.168.0.3") - ip3 := netip.MustParseAddr("2001:db8::2") + var ( + existIP = netip.MustParseAddr("192.168.0.2") + newIP = netip.MustParseAddr("192.168.0.3") + newIPv6 = 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") + existMAC = mustParseMAC(t, "01:02:03:04:05:06") + newMAC = mustParseMAC(t, "06:05:04:03:02:01") + ipv6MAC = mustParseMAC(t, "02:03:04:05:06:07") + ) require.NoError(t, srv.AddLease(ctx, &dhcpsvc.Lease{ - Hostname: host1, - IP: ip1, - HWAddr: mac1, + Hostname: existHost, + IP: existIP, + HWAddr: existMAC, IsStatic: true, })) @@ -216,61 +194,61 @@ func TestDHCPServer_AddLease(t *testing.T) { }{{ name: "outside_range", lease: &dhcpsvc.Lease{ - Hostname: host2, + Hostname: newHost, IP: netip.MustParseAddr("1.2.3.4"), - HWAddr: mac2, + HWAddr: newMAC, }, wantErrMsg: "adding lease: no interface for ip 1.2.3.4", }, { name: "duplicate_ip", lease: &dhcpsvc.Lease{ - Hostname: host2, - IP: ip1, - HWAddr: mac2, + Hostname: newHost, + IP: existIP, + HWAddr: newMAC, }, - wantErrMsg: "adding lease: lease for ip " + ip1.String() + + wantErrMsg: "adding lease: lease for ip " + existIP.String() + " already exists", }, { name: "duplicate_hostname", lease: &dhcpsvc.Lease{ - Hostname: host1, - IP: ip2, - HWAddr: mac2, + Hostname: existHost, + IP: newIP, + HWAddr: newMAC, }, - wantErrMsg: "adding lease: lease for hostname " + host1 + + wantErrMsg: "adding lease: lease for hostname " + existHost + " already exists", }, { name: "duplicate_hostname_case", lease: &dhcpsvc.Lease{ - Hostname: strings.ToUpper(host1), - IP: ip2, - HWAddr: mac2, + Hostname: strings.ToUpper(existHost), + IP: newIP, + HWAddr: newMAC, }, wantErrMsg: "adding lease: lease for hostname " + - strings.ToUpper(host1) + " already exists", + strings.ToUpper(existHost) + " already exists", }, { name: "duplicate_mac", lease: &dhcpsvc.Lease{ - Hostname: host2, - IP: ip2, - HWAddr: mac1, + Hostname: newHost, + IP: newIP, + HWAddr: existMAC, }, - wantErrMsg: "adding lease: lease for mac " + mac1.String() + + wantErrMsg: "adding lease: lease for mac " + existMAC.String() + " already exists", }, { name: "valid", lease: &dhcpsvc.Lease{ - Hostname: host2, - IP: ip2, - HWAddr: mac2, + Hostname: newHost, + IP: newIP, + HWAddr: newMAC, }, wantErrMsg: "", }, { name: "valid_v6", lease: &dhcpsvc.Lease{ - Hostname: host3, - IP: ip3, - HWAddr: mac3, + Hostname: ipv6Host, + IP: newIPv6, + HWAddr: ipv6MAC, }, wantErrMsg: "", }} @@ -280,16 +258,21 @@ func TestDHCPServer_AddLease(t *testing.T) { testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.AddLease(ctx, tc.lease)) }) } + + assert.NotEmpty(t, srv.Leases()) + assert.FileExists(t, leasesPath) } func TestDHCPServer_index(t *testing.T) { ctx := testutil.ContextWithTimeout(t, testTimeout) + leasesPath := newTempDB(t) srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{ Enabled: true, Logger: discardLog, LocalDomainName: testLocalTLD, Interfaces: testInterfaceConf, + DBFilePath: leasesPath, }) require.NoError(t, err) @@ -301,46 +284,23 @@ func TestDHCPServer_index(t *testing.T) { host5 = "host5" ) - ip1 := netip.MustParseAddr("192.168.0.2") - ip2 := netip.MustParseAddr("192.168.0.3") - ip3 := netip.MustParseAddr("172.16.0.3") - ip4 := netip.MustParseAddr("172.16.0.4") + var ( + ip1 = netip.MustParseAddr("192.168.0.2") + ip2 = netip.MustParseAddr("192.168.0.3") + ip3 = netip.MustParseAddr("172.16.0.3") + ip4 = netip.MustParseAddr("172.16.0.4") - 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, - IP: ip1, - HWAddr: mac1, - IsStatic: true, - }, { - Hostname: host2, - IP: ip2, - HWAddr: mac2, - IsStatic: true, - }, { - Hostname: host3, - IP: ip3, - HWAddr: mac3, - IsStatic: true, - }, { - Hostname: host4, - IP: ip4, - HWAddr: mac1, - IsStatic: true, - }} - for _, l := range leases { - require.NoError(t, srv.AddLease(ctx, l)) - } + 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") + ) t.Run("ip_idx", func(t *testing.T) { assert.Equal(t, ip1, srv.IPByHost(host1)) assert.Equal(t, ip2, srv.IPByHost(host2)) assert.Equal(t, ip3, srv.IPByHost(host3)) assert.Equal(t, ip4, srv.IPByHost(host4)) - assert.Equal(t, netip.Addr{}, srv.IPByHost(host5)) + assert.Zero(t, srv.IPByHost(host5)) }) t.Run("name_idx", func(t *testing.T) { @@ -348,7 +308,7 @@ func TestDHCPServer_index(t *testing.T) { assert.Equal(t, host2, srv.HostByIP(ip2)) assert.Equal(t, host3, srv.HostByIP(ip3)) assert.Equal(t, host4, srv.HostByIP(ip4)) - assert.Equal(t, "", srv.HostByIP(netip.Addr{})) + assert.Zero(t, srv.HostByIP(netip.Addr{})) }) t.Run("mac_idx", func(t *testing.T) { @@ -356,18 +316,20 @@ func TestDHCPServer_index(t *testing.T) { assert.Equal(t, mac2, srv.MACByIP(ip2)) assert.Equal(t, mac3, srv.MACByIP(ip3)) assert.Equal(t, mac1, srv.MACByIP(ip4)) - assert.Nil(t, srv.MACByIP(netip.Addr{})) + assert.Zero(t, srv.MACByIP(netip.Addr{})) }) } func TestDHCPServer_UpdateStaticLease(t *testing.T) { ctx := testutil.ContextWithTimeout(t, testTimeout) + leasesPath := newTempDB(t) srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{ Enabled: true, Logger: discardLog, LocalDomainName: testLocalTLD, Interfaces: testInterfaceConf, + DBFilePath: leasesPath, }) require.NoError(t, err) @@ -380,36 +342,16 @@ func TestDHCPServer_UpdateStaticLease(t *testing.T) { 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") + var ( + 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::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(ctx, l)) - } + mac1 = mustParseMAC(t, "01:02:03:04:05:06") + mac2 = mustParseMAC(t, "06:05:04:03:02:01") + mac3 = mustParseMAC(t, "06:05:04:03:02:02") + ) testCases := []struct { name string @@ -428,9 +370,9 @@ func TestDHCPServer_UpdateStaticLease(t *testing.T) { lease: &dhcpsvc.Lease{ Hostname: host3, IP: ip3, - HWAddr: mac3, + HWAddr: mac2, }, - wantErrMsg: "updating static lease: no lease for mac " + mac3.String(), + wantErrMsg: "updating static lease: no lease for mac " + mac2.String(), }, { name: "duplicate_ip", lease: &dhcpsvc.Lease{ @@ -470,8 +412,8 @@ func TestDHCPServer_UpdateStaticLease(t *testing.T) { name: "valid_v6", lease: &dhcpsvc.Lease{ Hostname: host6, - IP: ip5, - HWAddr: mac4, + IP: ip4, + HWAddr: mac3, }, wantErrMsg: "", }} @@ -481,16 +423,20 @@ func TestDHCPServer_UpdateStaticLease(t *testing.T) { testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.UpdateStaticLease(ctx, tc.lease)) }) } + + assert.FileExists(t, leasesPath) } func TestDHCPServer_RemoveLease(t *testing.T) { ctx := testutil.ContextWithTimeout(t, testTimeout) + leasesPath := newTempDB(t) srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{ Enabled: true, Logger: discardLog, LocalDomainName: testLocalTLD, Interfaces: testInterfaceConf, + DBFilePath: leasesPath, }) require.NoError(t, err) @@ -500,28 +446,15 @@ func TestDHCPServer_RemoveLease(t *testing.T) { host3 = "host3" ) - ip1 := netip.MustParseAddr("192.168.0.2") - ip2 := netip.MustParseAddr("192.168.0.3") - ip3 := netip.MustParseAddr("2001:db8::2") + var ( + existIP = netip.MustParseAddr("192.168.0.2") + newIP = netip.MustParseAddr("192.168.0.3") + newIPv6 = 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(ctx, l)) - } + existMAC = mustParseMAC(t, "01:02:03:04:05:06") + newMAC = mustParseMAC(t, "02:03:04:05:06:07") + ipv6MAC = mustParseMAC(t, "06:05:04:03:02:01") + ) testCases := []struct { name string @@ -531,40 +464,40 @@ func TestDHCPServer_RemoveLease(t *testing.T) { name: "not_found_mac", lease: &dhcpsvc.Lease{ Hostname: host1, - IP: ip1, - HWAddr: mac2, + IP: existIP, + HWAddr: newMAC, }, - wantErrMsg: "removing lease: no lease for mac " + mac2.String(), + wantErrMsg: "removing lease: no lease for mac " + newMAC.String(), }, { name: "not_found_ip", lease: &dhcpsvc.Lease{ Hostname: host1, - IP: ip2, - HWAddr: mac1, + IP: newIP, + HWAddr: existMAC, }, - wantErrMsg: "removing lease: no lease for ip " + ip2.String(), + wantErrMsg: "removing lease: no lease for ip " + newIP.String(), }, { name: "not_found_host", lease: &dhcpsvc.Lease{ Hostname: host2, - IP: ip1, - HWAddr: mac1, + IP: existIP, + HWAddr: existMAC, }, wantErrMsg: "removing lease: no lease for hostname " + host2, }, { name: "valid", lease: &dhcpsvc.Lease{ Hostname: host1, - IP: ip1, - HWAddr: mac1, + IP: existIP, + HWAddr: existMAC, }, wantErrMsg: "", }, { name: "valid_v6", lease: &dhcpsvc.Lease{ Hostname: host3, - IP: ip3, - HWAddr: mac3, + IP: newIPv6, + HWAddr: ipv6MAC, }, wantErrMsg: "", }} @@ -575,49 +508,64 @@ func TestDHCPServer_RemoveLease(t *testing.T) { }) } + assert.FileExists(t, leasesPath) assert.Empty(t, srv.Leases()) } func TestDHCPServer_Reset(t *testing.T) { - ctx := testutil.ContextWithTimeout(t, testTimeout) - - srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{ + leasesPath := newTempDB(t) + conf := &dhcpsvc.Config{ Enabled: true, Logger: discardLog, 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(ctx, l)) + DBFilePath: leasesPath, } - require.Len(t, srv.Leases(), len(leases)) + ctx := testutil.ContextWithTimeout(t, testTimeout) + srv, err := dhcpsvc.New(ctx, conf) + require.NoError(t, err) + + const leasesNum = 4 + + require.Len(t, srv.Leases(), leasesNum) require.NoError(t, srv.Reset(ctx)) + assert.FileExists(t, leasesPath) assert.Empty(t, srv.Leases()) } + +func TestServer_Leases(t *testing.T) { + leasesPath := newTempDB(t) + conf := &dhcpsvc.Config{ + Enabled: true, + Logger: discardLog, + LocalDomainName: testLocalTLD, + Interfaces: testInterfaceConf, + DBFilePath: leasesPath, + } + + ctx := testutil.ContextWithTimeout(t, testTimeout) + + srv, err := dhcpsvc.New(ctx, conf) + require.NoError(t, err) + + expiry, err := time.Parse(time.RFC3339, "2042-01-02T03:04:05Z") + require.NoError(t, err) + + wantLeases := []*dhcpsvc.Lease{{ + Expiry: expiry, + IP: netip.MustParseAddr("192.168.0.3"), + Hostname: "example.host", + HWAddr: mustParseMAC(t, "AA:AA:AA:AA:AA:AA"), + IsStatic: false, + }, { + Expiry: time.Time{}, + IP: netip.MustParseAddr("192.168.0.4"), + Hostname: "example.static.host", + HWAddr: mustParseMAC(t, "BB:BB:BB:BB:BB:BB"), + IsStatic: true, + }} + assert.Equal(t, wantLeases, srv.Leases()) +} diff --git a/internal/dhcpsvc/testdata/TestDHCPServer_RemoveLease/leases.json b/internal/dhcpsvc/testdata/TestDHCPServer_RemoveLease/leases.json new file mode 100644 index 00000000..ef33c846 --- /dev/null +++ b/internal/dhcpsvc/testdata/TestDHCPServer_RemoveLease/leases.json @@ -0,0 +1,19 @@ +{ + "leases": [ + { + "expires": "", + "ip": "192.168.0.2", + "hostname": "host1", + "mac": "01:02:03:04:05:06", + "static": true + }, + { + "expires": "", + "ip": "2001:db8::2", + "hostname": "host3", + "mac": "06:05:04:03:02:01", + "static": true + } + ], + "version": 1 +} diff --git a/internal/dhcpsvc/testdata/TestDHCPServer_Reset/leases.json b/internal/dhcpsvc/testdata/TestDHCPServer_Reset/leases.json new file mode 100644 index 00000000..bab868ce --- /dev/null +++ b/internal/dhcpsvc/testdata/TestDHCPServer_Reset/leases.json @@ -0,0 +1,33 @@ +{ + "leases": [ + { + "expires": "", + "ip": "192.168.0.2", + "hostname": "host1", + "mac": "01:02:03:04:05:06", + "static": true + }, + { + "expires": "", + "ip": "192.168.0.3", + "hostname": "host2", + "mac": "06:05:04:03:02:01", + "static": true + }, + { + "expires": "", + "ip": "2001:db8::2", + "hostname": "host3", + "mac": "02:03:04:05:06:07", + "static": true + }, + { + "expires": "", + "ip": "2001:db8::3", + "hostname": "host4", + "mac": "06:05:04:03:02:02", + "static": true + } + ], + "version": 1 +} diff --git a/internal/dhcpsvc/testdata/TestDHCPServer_UpdateStaticLease/leases.json b/internal/dhcpsvc/testdata/TestDHCPServer_UpdateStaticLease/leases.json new file mode 100644 index 00000000..6506b4d4 --- /dev/null +++ b/internal/dhcpsvc/testdata/TestDHCPServer_UpdateStaticLease/leases.json @@ -0,0 +1,26 @@ +{ + "leases": [ + { + "expires": "", + "ip": "192.168.0.2", + "hostname": "host1", + "mac": "01:02:03:04:05:06", + "static": true + }, + { + "expires": "", + "ip": "192.168.0.3", + "hostname": "host2", + "mac": "01:02:03:04:05:07", + "static": true + }, + { + "expires": "", + "ip": "2001:db8::2", + "hostname": "host4", + "mac": "06:05:04:03:02:02", + "static": true + } + ], + "version": 1 +} diff --git a/internal/dhcpsvc/testdata/TestDHCPServer_index/leases.json b/internal/dhcpsvc/testdata/TestDHCPServer_index/leases.json new file mode 100644 index 00000000..2c86b108 --- /dev/null +++ b/internal/dhcpsvc/testdata/TestDHCPServer_index/leases.json @@ -0,0 +1,33 @@ +{ + "leases": [ + { + "expires": "", + "ip": "192.168.0.2", + "hostname": "host1", + "mac": "01:02:03:04:05:06", + "static": true + }, + { + "expires": "", + "ip": "192.168.0.3", + "hostname": "host2", + "mac": "06:05:04:03:02:01", + "static": true + }, + { + "expires": "", + "ip": "172.16.0.3", + "hostname": "host3", + "mac": "02:03:04:05:06:07", + "static": true + }, + { + "expires": "", + "ip": "172.16.0.4", + "hostname": "host4", + "mac": "01:02:03:04:05:06", + "static": true + } + ], + "version": 1 +} diff --git a/internal/dhcpsvc/testdata/TestServer_Leases/leases.json b/internal/dhcpsvc/testdata/TestServer_Leases/leases.json new file mode 100644 index 00000000..c5ccad0d --- /dev/null +++ b/internal/dhcpsvc/testdata/TestServer_Leases/leases.json @@ -0,0 +1,15 @@ +{ + "leases": [{ + "expires": "2042-01-02T03:04:05Z", + "ip": "192.168.0.3", + "hostname": "example.host", + "mac": "AA:AA:AA:AA:AA:AA", + "static": false + }, { + "ip": "192.168.0.4", + "hostname": "example.static.host", + "mac": "BB:BB:BB:BB:BB:BB", + "static": true + }], + "version": 1 +} diff --git a/internal/dhcpsvc/v4.go b/internal/dhcpsvc/v4.go index 09df8013..10624105 100644 --- a/internal/dhcpsvc/v4.go +++ b/internal/dhcpsvc/v4.go @@ -120,6 +120,7 @@ func newNetInterfaceV4( keyInterface, name, keyFamily, netutil.AddrFamilyIPv4, ) + if !conf.Enabled { l.DebugContext(ctx, "disabled")