diff --git a/CHANGELOG.md b/CHANGELOG.md index c54857dd..2baec6eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to ### Added +- Hostname generating for DHCP clients which don't provide their own ([#2723]). - New flag `--no-etc-hosts` to disable client domain name lookups in the operating system's /etc/hosts files ([#1947]). - The ability to set up custom upstreams to resolve PTR queries for local @@ -67,6 +68,7 @@ and this project adheres to [#2533]: https://github.com/AdguardTeam/AdGuardHome/issues/2533 [#2541]: https://github.com/AdguardTeam/AdGuardHome/issues/2541 [#2704]: https://github.com/AdguardTeam/AdGuardHome/issues/2704 +[#2723]: https://github.com/AdguardTeam/AdGuardHome/issues/2723 [#2824]: https://github.com/AdguardTeam/AdGuardHome/issues/2824 [#2828]: https://github.com/AdguardTeam/AdGuardHome/issues/2828 [#2835]: https://github.com/AdguardTeam/AdGuardHome/issues/2835 diff --git a/internal/aghnet/addr.go b/internal/aghnet/addr.go index fb467200..ff357c5b 100644 --- a/internal/aghnet/addr.go +++ b/internal/aghnet/addr.go @@ -3,6 +3,7 @@ package aghnet import ( "fmt" "net" + "strconv" "strings" "github.com/AdguardTeam/AdGuardHome/internal/agherr" @@ -99,3 +100,59 @@ func ValidateDomainName(name string) (err error) { return nil } + +// The maximum lengths of generated hostnames for different IP versions. +const ( + ipv4HostnameMaxLen = len("192-168-100-10-") + ipv6HostnameMaxLen = len("ff80-f076-0000-0000-0000-0000-0000-0010") +) + +// generateIPv4Hostname generates the hostname for specific IP version. +func generateIPv4Hostname(ipv4 net.IP) (hostname string) { + hnData := make([]byte, 0, ipv4HostnameMaxLen) + for i, part := range ipv4 { + if i > 0 { + hnData = append(hnData, '-') + } + hnData = strconv.AppendUint(hnData, uint64(part), 10) + } + + return string(hnData) +} + +// generateIPv6Hostname generates the hostname for specific IP version. +func generateIPv6Hostname(ipv6 net.IP) (hostname string) { + hnData := make([]byte, 0, ipv6HostnameMaxLen) + for i, partsNum := 0, net.IPv6len/2; i < partsNum; i++ { + if i > 0 { + hnData = append(hnData, '-') + } + for _, val := range ipv6[i*2 : i*2+2] { + if val < 10 { + hnData = append(hnData, '0') + } + hnData = strconv.AppendUint(hnData, uint64(val), 16) + } + } + + return string(hnData) +} + +// GenerateHostName generates the hostname from ip. In case of using IPv4 the +// result should be like: +// +// 192-168-10-1 +// +// In case of using IPv6, the result is like: +// +// ff80-f076-0000-0000-0000-0000-0000-0010 +// +func GenerateHostName(ip net.IP) (hostname string) { + if ipv4 := ip.To4(); ipv4 != nil { + return generateIPv4Hostname(ipv4) + } else if ipv6 := ip.To16(); ipv6 != nil { + return generateIPv6Hostname(ipv6) + } + + return "" +} diff --git a/internal/aghnet/addr_test.go b/internal/aghnet/addr_test.go index 51376059..53af1aac 100644 --- a/internal/aghnet/addr_test.go +++ b/internal/aghnet/addr_test.go @@ -131,3 +131,43 @@ func TestValidateDomainName(t *testing.T) { }) } } + +func TestGenerateHostName(t *testing.T) { + testCases := []struct { + name string + want string + ip net.IP + }{{ + name: "good_ipv4", + want: "127-0-0-1", + ip: net.IP{127, 0, 0, 1}, + }, { + name: "bad_ipv4", + want: "", + ip: net.IP{127, 0, 0, 1, 0}, + }, { + name: "good_ipv6", + want: "fe00-0000-0000-0000-0000-0000-0000-0001", + ip: net.ParseIP("fe00::1"), + }, { + name: "bad_ipv6", + want: "", + ip: net.IP{ + 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, + }, + }, { + name: "nil", + want: "", + ip: nil, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + hostname := GenerateHostName(tc.ip) + assert.Equal(t, tc.want, hostname) + }) + } +} diff --git a/internal/dhcpd/v4.go b/internal/dhcpd/v4.go index 3ad76036..d0a4a7a1 100644 --- a/internal/dhcpd/v4.go +++ b/internal/dhcpd/v4.go @@ -634,13 +634,19 @@ func (s *v4Server) processRequest(req, resp *dhcpv4.DHCPv4) (lease *Lease, ok bo } if !lease.IsStatic() { - lease.Hostname, err = s.normalizeHostname(req.HostName()) + var hostname string + hostname, err = s.normalizeHostname(req.HostName()) if err != nil { log.Error("dhcpv4: cannot normalize hostname for %s: %s", mac, err) return nil, false } + if hostname == "" { + hostname = aghnet.GenerateHostName(reqIP) + } + + lease.Hostname = hostname s.commitLease(lease) } else if len(lease.Hostname) != 0 { o := &optFQDN{