Pull request 1978: 4923 gopacket dhcp vol.2

Merge in DNS/adguard-home from 4923-gopacket-dhcp-vol.2 to master

Updates #4923.

Squashed commit of the following:

commit d0ef7d44af9790ed55401f6f65c7149f4c3658f7
Merge: f92b4c72d a4fdc3e8e
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Aug 30 13:43:41 2023 +0300

    Merge branch 'master' into 4923-gopacket-dhcp-vol.2

commit f92b4c72de03ceacb9b8890b7cf4307688795ce5
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Aug 28 12:33:29 2023 +0300

    dhcpd: imp code

commit 63f0fce99a0343af2670943770cfef4694ae93ed
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Aug 24 15:01:34 2023 +0300

    all: imp dhcpd code

commit 563b43b4b5ab6c9c9046c7f09008ea3ef344f4e9
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Jul 28 17:03:55 2023 +0300

    dhcpd: imp indexing

commit 340d3efa90ac4d34ba3d18702692de0fbc0247be
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Jul 28 16:22:43 2023 +0300

    all: adapt current code
This commit is contained in:
Eugene Burkov 2023-08-30 14:02:12 +03:00
parent a4fdc3e8ed
commit a325c9b6bb
18 changed files with 502 additions and 394 deletions

View File

@ -23,9 +23,12 @@ type ServerConfig struct {
Enabled bool `yaml:"enabled"` Enabled bool `yaml:"enabled"`
InterfaceName string `yaml:"interface_name"` InterfaceName string `yaml:"interface_name"`
// LocalDomainName is the domain name used for DHCP hosts. For example, // LocalDomainName is the domain name used for DHCP hosts. For example, a
// a DHCP client with the hostname "myhost" can be addressed as "myhost.lan" // DHCP client with the hostname "myhost" can be addressed as "myhost.lan"
// when LocalDomainName is "lan". // when LocalDomainName is "lan".
//
// TODO(e.burkov): Probably, remove this field. See the TODO on
// [Interface.Enabled].
LocalDomainName string `yaml:"local_domain_name"` LocalDomainName string `yaml:"local_domain_name"`
Conf4 V4ServerConf `yaml:"dhcpv4"` Conf4 V4ServerConf `yaml:"dhcpv4"`
@ -58,6 +61,14 @@ type DHCPServer interface {
// there is one. // there is one.
FindMACbyIP(ip netip.Addr) (mac net.HardwareAddr) FindMACbyIP(ip netip.Addr) (mac net.HardwareAddr)
// HostByIP returns a hostname by the IP address of its lease, if there is
// one.
HostByIP(ip netip.Addr) (host string)
// IPByHost returns an IP address by the hostname of its lease, if there is
// one.
IPByHost(host string) (ip netip.Addr)
// WriteDiskConfig4 - copy disk configuration // WriteDiskConfig4 - copy disk configuration
WriteDiskConfig4(c *V4ServerConf) WriteDiskConfig4(c *V4ServerConf)
// WriteDiskConfig6 - copy disk configuration // WriteDiskConfig6 - copy disk configuration

View File

@ -9,6 +9,7 @@ import (
"path/filepath" "path/filepath"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/timeutil" "github.com/AdguardTeam/golibs/timeutil"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
@ -31,6 +32,8 @@ const (
// Lease contains the necessary information about a DHCP lease. It's used as is // 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 // in the database, so don't change it until it's absolutely necessary, see
// [dataVersion]. // [dataVersion].
//
// TODO(e.burkov): Unexport it and use [dhcpsvc.Lease].
type Lease struct { type Lease struct {
// Expiry is the expiration time of the lease. // Expiry is the expiration time of the lease.
Expiry time.Time `json:"expires"` Expiry time.Time `json:"expires"`
@ -153,53 +156,37 @@ const (
type Interface interface { type Interface interface {
Start() (err error) Start() (err error)
Stop() (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) Enabled() (ok bool)
Leases(flags GetLeasesFlags) (leases []*Lease) // Leases returns all the leases in the database.
SetOnLeaseChanged(onLeaseChanged OnLeaseChangedT) Leases() (leases []*dhcpsvc.Lease)
FindMACbyIP(ip netip.Addr) (mac net.HardwareAddr)
// 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) 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)
OnFindMACbyIP func(ip netip.Addr) (mac net.HardwareAddr)
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) }
// FindMACbyIP implements the [Interface] for *MockInterface.
func (s *MockInterface) FindMACbyIP(ip netip.Addr) (mac net.HardwareAddr) {
return s.OnFindMACbyIP(ip)
}
// 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. // server is the DHCP service that handles DHCPv4, DHCPv6, and HTTP API.
type server struct { type server struct {
srv4 DHCPServer srv4 DHCPServer
@ -269,7 +256,8 @@ func Create(conf *ServerConfig) (s *server, err error) {
} }
// setServers updates DHCPv4 and DHCPv6 servers created from the provided // setServers updates DHCPv4 and DHCPv6 servers created from the provided
// configuration conf. // 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) { func (s *server) setServers(conf *ServerConfig) (v4Enabled, v6Enabled bool, err error) {
v4conf := conf.Conf4 v4conf := conf.Conf4
v4conf.InterfaceName = s.conf.InterfaceName v4conf.InterfaceName = s.conf.InterfaceName
@ -279,7 +267,7 @@ func (s *server) setServers(conf *ServerConfig) (v4Enabled, v6Enabled bool, err
s.srv4, err = v4Create(&v4conf) s.srv4, err = v4Create(&v4conf)
if err != nil { if err != nil {
if v4conf.Enabled { if v4conf.Enabled {
return true, false, fmt.Errorf("creating dhcpv4 srv: %w", err) return false, false, fmt.Errorf("creating dhcpv4 srv: %w", err)
} }
log.Debug("dhcpd: warning: creating dhcpv4 srv: %s", err) log.Debug("dhcpd: warning: creating dhcpv4 srv: %s", err)
@ -288,14 +276,11 @@ func (s *server) setServers(conf *ServerConfig) (v4Enabled, v6Enabled bool, err
v6conf := conf.Conf6 v6conf := conf.Conf6
v6conf.InterfaceName = s.conf.InterfaceName v6conf.InterfaceName = s.conf.InterfaceName
v6conf.notify = s.onNotify v6conf.notify = s.onNotify
v6conf.Enabled = s.conf.Enabled v6conf.Enabled = s.conf.Enabled && len(v6conf.RangeStart) != 0
if len(v6conf.RangeStart) == 0 {
v6conf.Enabled = false
}
s.srv6, err = v6Create(v6conf) s.srv6, err = v6Create(v6conf)
if err != nil { if err != nil {
return v4conf.Enabled, v6conf.Enabled, fmt.Errorf("creating dhcpv6 srv: %w", err) return v4conf.Enabled, false, fmt.Errorf("creating dhcpv6 srv: %w", err)
} }
return v4conf.Enabled, v6conf.Enabled, nil return v4conf.Enabled, v6conf.Enabled, nil
@ -337,11 +322,6 @@ func (s *server) onNotify(flags uint32) {
s.notify(int(flags)) s.notify(int(flags))
} }
// SetOnLeaseChanged - set callback
func (s *server) SetOnLeaseChanged(onLeaseChanged OnLeaseChangedT) {
s.onLeaseChanged = append(s.onLeaseChanged, onLeaseChanged)
}
func (s *server) notify(flags int) { func (s *server) notify(flags int) {
for _, f := range s.onLeaseChanged { for _, f := range s.onLeaseChanged {
f(flags) f(flags)
@ -388,15 +368,26 @@ func (s *server) Stop() (err error) {
return nil return nil
} }
// Leases returns the list of active IPv4 and IPv6 DHCP leases. It's safe for // Leases returns the list of active DHCP leases.
// concurrent use. func (s *server) Leases() (leases []*dhcpsvc.Lease) {
func (s *server) Leases(flags GetLeasesFlags) (leases []*Lease) { ls := append(s.srv4.GetLeases(LeasesAll), s.srv6.GetLeases(LeasesAll)...)
return append(s.srv4.GetLeases(flags), s.srv6.GetLeases(flags)...) 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
} }
// FindMACbyIP returns a MAC address by the IP address of its lease, if there is // MACByIP returns a MAC address by the IP address of its lease, if there is
// one. // one.
func (s *server) FindMACbyIP(ip netip.Addr) (mac net.HardwareAddr) { func (s *server) MACByIP(ip netip.Addr) (mac net.HardwareAddr) {
if ip.Is4() { if ip.Is4() {
return s.srv4.FindMACbyIP(ip) return s.srv4.FindMACbyIP(ip)
} }
@ -404,6 +395,24 @@ func (s *server) FindMACbyIP(ip netip.Addr) (mac net.HardwareAddr) {
return s.srv6.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 // AddStaticLease - add static v4 lease
func (s *server) AddStaticLease(l *Lease) error { func (s *server) AddStaticLease(l *Lease) error {
return s.srv4.AddStaticLease(l) return s.srv4.AddStaticLease(l)

View File

@ -14,9 +14,11 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghalg" "github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet" "github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"golang.org/x/exp/slices"
) )
type v4ServerConfJSON struct { type v4ServerConfJSON struct {
@ -75,7 +77,7 @@ type leaseStatic struct {
} }
// leasesToStatic converts list of leases to their JSON form. // leasesToStatic converts list of leases to their JSON form.
func leasesToStatic(leases []*Lease) (static []*leaseStatic) { func leasesToStatic(leases []*dhcpsvc.Lease) (static []*leaseStatic) {
static = make([]*leaseStatic, len(leases)) static = make([]*leaseStatic, len(leases))
for i, l := range leases { for i, l := range leases {
@ -113,7 +115,7 @@ type leaseDynamic struct {
} }
// leasesToDynamic converts list of leases to their JSON form. // leasesToDynamic converts list of leases to their JSON form.
func leasesToDynamic(leases []*Lease) (dynamic []*leaseDynamic) { func leasesToDynamic(leases []*dhcpsvc.Lease) (dynamic []*leaseDynamic) {
dynamic = make([]*leaseDynamic, len(leases)) dynamic = make([]*leaseDynamic, len(leases))
for i, l := range leases { for i, l := range leases {
@ -143,8 +145,27 @@ func (s *server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
s.srv4.WriteDiskConfig4(&status.V4) s.srv4.WriteDiskConfig4(&status.V4)
s.srv6.WriteDiskConfig6(&status.V6) s.srv6.WriteDiskConfig6(&status.V6)
status.Leases = leasesToDynamic(s.Leases(LeasesDynamic)) leases := s.Leases()
status.StaticLeases = leasesToStatic(s.Leases(LeasesStatic)) slices.SortFunc(leases, func(a, b *dhcpsvc.Lease) (res int) {
if a.IsStatic == b.IsStatic {
return 0
} else if a.IsStatic {
return -1
} else {
return 1
}
})
dynamicIdx := slices.IndexFunc(leases, func(l *dhcpsvc.Lease) (ok bool) {
return !l.IsStatic
})
if dynamicIdx == -1 {
dynamicIdx = len(leases)
}
status.Leases = leasesToDynamic(leases[dynamicIdx:])
status.StaticLeases = leasesToStatic(leases[:dynamicIdx])
aghhttp.WriteJSONResponseOK(w, r, status) aghhttp.WriteJSONResponseOK(w, r, status)
} }

View File

@ -24,6 +24,8 @@ func (winServer) WriteDiskConfig4(_ *V4ServerConf) {}
func (winServer) WriteDiskConfig6(_ *V6ServerConf) {} func (winServer) WriteDiskConfig6(_ *V6ServerConf) {}
func (winServer) Start() (err error) { return nil } func (winServer) Start() (err error) { return nil }
func (winServer) Stop() (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 v4Create(_ *V4ServerConf) (s DHCPServer, err error) { return winServer{}, nil }
func v6Create(_ V6ServerConf) (s DHCPServer, err error) { return winServer{}, nil } func v6Create(_ V6ServerConf) (s DHCPServer, err error) { return winServer{}, nil }

View File

@ -15,7 +15,6 @@ import (
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/golibs/timeutil" "github.com/AdguardTeam/golibs/timeutil"
"github.com/go-ping/ping" "github.com/go-ping/ping"
"github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4"
@ -46,11 +45,14 @@ type v4Server struct {
// leased. // leased.
leasedOffsets *bitSet leasedOffsets *bitSet
// leaseHosts is the set of all hostnames of all known DHCP clients.
leaseHosts *stringutil.Set
// leases contains all dynamic and static leases. // leases contains all dynamic and static leases.
leases []*Lease leases []*Lease
// hostsIndex is the set of all hostnames of all known DHCP clients.
hostsIndex map[string]*Lease
// ipIndex is an index of leases by their IP addresses.
ipIndex map[netip.Addr]*Lease
} }
func (s *v4Server) enabled() (ok bool) { func (s *v4Server) enabled() (ok bool) {
@ -114,6 +116,30 @@ func (s *v4Server) validHostnameForClient(cliHostname string, ip netip.Addr) (ho
return hostname return hostname
} }
// HostByIP implements the [Interface] interface for *v4Server.
func (s *v4Server) HostByIP(ip netip.Addr) (host string) {
s.leasesLock.Lock()
defer s.leasesLock.Unlock()
if l, ok := s.ipIndex[ip]; ok {
return l.Hostname
}
return ""
}
// IPByHost implements the [Interface] interface for *v4Server.
func (s *v4Server) IPByHost(host string) (ip netip.Addr) {
s.leasesLock.Lock()
defer s.leasesLock.Unlock()
if l, ok := s.hostsIndex[host]; ok {
return l.IP
}
return netip.Addr{}
}
// ResetLeases resets leases. // ResetLeases resets leases.
func (s *v4Server) ResetLeases(leases []*Lease) (err error) { func (s *v4Server) ResetLeases(leases []*Lease) (err error) {
defer func() { err = errors.Annotate(err, "dhcpv4: %w") }() defer func() { err = errors.Annotate(err, "dhcpv4: %w") }()
@ -123,7 +149,8 @@ func (s *v4Server) ResetLeases(leases []*Lease) (err error) {
} }
s.leasedOffsets = newBitSet() s.leasedOffsets = newBitSet()
s.leaseHosts = stringutil.NewSet() s.hostsIndex = make(map[string]*Lease, len(leases))
s.ipIndex = make(map[netip.Addr]*Lease, len(leases))
s.leases = nil s.leases = nil
for _, l := range leases { for _, l := range leases {
@ -199,22 +226,20 @@ func (s *v4Server) GetLeases(flags GetLeasesFlags) (leases []*Lease) {
// FindMACbyIP implements the [Interface] for *v4Server. // FindMACbyIP implements the [Interface] for *v4Server.
func (s *v4Server) FindMACbyIP(ip netip.Addr) (mac net.HardwareAddr) { func (s *v4Server) FindMACbyIP(ip netip.Addr) (mac net.HardwareAddr) {
if !ip.Is4() {
return nil
}
now := time.Now() now := time.Now()
s.leasesLock.Lock() s.leasesLock.Lock()
defer s.leasesLock.Unlock() defer s.leasesLock.Unlock()
if !ip.Is4() { if l, ok := s.ipIndex[ip]; ok {
return nil
}
for _, l := range s.leases {
if l.IP == ip {
if l.IsStatic || l.Expiry.After(now) { if l.IsStatic || l.Expiry.After(now) {
return l.HWAddr return l.HWAddr
} }
} }
}
return nil return nil
} }
@ -249,7 +274,8 @@ func (s *v4Server) rmLeaseByIndex(i int) {
s.leasedOffsets.set(offset, false) s.leasedOffsets.set(offset, false)
} }
s.leaseHosts.Del(l.Hostname) delete(s.hostsIndex, l.Hostname)
delete(s.ipIndex, l.IP)
log.Debug("dhcpv4: removed lease %s (%s)", l.IP, l.HWAddr) log.Debug("dhcpv4: removed lease %s (%s)", l.IP, l.HWAddr)
} }
@ -303,13 +329,15 @@ func (s *v4Server) addLease(l *Lease) (err error) {
return fmt.Errorf("lease %s (%s) out of range, not adding", l.IP, l.HWAddr) return fmt.Errorf("lease %s (%s) out of range, not adding", l.IP, l.HWAddr)
} }
// TODO(e.burkov): l must have a valid hostname here, investigate.
if l.Hostname != "" { if l.Hostname != "" {
if s.leaseHosts.Has(l.Hostname) { if _, ok := s.hostsIndex[l.Hostname]; ok {
return ErrDupHostname return ErrDupHostname
} }
s.leaseHosts.Add(l.Hostname) s.hostsIndex[l.Hostname] = l
} }
s.ipIndex[l.IP] = l
s.leases = append(s.leases, l) s.leases = append(s.leases, l)
s.leasedOffsets.set(offset, true) s.leasedOffsets.set(offset, true)
@ -574,7 +602,7 @@ func (s *v4Server) commitLease(l *Lease, hostname string) {
prev := l.Hostname prev := l.Hostname
hostname = s.validHostnameForClient(hostname, l.IP) hostname = s.validHostnameForClient(hostname, l.IP)
if s.leaseHosts.Has(hostname) { if _, ok := s.hostsIndex[hostname]; ok {
log.Info("dhcpv4: hostname %q already exists", hostname) log.Info("dhcpv4: hostname %q already exists", hostname)
if prev == "" { if prev == "" {
@ -590,11 +618,12 @@ func (s *v4Server) commitLease(l *Lease, hostname string) {
l.Expiry = time.Now().Add(s.conf.leaseTime) l.Expiry = time.Now().Add(s.conf.leaseTime)
if prev != "" && prev != l.Hostname { if prev != "" && prev != l.Hostname {
s.leaseHosts.Del(prev) delete(s.hostsIndex, prev)
} }
if l.Hostname != "" { if l.Hostname != "" {
s.leaseHosts.Add(l.Hostname) s.hostsIndex[l.Hostname] = l
} }
s.ipIndex[l.IP] = l
} }
// allocateLease allocates a new lease for the MAC address. If there are no IP // allocateLease allocates a new lease for the MAC address. If there are no IP
@ -1292,7 +1321,8 @@ func (s *v4Server) Stop() (err error) {
// Create DHCPv4 server // Create DHCPv4 server
func v4Create(conf *V4ServerConf) (srv *v4Server, err error) { func v4Create(conf *V4ServerConf) (srv *v4Server, err error) {
s := &v4Server{ s := &v4Server{
leaseHosts: stringutil.NewSet(), hostsIndex: map[string]*Lease{},
ipIndex: map[netip.Addr]*Lease{},
} }
err = conf.Validate() err = conf.Validate()

View File

@ -791,6 +791,14 @@ func TestV4Server_FindMACbyIP(t *testing.T) {
IP: anotherIP, IP: anotherIP,
}}, }},
} }
s.ipIndex = map[netip.Addr]*Lease{
staticIP: s.leases[0],
anotherIP: s.leases[1],
}
s.hostsIndex = map[string]*Lease{
staticName: s.leases[0],
anotherName: s.leases[1],
}
testCases := []struct { testCases := []struct {
want net.HardwareAddr want net.HardwareAddr

View File

@ -26,15 +26,14 @@ const valueIAID = "ADGH" // value for IANA.ID
// //
// TODO(a.garipov): Think about unifying this and v4Server. // TODO(a.garipov): Think about unifying this and v4Server.
type v6Server struct { type v6Server struct {
srv *server6.Server ra raCtx
leasesLock sync.Mutex
leases []*Lease
ipAddrs [256]byte
sid dhcpv6.DUID
ra raCtx // RA module
conf V6ServerConf conf V6ServerConf
sid dhcpv6.DUID
srv *server6.Server
leases []*Lease
leasesLock sync.Mutex
ipAddrs [256]byte
} }
// WriteDiskConfig4 - write configuration // WriteDiskConfig4 - write configuration
@ -59,6 +58,34 @@ func ip6InRange(start, ip net.IP) bool {
return start[15] <= ip[15] return start[15] <= ip[15]
} }
// HostByIP implements the [Interface] interface for *v6Server.
func (s *v6Server) HostByIP(ip netip.Addr) (host string) {
s.leasesLock.Lock()
defer s.leasesLock.Unlock()
for _, l := range s.leases {
if l.IP == ip {
return l.Hostname
}
}
return ""
}
// IPByHost implements the [Interface] interface for *v6Server.
func (s *v6Server) IPByHost(host string) (ip netip.Addr) {
s.leasesLock.Lock()
defer s.leasesLock.Unlock()
for _, l := range s.leases {
if l.Hostname == host {
return l.IP
}
}
return netip.Addr{}
}
// ResetLeases resets leases. // ResetLeases resets leases.
func (s *v6Server) ResetLeases(leases []*Lease) (err error) { func (s *v6Server) ResetLeases(leases []*Lease) (err error) {
defer func() { err = errors.Annotate(err, "dhcpv6: %w") }() defer func() { err = errors.Annotate(err, "dhcpv6: %w") }()

View File

@ -53,7 +53,7 @@ type Interface interface {
// IPByHost returns the IP address of the DHCP client with the given // IPByHost returns the IP address of the DHCP client with the given
// hostname. The hostname will be an empty string if there is no such // hostname. The hostname will be an empty string if there is no such
// client, due to an assumption that a DHCP client must always have a // client, due to an assumption that a DHCP client must always have a
// hostname, either set by the client or assigned automatically. // hostname, either set or generated.
IPByHost(host string) (ip netip.Addr) IPByHost(host string) (ip netip.Addr)
// Leases returns all the DHCP leases. // Leases returns all the DHCP leases.
@ -104,6 +104,9 @@ func (Empty) MACByIP(_ netip.Addr) (mac net.HardwareAddr) { return nil }
// IPByHost implements the [Interface] interface for Empty. // IPByHost implements the [Interface] interface for Empty.
func (Empty) IPByHost(_ string) (ip netip.Addr) { return netip.Addr{} } func (Empty) IPByHost(_ string) (ip netip.Addr) { return netip.Addr{} }
// type check
var _ Interface = Empty{}
// Leases implements the [Interface] interface for Empty. // Leases implements the [Interface] interface for Empty.
func (Empty) Leases() (leases []*Lease) { return nil } func (Empty) Leases() (leases []*Lease) { return nil }

View File

@ -15,7 +15,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghalg" "github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet" "github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/client" "github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/querylog" "github.com/AdguardTeam/AdGuardHome/internal/querylog"
"github.com/AdguardTeam/AdGuardHome/internal/rdns" "github.com/AdguardTeam/AdGuardHome/internal/rdns"
@ -48,19 +47,7 @@ var defaultBlockedHosts = []string{"version.bind", "id.server", "hostname.bind"}
var webRegistered bool var webRegistered bool
// hostToIPTable is a convenient type alias for tables of host names to an IP // DHCP is an interface for accesing DHCP lease data needed in this package.
// address.
//
// TODO(e.burkov): Use the [DHCP] interface instead.
type hostToIPTable = map[string]netip.Addr
// ipToHostTable is a convenient type alias for tables of IP addresses to their
// host names. For example, for use with PTR queries.
//
// TODO(e.burkov): Use the [DHCP] interface instead.
type ipToHostTable = map[netip.Addr]string
// DHCP is an interface for accessing DHCP lease data needed in this package.
type DHCP interface { type DHCP interface {
// HostByIP returns the hostname of the DHCP client with the given IP // 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, // address. The address will be netip.Addr{} if there is no such client,
@ -89,18 +76,34 @@ type DHCP interface {
// //
// The zero Server is empty and ready for use. // The zero Server is empty and ready for use.
type Server struct { type Server struct {
dnsProxy *proxy.Proxy // DNS proxy instance // dnsProxy is the DNS proxy for forwarding client's DNS requests.
dnsFilter *filtering.DNSFilter // DNS filter instance dnsProxy *proxy.Proxy
dhcpServer dhcpd.Interface // DHCP server instance (optional)
queryLog querylog.QueryLog // Query log instance // dnsFilter is the DNS filter for filtering client's DNS requests and
// responses.
dnsFilter *filtering.DNSFilter
// dhcpServer is the DHCP server for accessing lease data.
dhcpServer DHCP
// queryLog is the query log for client's DNS requests, responses and
// filtering results.
queryLog querylog.QueryLog
// stats is the statistics collector for client's DNS usage data.
stats stats.Interface stats stats.Interface
// access drops unallowed clients.
access *accessManager access *accessManager
// localDomainSuffix is the suffix used to detect internal hosts. It // localDomainSuffix is the suffix used to detect internal hosts. It
// must be a valid domain name plus dots on each side. // must be a valid domain name plus dots on each side.
localDomainSuffix string localDomainSuffix string
// ipset processes DNS requests using ipset data.
ipset ipsetCtx ipset ipsetCtx
// privateNets is the configured set of IP networks considered private.
privateNets netutil.SubnetSet privateNets netutil.SubnetSet
// addrProc, if not nil, is used to process clients' IP addresses with rDNS, // addrProc, if not nil, is used to process clients' IP addresses with rDNS,
@ -112,6 +115,9 @@ type Server struct {
// //
// TODO(e.burkov): Remove once the local resolvers logic moved to dnsproxy. // TODO(e.burkov): Remove once the local resolvers logic moved to dnsproxy.
localResolvers *proxy.Proxy localResolvers *proxy.Proxy
// sysResolvers used to fetch system resolvers to use by default for private
// PTR resolving.
sysResolvers aghnet.SystemResolvers sysResolvers aghnet.SystemResolvers
// recDetector is a cache for recursive requests. It is used to detect // recDetector is a cache for recursive requests. It is used to detect
@ -128,12 +134,6 @@ type Server struct {
// anonymizer masks the client's IP addresses if needed. // anonymizer masks the client's IP addresses if needed.
anonymizer *aghnet.IPMut anonymizer *aghnet.IPMut
tableHostToIP hostToIPTable
tableHostToIPLock sync.Mutex
tableIPToHost ipToHostTable
tableIPToHostLock sync.Mutex
// clientIDCache is a temporary storage for ClientIDs that were extracted // clientIDCache is a temporary storage for ClientIDs that were extracted
// during the BeforeRequestHandler stage. // during the BeforeRequestHandler stage.
clientIDCache cache.Cache clientIDCache cache.Cache
@ -142,13 +142,16 @@ type Server struct {
// We don't Start() it and so no listen port is required. // We don't Start() it and so no listen port is required.
internalProxy *proxy.Proxy internalProxy *proxy.Proxy
// isRunning is true if the DNS server is running.
isRunning bool isRunning bool
// protectionUpdateInProgress is used to make sure that only one goroutine // protectionUpdateInProgress is used to make sure that only one goroutine
// updating the protection configuration after a pause is running at a time. // updating the protection configuration after a pause is running at a time.
protectionUpdateInProgress atomic.Bool protectionUpdateInProgress atomic.Bool
// conf is the current configuration of the server.
conf ServerConfig conf ServerConfig
// serverLock protects Server. // serverLock protects Server.
serverLock sync.RWMutex serverLock sync.RWMutex
} }
@ -164,7 +167,7 @@ type DNSCreateParams struct {
DNSFilter *filtering.DNSFilter DNSFilter *filtering.DNSFilter
Stats stats.Interface Stats stats.Interface
QueryLog querylog.QueryLog QueryLog querylog.QueryLog
DHCPServer dhcpd.Interface DHCPServer DHCP
PrivateNets netutil.SubnetSet PrivateNets netutil.SubnetSet
Anonymizer *aghnet.IPMut Anonymizer *aghnet.IPMut
LocalDomain string LocalDomain string
@ -204,7 +207,8 @@ func NewServer(p DNSCreateParams) (s *Server, err error) {
stats: p.Stats, stats: p.Stats,
queryLog: p.QueryLog, queryLog: p.QueryLog,
privateNets: p.PrivateNets, privateNets: p.PrivateNets,
localDomainSuffix: localDomainSuffix, // TODO(e.burkov): Use some case-insensitive string comparison.
localDomainSuffix: strings.ToLower(localDomainSuffix),
recDetector: newRecursionDetector(recursionTTL, cachedRecurrentReqNum), recDetector: newRecursionDetector(recursionTTL, cachedRecurrentReqNum),
clientIDCache: cache.New(cache.Config{ clientIDCache: cache.New(cache.Config{
EnableLRU: true, EnableLRU: true,
@ -220,11 +224,7 @@ func NewServer(p DNSCreateParams) (s *Server, err error) {
return nil, fmt.Errorf("initializing system resolvers: %w", err) return nil, fmt.Errorf("initializing system resolvers: %w", err)
} }
if p.DHCPServer != nil {
s.dhcpServer = p.DHCPServer s.dhcpServer = p.DHCPServer
s.dhcpServer.SetOnLeaseChanged(s.onDHCPLeaseChanged)
s.onDHCPLeaseChanged(dhcpd.LeaseChangedAdded)
}
if runtime.GOARCH == "mips" || runtime.GOARCH == "mipsle" { if runtime.GOARCH == "mips" || runtime.GOARCH == "mipsle" {
// Use plain DNS on MIPS, encryption is too slow // Use plain DNS on MIPS, encryption is too slow

View File

@ -22,7 +22,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghalg" "github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet" "github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/hashprefix" "github.com/AdguardTeam/AdGuardHome/internal/filtering/hashprefix"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch" "github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
@ -94,8 +93,13 @@ func createTestServer(
f.SetEnabled(true) f.SetEnabled(true)
dhcp := &testDHCP{
OnEnabled: func() (ok bool) { return false },
OnHostByIP: func(ip netip.Addr) (host string) { return "" },
OnIPByHost: func(host string) (ip netip.Addr) { panic("not implemented") },
}
s, err = NewServer(DNSCreateParams{ s, err = NewServer(DNSCreateParams{
DHCPServer: testDHCP, DHCPServer: dhcp,
DNSFilter: f, DNSFilter: f,
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed), PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
}) })
@ -863,8 +867,13 @@ func TestBlockedCustomIP(t *testing.T) {
f, err := filtering.New(&filtering.Config{}, filters) f, err := filtering.New(&filtering.Config{}, filters)
require.NoError(t, err) require.NoError(t, err)
dhcp := &testDHCP{
OnEnabled: func() (ok bool) { return false },
OnHostByIP: func(_ netip.Addr) (host string) { panic("not implemented") },
OnIPByHost: func(_ string) (ip netip.Addr) { panic("not implemented") },
}
s, err := NewServer(DNSCreateParams{ s, err := NewServer(DNSCreateParams{
DHCPServer: testDHCP, DHCPServer: dhcp,
DNSFilter: f, DNSFilter: f,
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed), PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
}) })
@ -1016,8 +1025,13 @@ func TestRewrite(t *testing.T) {
f.SetEnabled(true) f.SetEnabled(true)
dhcp := &testDHCP{
OnEnabled: func() (ok bool) { return false },
OnHostByIP: func(ip netip.Addr) (host string) { panic("not implemented") },
OnIPByHost: func(host string) (ip netip.Addr) { panic("not implemented") },
}
s, err := NewServer(DNSCreateParams{ s, err := NewServer(DNSCreateParams{
DHCPServer: testDHCP, DHCPServer: dhcp,
DNSFilter: f, DNSFilter: f,
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed), PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
}) })
@ -1112,22 +1126,25 @@ func publicKey(priv any) any {
} }
} }
var testDHCP = &dhcpd.MockInterface{ // testDHCP is a mock implementation of the [DHCP] interface.
OnStart: func() (err error) { panic("not implemented") }, type testDHCP struct {
OnStop: func() (err error) { panic("not implemented") }, OnHostByIP func(ip netip.Addr) (host string)
OnEnabled: func() (ok bool) { return true }, OnIPByHost func(host string) (ip netip.Addr)
OnLeases: func(flags dhcpd.GetLeasesFlags) (leases []*dhcpd.Lease) { OnEnabled func() (ok bool)
return []*dhcpd.Lease{{
IP: netip.MustParseAddr("192.168.12.34"),
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
Hostname: "myhost",
}}
},
OnSetOnLeaseChanged: func(olct dhcpd.OnLeaseChangedT) {},
OnFindMACbyIP: func(ip netip.Addr) (mac net.HardwareAddr) { panic("not implemented") },
OnWriteDiskConfig: func(c *dhcpd.ServerConfig) { panic("not implemented") },
} }
// type check
var _ DHCP = (*testDHCP)(nil)
// HostByIP implements the [DHCP] interface for *testDHCP.
func (d *testDHCP) HostByIP(ip netip.Addr) (host string) { return d.OnHostByIP(ip) }
// IPByHost implements the [DHCP] interface for *testDHCP.
func (d *testDHCP) IPByHost(host string) (ip netip.Addr) { return d.OnIPByHost(host) }
// IsClientHost implements the [DHCP] interface for *testDHCP.
func (d *testDHCP) Enabled() (ok bool) { return d.OnEnabled() }
func TestPTRResponseFromDHCPLeases(t *testing.T) { func TestPTRResponseFromDHCPLeases(t *testing.T) {
const localDomain = "lan" const localDomain = "lan"
@ -1136,7 +1153,13 @@ func TestPTRResponseFromDHCPLeases(t *testing.T) {
s, err := NewServer(DNSCreateParams{ s, err := NewServer(DNSCreateParams{
DNSFilter: flt, DNSFilter: flt,
DHCPServer: testDHCP, DHCPServer: &testDHCP{
OnEnabled: func() (ok bool) { return true },
OnIPByHost: func(host string) (ip netip.Addr) { panic("not implemented") },
OnHostByIP: func(ip netip.Addr) (host string) {
return "myhost"
},
},
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed), PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
LocalDomain: localDomain, LocalDomain: localDomain,
}) })
@ -1185,6 +1208,12 @@ func TestPTRResponseFromHosts(t *testing.T) {
`)}, `)},
} }
dhcp := &testDHCP{
OnEnabled: func() (ok bool) { return false },
OnIPByHost: func(host string) (ip netip.Addr) { panic("not implemented") },
OnHostByIP: func(ip netip.Addr) (host string) { return "" },
}
var eventsCalledCounter uint32 var eventsCalledCounter uint32
hc, err := aghnet.NewHostsContainer(testFS, &aghtest.FSWatcher{ hc, err := aghnet.NewHostsContainer(testFS, &aghtest.FSWatcher{
OnEvents: func() (e <-chan struct{}) { OnEvents: func() (e <-chan struct{}) {
@ -1213,7 +1242,7 @@ func TestPTRResponseFromHosts(t *testing.T) {
var s *Server var s *Server
s, err = NewServer(DNSCreateParams{ s, err = NewServer(DNSCreateParams{
DHCPServer: testDHCP, DHCPServer: dhcp,
DNSFilter: flt, DNSFilter: flt,
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed), PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
}) })

View File

@ -47,7 +47,11 @@ func TestHandleDNSRequest_handleDNSRequest(t *testing.T) {
f.SetEnabled(true) f.SetEnabled(true)
s, err := NewServer(DNSCreateParams{ s, err := NewServer(DNSCreateParams{
DHCPServer: testDHCP, DHCPServer: &testDHCP{
OnEnabled: func() (ok bool) { return false },
OnHostByIP: func(ip netip.Addr) (host string) { panic("not implemented") },
OnIPByHost: func(host string) (ip netip.Addr) { panic("not implemented") },
},
DNSFilter: f, DNSFilter: f,
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed), PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
}) })
@ -219,7 +223,7 @@ func TestHandleDNSRequest_filterDNSResponse(t *testing.T) {
f.SetEnabled(true) f.SetEnabled(true)
s, err := NewServer(DNSCreateParams{ s, err := NewServer(DNSCreateParams{
DHCPServer: testDHCP, DHCPServer: &testDHCP{},
DNSFilter: f, DNSFilter: f,
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed), PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
}) })

View File

@ -8,7 +8,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/dnsproxy/upstream"
@ -70,20 +69,26 @@ type dnsContext struct {
// isLocalClient shows if client's IP address is from locally served // isLocalClient shows if client's IP address is from locally served
// network. // network.
isLocalClient bool isLocalClient bool
// isDHCPHost is true if the request for a local domain name and the DHCP is
// available for this request.
isDHCPHost bool
} }
// resultCode is the result of a request processing function. // resultCode is the result of a request processing function.
type resultCode int type resultCode int
const ( const (
// resultCodeSuccess is returned when a handler performed successfully, // resultCodeSuccess is returned when a handler performed successfully, and
// and the next handler must be called. // the next handler must be called.
resultCodeSuccess resultCode = iota resultCodeSuccess resultCode = iota
// resultCodeFinish is returned when a handler performed successfully,
// and the processing of the request must be stopped. // resultCodeFinish is returned when a handler performed successfully, and
// the processing of the request must be stopped.
resultCodeFinish resultCodeFinish
// resultCodeError is returned when a handler failed, and the processing
// of the request must be stopped. // resultCodeError is returned when a handler failed, and the processing of
// the request must be stopped.
resultCodeError resultCodeError
) )
@ -239,70 +244,6 @@ func (s *Server) processClientIP(addr net.Addr) {
s.addrProc.Process(clientIP) s.addrProc.Process(clientIP)
} }
func (s *Server) setTableHostToIP(t hostToIPTable) {
s.tableHostToIPLock.Lock()
defer s.tableHostToIPLock.Unlock()
s.tableHostToIP = t
}
func (s *Server) setTableIPToHost(t ipToHostTable) {
s.tableIPToHostLock.Lock()
defer s.tableIPToHostLock.Unlock()
s.tableIPToHost = t
}
func (s *Server) onDHCPLeaseChanged(flags int) {
switch flags {
case dhcpd.LeaseChangedAdded,
dhcpd.LeaseChangedAddedStatic,
dhcpd.LeaseChangedRemovedStatic:
// Go on.
case dhcpd.LeaseChangedRemovedAll:
s.setTableHostToIP(nil)
s.setTableIPToHost(nil)
return
default:
return
}
ll := s.dhcpServer.Leases(dhcpd.LeasesAll)
hostToIP := make(hostToIPTable, len(ll))
ipToHost := make(ipToHostTable, len(ll))
for _, l := range ll {
// TODO(a.garipov): Remove this after we're finished with the client
// hostname validations in the DHCP server code.
err := netutil.ValidateHostname(l.Hostname)
if err != nil {
log.Debug("dnsforward: skipping invalid hostname %q from dhcp: %s", l.Hostname, err)
continue
}
lowhost := strings.ToLower(l.Hostname + "." + s.localDomainSuffix)
// Assume that we only process IPv4 now.
if !l.IP.Is4() {
log.Debug("dnsforward: skipping invalid ip from dhcp: bad ipv4 net.IP %v", l.IP)
continue
}
leaseIP := l.IP
ipToHost[leaseIP] = lowhost
hostToIP[lowhost] = leaseIP
}
s.setTableHostToIP(hostToIP)
s.setTableIPToHost(ipToHost)
log.Debug("dnsforward: added %d a and ptr entries from dhcp", len(ipToHost))
}
// processDDRQuery responds to Discovery of Designated Resolvers (DDR) SVCB // processDDRQuery responds to Discovery of Designated Resolvers (DDR) SVCB
// queries. The response contains different types of encryption supported by // queries. The response contains different types of encryption supported by
// current user configuration. // current user configuration.
@ -420,18 +361,6 @@ func (s *Server) processDetermineLocal(dctx *dnsContext) (rc resultCode) {
return rc return rc
} }
// dhcpHostToIP tries to get an IP leased by DHCP and returns the copy of
// address since the data inside the internal table may be changed while request
// processing. It's safe for concurrent use.
func (s *Server) dhcpHostToIP(host string) (ip netip.Addr, ok bool) {
s.tableHostToIPLock.Lock()
defer s.tableHostToIPLock.Unlock()
ip, ok = s.tableHostToIP[host]
return ip, ok
}
// processDHCPHosts respond to A requests if the target hostname is known to // processDHCPHosts respond to A requests if the target hostname is known to
// the server. It responds with a mapped IP address if the DNS64 is enabled and // the server. It responds with a mapped IP address if the DNS64 is enabled and
// the request is for AAAA. // the request is for AAAA.
@ -443,30 +372,31 @@ func (s *Server) processDHCPHosts(dctx *dnsContext) (rc resultCode) {
pctx := dctx.proxyCtx pctx := dctx.proxyCtx
req := pctx.Req req := pctx.Req
q := req.Question[0]
reqHost, ok := s.isDHCPClientHostQ(q) q := &req.Question[0]
if !ok { dhcpHost := s.dhcpHostFromRequest(q)
if dctx.isDHCPHost = dhcpHost != ""; !dctx.isDHCPHost {
return resultCodeSuccess return resultCodeSuccess
} }
if !dctx.isLocalClient { if !dctx.isLocalClient {
log.Debug("dnsforward: %q requests for dhcp host %q", pctx.Addr, reqHost) log.Debug("dnsforward: %q requests for dhcp host %q", pctx.Addr, dhcpHost)
pctx.Res = s.genNXDomain(req) pctx.Res = s.genNXDomain(req)
// Do not even put into query log. // Do not even put into query log.
return resultCodeFinish return resultCodeFinish
} }
ip, ok := s.dhcpHostToIP(reqHost) ip := s.dhcpServer.IPByHost(dhcpHost)
if !ok { if ip == (netip.Addr{}) {
// Go on and process them with filters, including dnsrewrite ones, and // Go on and process them with filters, including dnsrewrite ones, and
// possibly route them to a domain-specific upstream. // possibly route them to a domain-specific upstream.
log.Debug("dnsforward: no dhcp record for %q", reqHost) log.Debug("dnsforward: no dhcp record for %q", dhcpHost)
return resultCodeSuccess return resultCodeSuccess
} }
log.Debug("dnsforward: dhcp record for %q is %s", reqHost, ip) log.Debug("dnsforward: dhcp record for %q is %s", dhcpHost, ip)
resp := s.makeResponse(req) resp := s.makeResponse(req)
switch q.Qtype { switch q.Qtype {
@ -638,17 +568,6 @@ func (s *Server) processRestrictLocal(dctx *dnsContext) (rc resultCode) {
return resultCodeSuccess return resultCodeSuccess
} }
// ipToDHCPHost tries to get a hostname leased by DHCP. It's safe for
// concurrent use.
func (s *Server) ipToDHCPHost(ip netip.Addr) (host string, ok bool) {
s.tableIPToHostLock.Lock()
defer s.tableIPToHostLock.Unlock()
host, ok = s.tableIPToHost[ip]
return host, ok
}
// processDHCPAddrs responds to PTR requests if the target IP is leased by the // processDHCPAddrs responds to PTR requests if the target IP is leased by the
// DHCP server. // DHCP server.
func (s *Server) processDHCPAddrs(dctx *dnsContext) (rc resultCode) { func (s *Server) processDHCPAddrs(dctx *dnsContext) (rc resultCode) {
@ -673,12 +592,12 @@ func (s *Server) processDHCPAddrs(dctx *dnsContext) (rc resultCode) {
return resultCodeSuccess return resultCodeSuccess
} }
host, ok := s.ipToDHCPHost(ipAddr) host := s.dhcpServer.HostByIP(ipAddr)
if !ok { if host == "" {
return resultCodeSuccess return resultCodeSuccess
} }
log.Debug("dnsforward: dhcp reverse record for %s is %q", ip, host) log.Debug("dnsforward: dhcp client %s is %q", ip, host)
req := pctx.Req req := pctx.Req
resp := s.makeResponse(req) resp := s.makeResponse(req)
@ -686,10 +605,12 @@ func (s *Server) processDHCPAddrs(dctx *dnsContext) (rc resultCode) {
Hdr: dns.RR_Header{ Hdr: dns.RR_Header{
Name: req.Question[0].Name, Name: req.Question[0].Name,
Rrtype: dns.TypePTR, Rrtype: dns.TypePTR,
// TODO(e.burkov): Use [dhcpsvc.Lease.Expiry]. See
// https://github.com/AdguardTeam/AdGuardHome/issues/3932.
Ttl: s.conf.BlockedResponseTTL, Ttl: s.conf.BlockedResponseTTL,
Class: dns.ClassINET, Class: dns.ClassINET,
}, },
Ptr: dns.Fqdn(host), Ptr: dns.Fqdn(strings.Join([]string{host, s.localDomainSuffix}, ".")),
} }
resp.Answer = append(resp.Answer, ptr) resp.Answer = append(resp.Answer, ptr)
pctx.Res = resp pctx.Res = resp
@ -788,17 +709,18 @@ func (s *Server) processUpstream(dctx *dnsContext) (rc resultCode) {
pctx := dctx.proxyCtx pctx := dctx.proxyCtx
req := pctx.Req req := pctx.Req
q := req.Question[0]
if pctx.Res != nil { if pctx.Res != nil {
// The response has already been set. // The response has already been set.
return resultCodeSuccess return resultCodeSuccess
} else if reqHost, ok := s.isDHCPClientHostQ(q); ok { } else if dctx.isDHCPHost {
// A DHCP client hostname query that hasn't been handled or filtered. // A DHCP client hostname query that hasn't been handled or filtered.
// Respond with an NXDOMAIN. // Respond with an NXDOMAIN.
// //
// TODO(a.garipov): Route such queries to a custom upstream for the // TODO(a.garipov): Route such queries to a custom upstream for the
// local domain name if there is one. // local domain name if there is one.
log.Debug("dnsforward: dhcp client hostname %q was not filtered", reqHost) name := req.Question[0].Name
log.Debug("dnsforward: dhcp client hostname %q was not filtered", name[:len(name)-1])
pctx.Res = s.genNXDomain(req) pctx.Res = s.genNXDomain(req)
return resultCodeFinish return resultCodeFinish
@ -885,26 +807,26 @@ func (s *Server) setRespAD(pctx *proxy.DNSContext, reqWantsDNSSEC bool) {
} }
} }
// isDHCPClientHostQ returns true if q is from a request for a DHCP client // dhcpHostFromRequest returns a hostname from question, if the request is for a
// hostname. If ok is true, reqHost contains the requested hostname. // DHCP client's hostname when DHCP is enabled, and an empty string otherwise.
func (s *Server) isDHCPClientHostQ(q dns.Question) (reqHost string, ok bool) { func (s *Server) dhcpHostFromRequest(q *dns.Question) (reqHost string) {
if !s.dhcpServer.Enabled() { if !s.dhcpServer.Enabled() {
return "", false return ""
} }
// Include AAAA here, because despite the fact that we don't support it yet, // Include AAAA here, because despite the fact that we don't support it yet,
// the expected behavior here is to respond with an empty answer and not // the expected behavior here is to respond with an empty answer and not
// NXDOMAIN. // NXDOMAIN.
if qt := q.Qtype; qt != dns.TypeA && qt != dns.TypeAAAA { if qt := q.Qtype; qt != dns.TypeA && qt != dns.TypeAAAA {
return "", false return ""
} }
reqHost = strings.ToLower(q.Name[:len(q.Name)-1]) reqHost = strings.ToLower(q.Name[:len(q.Name)-1])
if strings.HasSuffix(reqHost, s.localDomainSuffix) { if !netutil.IsImmediateSubdomain(reqHost, s.localDomainSuffix) {
return reqHost, true return ""
} }
return "", false return reqHost[:len(reqHost)-len(s.localDomainSuffix)-1]
} }
// setCustomUpstream sets custom upstream settings in pctx, if necessary. // setCustomUpstream sets custom upstream settings in pctx, if necessary.

View File

@ -416,47 +416,58 @@ func TestServer_ProcessDetermineLocal(t *testing.T) {
} }
func TestServer_ProcessDHCPHosts_localRestriction(t *testing.T) { func TestServer_ProcessDHCPHosts_localRestriction(t *testing.T) {
const (
localDomainSuffix = "lan"
dhcpClient = "example"
knownHost = dhcpClient + "." + localDomainSuffix
unknownHost = "wronghost." + localDomainSuffix
)
knownIP := netip.MustParseAddr("1.2.3.4") knownIP := netip.MustParseAddr("1.2.3.4")
dhcp := &testDHCP{
OnEnabled: func() (_ bool) { return true },
OnIPByHost: func(host string) (ip netip.Addr) {
if host == dhcpClient {
ip = knownIP
}
return ip
},
}
testCases := []struct { testCases := []struct {
wantIP netip.Addr wantIP netip.Addr
name string name string
host string host string
wantRes resultCode
isLocalCli bool isLocalCli bool
}{{ }{{
wantIP: knownIP, wantIP: knownIP,
name: "local_client_success", name: "local_client_success",
host: "example.lan", host: knownHost,
wantRes: resultCodeSuccess,
isLocalCli: true, isLocalCli: true,
}, { }, {
wantIP: netip.Addr{}, wantIP: netip.Addr{},
name: "local_client_unknown_host", name: "local_client_unknown_host",
host: "wronghost.lan", host: unknownHost,
wantRes: resultCodeSuccess,
isLocalCli: true, isLocalCli: true,
}, { }, {
wantIP: netip.Addr{}, wantIP: netip.Addr{},
name: "external_client_known_host", name: "external_client_known_host",
host: "example.lan", host: knownHost,
wantRes: resultCodeFinish,
isLocalCli: false, isLocalCli: false,
}, { }, {
wantIP: netip.Addr{}, wantIP: netip.Addr{},
name: "external_client_unknown_host", name: "external_client_unknown_host",
host: "wronghost.lan", host: unknownHost,
wantRes: resultCodeFinish,
isLocalCli: false, isLocalCli: false,
}} }}
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
s := &Server{ s := &Server{
dhcpServer: testDHCP, dhcpServer: dhcp,
localDomainSuffix: defaultLocalDomainSuffix, localDomainSuffix: localDomainSuffix,
tableHostToIP: hostToIPTable{
"example." + defaultLocalDomainSuffix: knownIP,
},
} }
req := &dns.Msg{ req := &dns.Msg{
@ -478,20 +489,26 @@ func TestServer_ProcessDHCPHosts_localRestriction(t *testing.T) {
} }
res := s.processDHCPHosts(dctx) res := s.processDHCPHosts(dctx)
require.Equal(t, tc.wantRes, res)
pctx := dctx.proxyCtx pctx := dctx.proxyCtx
if tc.wantRes == resultCodeFinish { if !tc.isLocalCli {
require.Equal(t, resultCodeFinish, res)
require.NotNil(t, pctx.Res) require.NotNil(t, pctx.Res)
assert.Equal(t, dns.RcodeNameError, pctx.Res.Rcode) assert.Equal(t, dns.RcodeNameError, pctx.Res.Rcode)
assert.Len(t, pctx.Res.Answer, 0) assert.Empty(t, pctx.Res.Answer)
return return
} }
require.Equal(t, resultCodeSuccess, res)
if tc.wantIP == (netip.Addr{}) { if tc.wantIP == (netip.Addr{}) {
assert.Nil(t, pctx.Res) assert.Nil(t, pctx.Res)
} else {
return
}
require.NotNil(t, pctx.Res) require.NotNil(t, pctx.Res)
ans := pctx.Res.Answer ans := pctx.Res.Answer
@ -503,18 +520,21 @@ func TestServer_ProcessDHCPHosts_localRestriction(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, tc.wantIP, ip) assert.Equal(t, tc.wantIP, ip)
}
}) })
} }
} }
func TestServer_ProcessDHCPHosts(t *testing.T) { func TestServer_ProcessDHCPHosts(t *testing.T) {
const ( const (
examplecom = "example.com" localTLD = "lan"
examplelan = "example." + defaultLocalDomainSuffix
knownClient = "example"
externalHost = knownClient + ".com"
clientHost = knownClient + "." + localTLD
) )
knownIP := netip.MustParseAddr("1.2.3.4") knownIP := netip.MustParseAddr("1.2.3.4")
testCases := []struct { testCases := []struct {
wantIP netip.Addr wantIP netip.Addr
name string name string
@ -524,55 +544,64 @@ func TestServer_ProcessDHCPHosts(t *testing.T) {
qtyp uint16 qtyp uint16
}{{ }{{
wantIP: netip.Addr{}, wantIP: netip.Addr{},
name: "success_external", name: "external",
host: examplecom, host: externalHost,
suffix: defaultLocalDomainSuffix, suffix: localTLD,
wantRes: resultCodeSuccess, wantRes: resultCodeSuccess,
qtyp: dns.TypeA, qtyp: dns.TypeA,
}, { }, {
wantIP: netip.Addr{}, wantIP: netip.Addr{},
name: "success_external_non_a", name: "external_non_a",
host: examplecom, host: externalHost,
suffix: defaultLocalDomainSuffix, suffix: localTLD,
wantRes: resultCodeSuccess, wantRes: resultCodeSuccess,
qtyp: dns.TypeCNAME, qtyp: dns.TypeCNAME,
}, { }, {
wantIP: knownIP, wantIP: knownIP,
name: "success_internal", name: "internal",
host: examplelan, host: clientHost,
suffix: defaultLocalDomainSuffix, suffix: localTLD,
wantRes: resultCodeSuccess, wantRes: resultCodeSuccess,
qtyp: dns.TypeA, qtyp: dns.TypeA,
}, { }, {
wantIP: netip.Addr{}, wantIP: netip.Addr{},
name: "success_internal_unknown", name: "internal_unknown",
host: "example-new.lan", host: "example-new.lan",
suffix: defaultLocalDomainSuffix, suffix: localTLD,
wantRes: resultCodeSuccess, wantRes: resultCodeSuccess,
qtyp: dns.TypeA, qtyp: dns.TypeA,
}, { }, {
wantIP: netip.Addr{}, wantIP: netip.Addr{},
name: "success_internal_aaaa", name: "internal_aaaa",
host: examplelan, host: clientHost,
suffix: defaultLocalDomainSuffix, suffix: localTLD,
wantRes: resultCodeSuccess, wantRes: resultCodeSuccess,
qtyp: dns.TypeAAAA, qtyp: dns.TypeAAAA,
}, { }, {
wantIP: knownIP, wantIP: knownIP,
name: "success_custom_suffix", name: "custom_suffix",
host: "example.custom", host: knownClient + ".custom",
suffix: "custom", suffix: "custom",
wantRes: resultCodeSuccess, wantRes: resultCodeSuccess,
qtyp: dns.TypeA, qtyp: dns.TypeA,
}} }}
for _, tc := range testCases { for _, tc := range testCases {
testDHCP := &testDHCP{
OnEnabled: func() (_ bool) { return true },
OnIPByHost: func(host string) (ip netip.Addr) {
if host == knownClient {
ip = knownIP
}
return ip
},
OnHostByIP: func(ip netip.Addr) (host string) { panic("not implemented") },
}
s := &Server{ s := &Server{
dhcpServer: testDHCP, dhcpServer: testDHCP,
localDomainSuffix: tc.suffix, localDomainSuffix: tc.suffix,
tableHostToIP: hostToIPTable{
"example." + tc.suffix: knownIP,
},
} }
req := &dns.Msg{ req := &dns.Msg{
@ -597,13 +626,6 @@ func TestServer_ProcessDHCPHosts(t *testing.T) {
res := s.processDHCPHosts(dctx) res := s.processDHCPHosts(dctx)
pctx := dctx.proxyCtx pctx := dctx.proxyCtx
assert.Equal(t, tc.wantRes, res) assert.Equal(t, tc.wantRes, res)
if tc.wantRes == resultCodeFinish {
require.NotNil(t, pctx.Res)
assert.Equal(t, dns.RcodeNameError, pctx.Res.Rcode)
return
}
require.NoError(t, dctx.err) require.NoError(t, dctx.err)
if tc.qtyp == dns.TypeAAAA { if tc.qtyp == dns.TypeAAAA {

View File

@ -12,7 +12,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghnet" "github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/arpdb" "github.com/AdguardTeam/AdGuardHome/internal/arpdb"
"github.com/AdguardTeam/AdGuardHome/internal/client" "github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward" "github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
@ -35,7 +34,7 @@ type DHCP interface {
// HostByIP returns the hostname of the DHCP client with the given IP // 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, // 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. // due to an assumption that a DHCP client must always have a hostname.
HostByIP(ip netip.Addr) (host string) HostByIP(ip netip.Addr) (host string)
// MACByIP returns the MAC address for the given IP address leased. It // MACByIP returns the MAC address for the given IP address leased. It
@ -56,8 +55,8 @@ type clientsContainer struct {
allTags *stringutil.Set allTags *stringutil.Set
// dhcpServer is used for looking up clients IP addresses by MAC addresses // dhcp is the DHCP service implementation.
dhcpServer dhcpd.Interface dhcp DHCP
// dnsServer is used for checking clients IP status access list status // dnsServer is used for checking clients IP status access list status
dnsServer *dnsforward.Server dnsServer *dnsforward.Server
@ -94,7 +93,7 @@ type clientsContainer struct {
// Note: this function must be called only once // Note: this function must be called only once
func (clients *clientsContainer) Init( func (clients *clientsContainer) Init(
objects []*clientObject, objects []*clientObject,
dhcpServer dhcpd.Interface, dhcpServer DHCP,
etcHosts *aghnet.HostsContainer, etcHosts *aghnet.HostsContainer,
arpDB arpdb.Interface, arpDB arpdb.Interface,
filteringConf *filtering.Config, filteringConf *filtering.Config,
@ -109,7 +108,9 @@ func (clients *clientsContainer) Init(
clients.allTags = stringutil.NewSet(clientTags...) clients.allTags = stringutil.NewSet(clientTags...)
clients.dhcpServer = dhcpServer // TODO(e.burkov): Use [dhcpsvc] implementation when it's ready.
clients.dhcp = dhcpServer
clients.etcHosts = etcHosts clients.etcHosts = etcHosts
clients.arpDB = arpDB clients.arpDB = arpDB
err = clients.addFromConfig(objects, filteringConf) err = clients.addFromConfig(objects, filteringConf)
@ -125,11 +126,6 @@ func (clients *clientsContainer) Init(
return nil return nil
} }
if clients.dhcpServer != nil {
clients.dhcpServer.SetOnLeaseChanged(clients.onDHCPLeaseChanged)
clients.onDHCPLeaseChanged(dhcpd.LeaseChangedAdded)
}
if clients.etcHosts != nil { if clients.etcHosts != nil {
go clients.handleHostsUpdates() go clients.handleHostsUpdates()
} }
@ -310,40 +306,6 @@ func (clients *clientsContainer) periodicUpdate() {
} }
} }
// onDHCPLeaseChanged is a callback for the DHCP server. It updates the list of
// runtime clients using the DHCP server's leases.
//
// TODO(e.burkov): Remove when switched to dhcpsvc.
func (clients *clientsContainer) onDHCPLeaseChanged(flags int) {
if clients.dhcpServer == nil || !config.Clients.Sources.DHCP {
return
}
clients.lock.Lock()
defer clients.lock.Unlock()
clients.rmHostsBySrc(ClientSourceDHCP)
if flags == dhcpd.LeaseChangedRemovedAll {
return
}
leases := clients.dhcpServer.Leases(dhcpd.LeasesAll)
n := 0
for _, l := range leases {
if l.Hostname == "" {
continue
}
ok := clients.addHostLocked(l.IP, l.Hostname, ClientSourceDHCP)
if ok {
n++
}
}
log.Debug("clients: added %d client aliases from dhcp", n)
}
// clientSource checks if client with this IP address already exists and returns // clientSource checks if client with this IP address already exists and returns
// the source which updated it last. It returns [ClientSourceNone] if the // the source which updated it last. It returns [ClientSourceNone] if the
// client doesn't exist. // client doesn't exist.
@ -358,10 +320,14 @@ func (clients *clientsContainer) clientSource(ip netip.Addr) (src clientSource)
rc, ok := clients.ipToRC[ip] rc, ok := clients.ipToRC[ip]
if ok { if ok {
return rc.Source src = rc.Source
} }
return ClientSourceNone if src < ClientSourceDHCP && clients.dhcp.HostByIP(ip) != "" {
src = ClientSourceDHCP
}
return src
} }
// findMultiple is a wrapper around Find to make it a valid client finder for // findMultiple is a wrapper around Find to make it a valid client finder for
@ -522,17 +488,14 @@ func (clients *clientsContainer) findLocked(id string) (c *Client, ok bool) {
} }
} }
if clients.dhcpServer != nil { // TODO(e.burkov): Iterate through clients.list only once.
return clients.findDHCP(ip) return clients.findDHCP(ip)
}
return nil, false
} }
// findDHCP searches for a client by its MAC, if the DHCP server is active and // findDHCP searches for a client by its MAC, if the DHCP server is active and
// there is such client. clients.lock is expected to be locked. // there is such client. clients.lock is expected to be locked.
func (clients *clientsContainer) findDHCP(ip netip.Addr) (c *Client, ok bool) { func (clients *clientsContainer) findDHCP(ip netip.Addr) (c *Client, ok bool) {
foundMAC := clients.dhcpServer.FindMACbyIP(ip) foundMAC := clients.dhcp.MACByIP(ip)
if foundMAC == nil { if foundMAC == nil {
return nil, false return nil, false
} }
@ -553,8 +516,9 @@ func (clients *clientsContainer) findDHCP(ip netip.Addr) (c *Client, ok bool) {
return nil, false return nil, false
} }
// findRuntimeClient finds a runtime client by their IP. // runtimeClient returns a runtime client from internal index. Note that it
func (clients *clientsContainer) findRuntimeClient(ip netip.Addr) (rc *RuntimeClient, ok bool) { // doesn't include DHCP clients.
func (clients *clientsContainer) runtimeClient(ip netip.Addr) (rc *RuntimeClient, ok bool) {
if ip == (netip.Addr{}) { if ip == (netip.Addr{}) {
return nil, false return nil, false
} }
@ -567,6 +531,24 @@ func (clients *clientsContainer) findRuntimeClient(ip netip.Addr) (rc *RuntimeCl
return rc, ok return rc, ok
} }
// findRuntimeClient finds a runtime client by their IP.
func (clients *clientsContainer) findRuntimeClient(ip netip.Addr) (rc *RuntimeClient, ok bool) {
if rc, ok = clients.runtimeClient(ip); ok && rc.Source > ClientSourceDHCP {
return rc, ok
}
host := clients.dhcp.HostByIP(ip)
if host == "" {
return rc, ok
}
return &RuntimeClient{
Host: host,
Source: ClientSourceDHCP,
WHOIS: &whois.Info{},
}, true
}
// check validates the client. // check validates the client.
func (clients *clientsContainer) check(c *Client) (err error) { func (clients *clientsContainer) check(c *Client) (err error) {
switch { switch {
@ -824,10 +806,15 @@ func (clients *clientsContainer) addHostLocked(
) (ok bool) { ) (ok bool) {
rc, ok := clients.ipToRC[ip] rc, ok := clients.ipToRC[ip]
if !ok { if !ok {
if src < ClientSourceDHCP {
if clients.dhcp.HostByIP(ip) != "" {
return false
}
}
rc = &RuntimeClient{ rc = &RuntimeClient{
WHOIS: &whois.Info{}, WHOIS: &whois.Info{},
} }
clients.ipToRC[ip] = rc clients.ipToRC[ip] = rc
} else if src < rc.Source { } else if src < rc.Source {
return false return false

View File

@ -8,21 +8,44 @@ import (
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd" "github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/whois" "github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
type testDHCP struct {
OnLeases func() (leases []*dhcpsvc.Lease)
OnHostBy func(ip netip.Addr) (host string)
OnMACBy func(ip netip.Addr) (mac net.HardwareAddr)
}
// Lease implements the [DHCP] interface for testDHCP.
func (t *testDHCP) Leases() (leases []*dhcpsvc.Lease) { return t.OnLeases() }
// HostByIP implements the [DHCP] interface for testDHCP.
func (t *testDHCP) HostByIP(ip netip.Addr) (host string) { return t.OnHostBy(ip) }
// MACByIP implements the [DHCP] interface for testDHCP.
func (t *testDHCP) MACByIP(ip netip.Addr) (mac net.HardwareAddr) { return t.OnMACBy(ip) }
// newClientsContainer is a helper that creates a new clients container for // newClientsContainer is a helper that creates a new clients container for
// tests. // tests.
func newClientsContainer(t *testing.T) (c *clientsContainer) { func newClientsContainer(t *testing.T) (c *clientsContainer) {
t.Helper()
c = &clientsContainer{ c = &clientsContainer{
testing: true, testing: true,
} }
err := c.Init(nil, nil, nil, nil, &filtering.Config{}) dhcp := &testDHCP{
require.NoError(t, err) OnLeases: func() (leases []*dhcpsvc.Lease) { panic("not implemented") },
OnHostBy: func(ip netip.Addr) (host string) { return "" },
OnMACBy: func(ip netip.Addr) (mac net.HardwareAddr) { return nil },
}
require.NoError(t, c.Init(nil, dhcp, nil, nil, &filtering.Config{}))
return c return c
} }
@ -288,7 +311,7 @@ func TestClientsAddExisting(t *testing.T) {
dhcpServer, err := dhcpd.Create(config) dhcpServer, err := dhcpd.Create(config)
require.NoError(t, err) require.NoError(t, err)
clients.dhcpServer = dhcpServer clients.dhcp = dhcpServer
err = dhcpServer.AddStaticLease(&dhcpd.Lease{ err = dhcpServer.AddStaticLease(&dhcpd.Lease{
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},

View File

@ -123,6 +123,17 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http
data.RuntimeClients = append(data.RuntimeClients, cj) data.RuntimeClients = append(data.RuntimeClients, cj)
} }
for _, l := range clients.dhcp.Leases() {
cj := runtimeClientJSON{
Name: l.Hostname,
Source: ClientSourceDHCP,
IP: l.IP,
WHOIS: &whois.Info{},
}
data.RuntimeClients = append(data.RuntimeClients, cj)
}
data.Tags = clientTags data.Tags = clientTags
aghhttp.WriteJSONResponseOK(w, r, data) aghhttp.WriteJSONResponseOK(w, r, data)

View File

@ -14,7 +14,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet" "github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/client" "github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward" "github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/querylog" "github.com/AdguardTeam/AdGuardHome/internal/querylog"
@ -123,7 +122,7 @@ func initDNSServer(
filters *filtering.DNSFilter, filters *filtering.DNSFilter,
sts stats.Interface, sts stats.Interface,
qlog querylog.QueryLog, qlog querylog.QueryLog,
dhcpSrv dhcpd.Interface, dhcpSrv dnsforward.DHCP,
anonymizer *aghnet.IPMut, anonymizer *aghnet.IPMut,
httpReg aghhttp.RegisterFunc, httpReg aghhttp.RegisterFunc,
tlsConf *tlsConfigSettings, tlsConf *tlsConfigSettings,

View File

@ -7,6 +7,6 @@ import (
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
testutil.DiscardLogOutput(m)
initCmdLineOpts() initCmdLineOpts()
testutil.DiscardLogOutput(m)
} }