2020-11-16 19:01:12 +03:00
|
|
|
// Package dhcpd provides a DHCP server.
|
2018-12-28 17:17:51 +03:00
|
|
|
package dhcpd
|
|
|
|
|
|
|
|
import (
|
2021-01-20 15:59:24 +03:00
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2018-12-28 17:17:51 +03:00
|
|
|
"net"
|
2023-03-01 13:27:24 +03:00
|
|
|
"net/netip"
|
2019-08-19 14:27:02 +03:00
|
|
|
"path/filepath"
|
2018-12-28 17:17:51 +03:00
|
|
|
"time"
|
|
|
|
|
2019-02-25 16:44:22 +03:00
|
|
|
"github.com/AdguardTeam/golibs/log"
|
2022-09-13 23:45:35 +03:00
|
|
|
"github.com/AdguardTeam/golibs/timeutil"
|
2022-10-21 20:42:00 +03:00
|
|
|
"golang.org/x/exp/slices"
|
2018-12-28 17:17:51 +03:00
|
|
|
)
|
|
|
|
|
2020-11-16 19:01:12 +03:00
|
|
|
const (
|
2021-02-18 14:11:54 +03:00
|
|
|
// leaseExpireStatic is used to define the Expiry field for static
|
|
|
|
// leases.
|
|
|
|
//
|
|
|
|
// TODO(e.burkov): Remove it when static leases determining mechanism
|
|
|
|
// will be improved.
|
|
|
|
leaseExpireStatic = 1
|
2022-09-13 23:45:35 +03:00
|
|
|
|
|
|
|
// DefaultDHCPLeaseTTL is the default time-to-live for leases.
|
|
|
|
DefaultDHCPLeaseTTL = uint32(timeutil.Day / time.Second)
|
|
|
|
|
|
|
|
// DefaultDHCPTimeoutICMP is the default timeout for waiting ICMP responses.
|
|
|
|
DefaultDHCPTimeoutICMP = 1000
|
2020-11-16 19:01:12 +03:00
|
|
|
)
|
2018-12-28 17:17:51 +03:00
|
|
|
|
2022-09-13 23:45:35 +03:00
|
|
|
// Currently used defaults for ifaceDNSAddrs.
|
|
|
|
const (
|
|
|
|
defaultMaxAttempts int = 10
|
|
|
|
defaultBackoff time.Duration = 500 * time.Millisecond
|
|
|
|
)
|
2020-02-18 19:27:09 +03:00
|
|
|
|
2019-01-24 20:11:01 +03:00
|
|
|
// Lease contains the necessary information about a DHCP lease
|
2018-12-28 17:17:51 +03:00
|
|
|
type Lease struct {
|
2021-05-06 13:02:48 +03:00
|
|
|
// Expiry is the expiration time of the lease. The unix timestamp value
|
|
|
|
// of 1 means that this is a static lease.
|
|
|
|
Expiry time.Time `json:"expires"`
|
|
|
|
|
|
|
|
Hostname string `json:"hostname"`
|
2020-07-03 18:20:01 +03:00
|
|
|
HWAddr net.HardwareAddr `json:"mac"`
|
2023-03-01 13:27:24 +03:00
|
|
|
|
|
|
|
// IP is the IP address leased to the client.
|
|
|
|
//
|
|
|
|
// TODO(a.garipov): Migrate leases.db and use netip.Addr.
|
|
|
|
IP net.IP `json:"ip"`
|
2018-12-28 17:17:51 +03:00
|
|
|
}
|
|
|
|
|
2021-06-16 16:48:46 +03:00
|
|
|
// 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,
|
2022-10-21 20:42:00 +03:00
|
|
|
HWAddr: slices.Clone(l.HWAddr),
|
|
|
|
IP: slices.Clone(l.IP),
|
2021-06-16 16:48:46 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2021-03-18 17:07:13 +03:00
|
|
|
// IsStatic returns true if the lease is static.
|
|
|
|
//
|
|
|
|
// TODO(a.garipov): Just make it a boolean field.
|
|
|
|
func (l *Lease) IsStatic() (ok bool) {
|
|
|
|
return l != nil && l.Expiry.Unix() == leaseExpireStatic
|
|
|
|
}
|
|
|
|
|
2021-05-14 19:41:45 +03:00
|
|
|
// MarshalJSON implements the json.Marshaler interface for Lease.
|
|
|
|
func (l Lease) MarshalJSON() ([]byte, error) {
|
2021-02-18 14:11:54 +03:00
|
|
|
var expiryStr string
|
2021-03-18 17:07:13 +03:00
|
|
|
if !l.IsStatic() {
|
2021-02-18 14:11:54 +03:00
|
|
|
// 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.
|
2021-03-18 17:07:13 +03:00
|
|
|
expiryStr = l.Expiry.Format(time.RFC3339)
|
2021-02-18 14:11:54 +03:00
|
|
|
}
|
|
|
|
|
2021-01-20 15:59:24 +03:00
|
|
|
type lease Lease
|
|
|
|
return json.Marshal(&struct {
|
|
|
|
HWAddr string `json:"mac"`
|
2021-02-18 14:11:54 +03:00
|
|
|
Expiry string `json:"expires,omitempty"`
|
2021-05-14 19:41:45 +03:00
|
|
|
lease
|
2021-01-20 15:59:24 +03:00
|
|
|
}{
|
|
|
|
HWAddr: l.HWAddr.String(),
|
2021-02-18 14:11:54 +03:00
|
|
|
Expiry: expiryStr,
|
2021-05-14 19:41:45 +03:00
|
|
|
lease: lease(l),
|
2021-01-20 15:59:24 +03:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// UnmarshalJSON implements the json.Unmarshaler interface for *Lease.
|
|
|
|
func (l *Lease) UnmarshalJSON(data []byte) (err error) {
|
|
|
|
type lease Lease
|
|
|
|
aux := struct {
|
|
|
|
*lease
|
2021-05-14 19:41:45 +03:00
|
|
|
HWAddr string `json:"mac"`
|
2021-01-20 15:59:24 +03:00
|
|
|
}{
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-12-07 15:38:05 +03:00
|
|
|
// OnLeaseChangedT is a callback for lease changes.
|
2020-07-03 18:20:01 +03:00
|
|
|
type OnLeaseChangedT func(flags int)
|
2020-01-30 10:25:02 +03:00
|
|
|
|
|
|
|
// flags for onLeaseChanged()
|
|
|
|
const (
|
|
|
|
LeaseChangedAdded = iota
|
|
|
|
LeaseChangedAddedStatic
|
|
|
|
LeaseChangedRemovedStatic
|
2021-04-15 17:52:53 +03:00
|
|
|
LeaseChangedRemovedAll
|
2020-07-03 18:20:01 +03:00
|
|
|
|
|
|
|
LeaseChangedDBStore
|
2020-01-30 10:25:02 +03:00
|
|
|
)
|
|
|
|
|
2021-06-16 16:48:46 +03:00
|
|
|
// GetLeasesFlags are the flags for GetLeases.
|
|
|
|
type GetLeasesFlags uint8
|
|
|
|
|
|
|
|
// GetLeasesFlags values
|
|
|
|
const (
|
2022-09-13 23:45:35 +03:00
|
|
|
LeasesDynamic GetLeasesFlags = 0b01
|
|
|
|
LeasesStatic GetLeasesFlags = 0b10
|
2021-06-16 16:48:46 +03:00
|
|
|
|
|
|
|
LeasesAll = LeasesDynamic | LeasesStatic
|
|
|
|
)
|
|
|
|
|
2022-09-13 23:45:35 +03:00
|
|
|
// Interface is the DHCP server that deals with both IP address families.
|
|
|
|
type Interface interface {
|
|
|
|
Start() (err error)
|
|
|
|
Stop() (err error)
|
2021-04-29 16:00:07 +03:00
|
|
|
Enabled() (ok bool)
|
2022-09-13 23:45:35 +03:00
|
|
|
|
2021-06-16 16:48:46 +03:00
|
|
|
Leases(flags GetLeasesFlags) (leases []*Lease)
|
2020-07-03 18:20:01 +03:00
|
|
|
SetOnLeaseChanged(onLeaseChanged OnLeaseChangedT)
|
2023-03-01 13:27:24 +03:00
|
|
|
FindMACbyIP(ip netip.Addr) (mac net.HardwareAddr)
|
2022-09-13 23:45:35 +03:00
|
|
|
|
|
|
|
WriteDiskConfig(c *ServerConfig)
|
|
|
|
}
|
|
|
|
|
|
|
|
// MockInterface is a mock Interface implementation.
|
|
|
|
//
|
|
|
|
// TODO(e.burkov): Move to aghtest when the API stabilized.
|
|
|
|
type MockInterface struct {
|
|
|
|
OnStart func() (err error)
|
|
|
|
OnStop func() (err error)
|
|
|
|
OnEnabled func() (ok bool)
|
|
|
|
OnLeases func(flags GetLeasesFlags) (leases []*Lease)
|
|
|
|
OnSetOnLeaseChanged func(f OnLeaseChangedT)
|
2023-03-01 13:27:24 +03:00
|
|
|
OnFindMACbyIP func(ip netip.Addr) (mac net.HardwareAddr)
|
2022-09-13 23:45:35 +03:00
|
|
|
OnWriteDiskConfig func(c *ServerConfig)
|
|
|
|
}
|
|
|
|
|
|
|
|
var _ Interface = (*MockInterface)(nil)
|
|
|
|
|
|
|
|
// Start implements the Interface for *MockInterface.
|
|
|
|
func (s *MockInterface) Start() (err error) { return s.OnStart() }
|
|
|
|
|
|
|
|
// Stop implements the Interface for *MockInterface.
|
|
|
|
func (s *MockInterface) Stop() (err error) { return s.OnStop() }
|
|
|
|
|
|
|
|
// Enabled implements the Interface for *MockInterface.
|
|
|
|
func (s *MockInterface) Enabled() (ok bool) { return s.OnEnabled() }
|
|
|
|
|
|
|
|
// Leases implements the Interface for *MockInterface.
|
|
|
|
func (s *MockInterface) Leases(flags GetLeasesFlags) (ls []*Lease) { return s.OnLeases(flags) }
|
|
|
|
|
|
|
|
// SetOnLeaseChanged implements the Interface for *MockInterface.
|
|
|
|
func (s *MockInterface) SetOnLeaseChanged(f OnLeaseChangedT) { s.OnSetOnLeaseChanged(f) }
|
|
|
|
|
2023-03-01 13:27:24 +03:00
|
|
|
// FindMACbyIP implements the [Interface] for *MockInterface.
|
|
|
|
func (s *MockInterface) FindMACbyIP(ip netip.Addr) (mac net.HardwareAddr) {
|
|
|
|
return s.OnFindMACbyIP(ip)
|
|
|
|
}
|
2022-09-13 23:45:35 +03:00
|
|
|
|
|
|
|
// WriteDiskConfig implements the Interface for *MockInterface.
|
|
|
|
func (s *MockInterface) WriteDiskConfig(c *ServerConfig) { s.OnWriteDiskConfig(c) }
|
|
|
|
|
|
|
|
// server is the DHCP service that handles DHCPv4, DHCPv6, and HTTP API.
|
|
|
|
type server struct {
|
|
|
|
srv4 DHCPServer
|
|
|
|
srv6 DHCPServer
|
|
|
|
|
|
|
|
// TODO(a.garipov): Either create a separate type for the internal config or
|
|
|
|
// just put the config values into Server.
|
|
|
|
conf *ServerConfig
|
|
|
|
|
|
|
|
// Called when the leases DB is modified
|
|
|
|
onLeaseChanged []OnLeaseChangedT
|
2019-03-06 15:13:27 +03:00
|
|
|
}
|
|
|
|
|
2022-09-13 23:45:35 +03:00
|
|
|
// type check
|
|
|
|
var _ Interface = (*server)(nil)
|
|
|
|
|
|
|
|
// Create initializes and returns the DHCP server handling both address
|
|
|
|
// families. It also registers the corresponding HTTP API endpoints.
|
|
|
|
func Create(conf *ServerConfig) (s *server, err error) {
|
|
|
|
s = &server{
|
2022-01-25 19:47:02 +03:00
|
|
|
conf: &ServerConfig{
|
|
|
|
ConfigModified: conf.ConfigModified,
|
2020-11-16 19:01:12 +03:00
|
|
|
|
2022-01-25 19:47:02 +03:00
|
|
|
HTTPRegister: conf.HTTPRegister,
|
|
|
|
|
|
|
|
Enabled: conf.Enabled,
|
|
|
|
InterfaceName: conf.InterfaceName,
|
|
|
|
|
|
|
|
LocalDomainName: conf.LocalDomainName,
|
|
|
|
|
|
|
|
DBFilePath: filepath.Join(conf.WorkDir, dbFilename),
|
|
|
|
},
|
|
|
|
}
|
2019-11-06 13:04:01 +03:00
|
|
|
|
2022-11-25 18:21:25 +03:00
|
|
|
// TODO(e.burkov): Don't register handlers, see TODO on
|
|
|
|
// [aghhttp.RegisterFunc].
|
2022-09-13 23:45:35 +03:00
|
|
|
s.registerHandlers()
|
2020-04-07 11:48:03 +03:00
|
|
|
|
2021-02-11 20:49:03 +03:00
|
|
|
v4conf := conf.Conf4
|
2020-07-03 18:20:01 +03:00
|
|
|
v4conf.InterfaceName = s.conf.InterfaceName
|
|
|
|
v4conf.notify = s.onNotify
|
2022-10-14 15:29:44 +03:00
|
|
|
v4conf.Enabled = s.conf.Enabled && v4conf.RangeStart.IsValid()
|
2022-09-13 23:45:35 +03:00
|
|
|
|
|
|
|
s.srv4, err = v4Create(&v4conf)
|
2021-06-16 16:48:46 +03:00
|
|
|
if err != nil {
|
2022-09-13 23:45:35 +03:00
|
|
|
if v4conf.Enabled {
|
|
|
|
return nil, fmt.Errorf("creating dhcpv4 srv: %w", err)
|
|
|
|
}
|
|
|
|
|
2022-11-25 18:21:25 +03:00
|
|
|
log.Debug("dhcpd: warning: creating dhcpv4 srv: %s", err)
|
2021-06-16 16:48:46 +03:00
|
|
|
}
|
2020-07-03 18:20:01 +03:00
|
|
|
|
2021-02-11 20:49:03 +03:00
|
|
|
v6conf := conf.Conf6
|
2020-07-03 18:20:01 +03:00
|
|
|
v6conf.Enabled = s.conf.Enabled
|
|
|
|
if len(v6conf.RangeStart) == 0 {
|
|
|
|
v6conf.Enabled = false
|
|
|
|
}
|
|
|
|
v6conf.InterfaceName = s.conf.InterfaceName
|
|
|
|
v6conf.notify = s.onNotify
|
2021-06-16 16:48:46 +03:00
|
|
|
s.srv6, err = v6Create(v6conf)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("creating dhcpv6 srv: %w", err)
|
2020-07-03 18:20:01 +03:00
|
|
|
}
|
|
|
|
|
2021-02-11 20:49:03 +03:00
|
|
|
s.conf.Conf4 = conf.Conf4
|
|
|
|
s.conf.Conf6 = conf.Conf6
|
|
|
|
|
2020-08-04 14:18:35 +03:00
|
|
|
if s.conf.Enabled && !v4conf.Enabled && !v6conf.Enabled {
|
2021-06-16 16:48:46 +03:00
|
|
|
return nil, fmt.Errorf("neither dhcpv4 nor dhcpv6 srv is configured")
|
2020-08-04 14:18:35 +03:00
|
|
|
}
|
|
|
|
|
2021-06-16 16:48:46 +03:00
|
|
|
// Don't delay database loading until the DHCP server is started,
|
|
|
|
// because we need static leases functionality available beforehand.
|
|
|
|
err = s.dbLoad()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("loading db: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return s, nil
|
2019-11-22 14:21:08 +03:00
|
|
|
}
|
|
|
|
|
2021-04-29 16:00:07 +03:00
|
|
|
// Enabled returns true when the server is enabled.
|
2022-09-13 23:45:35 +03:00
|
|
|
func (s *server) Enabled() (ok bool) {
|
2021-04-29 16:00:07 +03:00
|
|
|
return s.conf.Enabled
|
|
|
|
}
|
|
|
|
|
2021-06-16 16:48:46 +03:00
|
|
|
// resetLeases resets all leases in the lease database.
|
2022-09-13 23:45:35 +03:00
|
|
|
func (s *server) resetLeases() (err error) {
|
2021-06-16 16:48:46 +03:00
|
|
|
err = s.srv4.ResetLeases(nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if s.srv6 != nil {
|
|
|
|
err = s.srv6.ResetLeases(nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return s.dbStore()
|
|
|
|
}
|
|
|
|
|
2020-07-03 18:20:01 +03:00
|
|
|
// server calls this function after DB is updated
|
2022-09-13 23:45:35 +03:00
|
|
|
func (s *server) onNotify(flags uint32) {
|
2020-07-03 18:20:01 +03:00
|
|
|
if flags == LeaseChangedDBStore {
|
2021-06-16 16:48:46 +03:00
|
|
|
err := s.dbStore()
|
|
|
|
if err != nil {
|
|
|
|
log.Error("updating db: %s", err)
|
|
|
|
}
|
2021-03-18 17:07:13 +03:00
|
|
|
|
2020-07-03 18:20:01 +03:00
|
|
|
return
|
2018-12-28 17:17:51 +03:00
|
|
|
}
|
2020-07-03 18:20:01 +03:00
|
|
|
|
|
|
|
s.notify(int(flags))
|
2019-05-14 15:49:53 +03:00
|
|
|
}
|
|
|
|
|
2020-01-30 10:25:02 +03:00
|
|
|
// SetOnLeaseChanged - set callback
|
2022-09-13 23:45:35 +03:00
|
|
|
func (s *server) SetOnLeaseChanged(onLeaseChanged OnLeaseChangedT) {
|
2020-06-23 12:13:13 +03:00
|
|
|
s.onLeaseChanged = append(s.onLeaseChanged, onLeaseChanged)
|
2020-01-30 10:25:02 +03:00
|
|
|
}
|
|
|
|
|
2022-09-13 23:45:35 +03:00
|
|
|
func (s *server) notify(flags int) {
|
2020-06-23 12:13:13 +03:00
|
|
|
for _, f := range s.onLeaseChanged {
|
|
|
|
f(flags)
|
|
|
|
}
|
2020-01-30 10:25:02 +03:00
|
|
|
}
|
|
|
|
|
2019-11-22 14:21:08 +03:00
|
|
|
// WriteDiskConfig - write configuration
|
2022-09-13 23:45:35 +03:00
|
|
|
func (s *server) WriteDiskConfig(c *ServerConfig) {
|
2020-07-03 18:20:01 +03:00
|
|
|
c.Enabled = s.conf.Enabled
|
|
|
|
c.InterfaceName = s.conf.InterfaceName
|
2022-01-25 19:47:02 +03:00
|
|
|
c.LocalDomainName = s.conf.LocalDomainName
|
2022-09-13 23:45:35 +03:00
|
|
|
|
2020-07-03 18:20:01 +03:00
|
|
|
s.srv4.WriteDiskConfig4(&c.Conf4)
|
|
|
|
s.srv6.WriteDiskConfig6(&c.Conf6)
|
2019-05-14 15:49:53 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Start will listen on port 67 and serve DHCP requests.
|
2022-09-13 23:45:35 +03:00
|
|
|
func (s *server) Start() (err error) {
|
2021-04-21 14:42:19 +03:00
|
|
|
err = s.srv4.Start()
|
2019-05-14 15:49:53 +03:00
|
|
|
if err != nil {
|
2020-07-03 18:20:01 +03:00
|
|
|
return err
|
2019-05-14 15:49:53 +03:00
|
|
|
}
|
2019-03-05 17:15:38 +03:00
|
|
|
|
2020-07-03 18:20:01 +03:00
|
|
|
err = s.srv6.Start()
|
2018-12-28 17:17:51 +03:00
|
|
|
if err != nil {
|
2020-07-03 18:20:01 +03:00
|
|
|
return err
|
2018-12-28 17:17:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-01-24 20:11:01 +03:00
|
|
|
// Stop closes the listening UDP socket
|
2022-09-13 23:45:35 +03:00
|
|
|
func (s *server) Stop() (err error) {
|
2021-06-16 16:48:46 +03:00
|
|
|
err = s.srv4.Stop()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-05-14 13:02:04 +03:00
|
|
|
|
2021-06-16 16:48:46 +03:00
|
|
|
err = s.srv6.Stop()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2020-01-30 10:25:02 +03:00
|
|
|
|
2021-02-18 14:11:54 +03:00
|
|
|
// Leases returns the list of active IPv4 and IPv6 DHCP leases. It's safe for
|
|
|
|
// concurrent use.
|
2022-09-13 23:45:35 +03:00
|
|
|
func (s *server) Leases(flags GetLeasesFlags) (leases []*Lease) {
|
2021-02-18 14:11:54 +03:00
|
|
|
return append(s.srv4.GetLeases(flags), s.srv6.GetLeases(flags)...)
|
2019-05-28 14:11:47 +03:00
|
|
|
}
|
|
|
|
|
2023-03-01 13:27:24 +03:00
|
|
|
// FindMACbyIP returns a MAC address by the IP address of its lease, if there is
|
|
|
|
// one.
|
|
|
|
func (s *server) FindMACbyIP(ip netip.Addr) (mac net.HardwareAddr) {
|
|
|
|
if ip.Is4() {
|
2020-07-03 18:20:01 +03:00
|
|
|
return s.srv4.FindMACbyIP(ip)
|
2019-12-23 16:59:02 +03:00
|
|
|
}
|
2023-03-01 13:27:24 +03:00
|
|
|
|
2020-07-03 18:20:01 +03:00
|
|
|
return s.srv6.FindMACbyIP(ip)
|
2019-09-26 16:40:52 +03:00
|
|
|
}
|
|
|
|
|
2020-07-03 18:20:01 +03:00
|
|
|
// AddStaticLease - add static v4 lease
|
2022-09-13 23:45:35 +03:00
|
|
|
func (s *server) AddStaticLease(l *Lease) error {
|
2021-06-16 16:48:46 +03:00
|
|
|
return s.srv4.AddStaticLease(l)
|
2019-03-01 20:03:22 +03:00
|
|
|
}
|