Files
AdGuardHome/internal/dhcpd/dhcpd.go
Stanislav Chzhen 86de4e75f0 Pull request 2442: AGDNS-3061-config-modifier
Merge in DNS/adguard-home from AGDNS-3061-config-modifier to master

Squashed commit of the following:

commit a0068547bd0209d12e8dbf98ddd5e4ed2545cdd0
Merge: 97b798af6 451255675
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Aug 5 17:39:31 2025 +0300

    Merge branch 'master' into AGDNS-3061-config-modifier

commit 97b798af6a50ee27ee5ed2bcf1c4c3670f5afc5d
Merge: 96d21efc9 b8043e4f0
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Jul 30 20:27:41 2025 +0300

    Merge branch 'master' into AGDNS-3061-config-modifier

commit 96d21efc984073adef5026de8a03b5bf94542648
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Jul 30 20:18:43 2025 +0300

    all: imp code

commit 67c5608b4be3bd712a0ab5980f25ecea1ed21d65
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Jul 29 20:31:19 2025 +0300

    all: imp code

commit 52f45a9f70f57d8e7f7fc0e9e8291ff0dde74343
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Jul 28 20:32:13 2025 +0300

    all: use config modifier

commit d389ffd286460d8ff1964bd2ee8dabdafb832b9b
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Jul 25 14:20:31 2025 +0300

    bamboo-specs: fix ci

commit 3f303ac9131a07e4af5783696b237436fd93d65c
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Jul 25 14:18:42 2025 +0300

    home: config modifier
2025-08-05 18:16:39 +03:00

307 lines
7.6 KiB
Go

// Package dhcpd provides a DHCP server.
package dhcpd
import (
"fmt"
"net"
"net/netip"
"path/filepath"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/timeutil"
)
const (
// 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
)
// Currently used defaults for ifaceDNSAddrs.
const (
defaultMaxAttempts int = 10
defaultBackoff time.Duration = 500 * time.Millisecond
)
// OnLeaseChangedT is a callback for lease changes.
type OnLeaseChangedT func(flags int)
// flags for onLeaseChanged()
const (
LeaseChangedAdded = iota
LeaseChangedAddedStatic
LeaseChangedRemovedStatic
LeaseChangedRemovedAll
LeaseChangedDBStore
)
// GetLeasesFlags are the flags for GetLeases.
type GetLeasesFlags uint8
// GetLeasesFlags values
const (
LeasesDynamic GetLeasesFlags = 0b01
LeasesStatic GetLeasesFlags = 0b10
LeasesAll = LeasesDynamic | LeasesStatic
)
// Interface is the DHCP server that deals with both IP address families.
type Interface interface {
Start() (err error)
Stop() (err error)
// Enabled returns true if the DHCP server is running.
//
// TODO(e.burkov): Currently, we need this method to determine whether the
// local domain suffix should be considered while resolving A/AAAA requests.
// This is because other parts of the code aren't aware of the DNS suffixes
// in DHCP clients names and caller is responsible for trimming it. This
// behavior should be changed in the future.
Enabled() (ok bool)
// Leases returns all the leases in the database.
Leases() (leases []*dhcpsvc.Lease)
// MacByIP returns the MAC address of a client with ip. It returns nil if
// there is no such client, due to an assumption that a DHCP client must
// always have a HardwareAddr.
MACByIP(ip netip.Addr) (mac net.HardwareAddr)
// HostByIP returns the hostname of the DHCP client with the given IP
// address. The address will be netip.Addr{} if there is no such client,
// due to an assumption that a DHCP client must always have an IP address.
HostByIP(ip netip.Addr) (host string)
// IPByHost returns the IP address of the DHCP client with the given
// hostname. The address will be netip.Addr{} if there is no such client,
// due to an assumption that a DHCP client must always have an IP address.
IPByHost(host string) (ip netip.Addr)
WriteDiskConfig(c *ServerConfig)
}
// 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
}
// 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{
conf: &ServerConfig{
ConfModifier: conf.ConfModifier,
HTTPRegister: conf.HTTPRegister,
Enabled: conf.Enabled,
InterfaceName: conf.InterfaceName,
LocalDomainName: conf.LocalDomainName,
dbFilePath: filepath.Join(conf.DataDir, dataFilename),
},
}
// TODO(e.burkov): Don't register handlers, see TODO on
// [aghhttp.RegisterFunc].
s.registerHandlers()
v4Enabled, v6Enabled, err := s.setServers(conf)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
s.conf.Conf4 = conf.Conf4
s.conf.Conf6 = conf.Conf6
if s.conf.Enabled && !v4Enabled && !v6Enabled {
return nil, fmt.Errorf("neither dhcpv4 nor dhcpv6 srv is configured")
}
// Migrate leases db if needed.
err = migrateDB(conf)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return nil, err
}
// 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
}
// setServers updates DHCPv4 and DHCPv6 servers created from the provided
// configuration conf. It returns the status of both the DHCPv4 and the DHCPv6
// servers, which is always false for corresponding server on any error.
func (s *server) setServers(conf *ServerConfig) (v4Enabled, v6Enabled bool, err error) {
v4conf := conf.Conf4
v4conf.InterfaceName = s.conf.InterfaceName
v4conf.notify = s.onNotify
v4conf.Enabled = s.conf.Enabled && v4conf.RangeStart.IsValid()
s.srv4, err = v4Create(&v4conf)
if err != nil {
if v4conf.Enabled {
return false, false, fmt.Errorf("creating dhcpv4 srv: %w", err)
}
log.Debug("dhcpd: warning: creating dhcpv4 srv: %s", err)
}
v6conf := conf.Conf6
v6conf.InterfaceName = s.conf.InterfaceName
v6conf.notify = s.onNotify
v6conf.Enabled = s.conf.Enabled && len(v6conf.RangeStart) != 0
s.srv6, err = v6Create(v6conf)
if err != nil {
return v4conf.Enabled, false, fmt.Errorf("creating dhcpv6 srv: %w", err)
}
return v4conf.Enabled, v6conf.Enabled, nil
}
// Enabled returns true when the server is enabled.
func (s *server) Enabled() (ok bool) {
return s.conf.Enabled
}
// resetLeases resets all leases in the lease database.
func (s *server) resetLeases() (err error) {
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()
}
// server calls this function after DB is updated
func (s *server) onNotify(flags uint32) {
if flags == LeaseChangedDBStore {
err := s.dbStore()
if err != nil {
log.Error("updating db: %s", err)
}
return
}
s.notify(int(flags))
}
func (s *server) notify(flags int) {
for _, f := range s.onLeaseChanged {
f(flags)
}
}
// WriteDiskConfig - write configuration
func (s *server) WriteDiskConfig(c *ServerConfig) {
c.Enabled = s.conf.Enabled
c.InterfaceName = s.conf.InterfaceName
c.LocalDomainName = s.conf.LocalDomainName
s.srv4.WriteDiskConfig4(&c.Conf4)
s.srv6.WriteDiskConfig6(&c.Conf6)
}
// Start will listen on port 67 and serve DHCP requests.
func (s *server) Start() (err error) {
err = s.srv4.Start()
if err != nil {
return err
}
err = s.srv6.Start()
if err != nil {
return err
}
return nil
}
// Stop closes the listening UDP socket
func (s *server) Stop() (err error) {
err = s.srv4.Stop()
if err != nil {
return err
}
err = s.srv6.Stop()
if err != nil {
return err
}
return nil
}
// Leases returns the list of active DHCP leases.
func (s *server) Leases() (leases []*dhcpsvc.Lease) {
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
// one.
func (s *server) MACByIP(ip netip.Addr) (mac net.HardwareAddr) {
if ip.Is4() {
return s.srv4.FindMACbyIP(ip)
}
return s.srv6.FindMACbyIP(ip)
}
// HostByIP implements the [Interface] interface for *server.
//
// TODO(e.burkov): Implement this method for DHCPv6.
func (s *server) HostByIP(ip netip.Addr) (host string) {
if ip.Is4() {
return s.srv4.HostByIP(ip)
}
return ""
}
// IPByHost implements the [Interface] interface for *server.
//
// TODO(e.burkov): Implement this method for DHCPv6.
func (s *server) IPByHost(host string) (ip netip.Addr) {
return s.srv4.IPByHost(host)
}
// AddStaticLease - add static v4 lease
func (s *server) AddStaticLease(l *dhcpsvc.Lease) error {
return s.srv4.AddStaticLease(l)
}