diff --git a/CHANGELOG.md b/CHANGELOG.md index 709e8fe0..02f1daed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,9 +21,33 @@ and this project adheres to ### Changed +- The setting `local_domain_name` is now in the `dhcp` block in the + configuration file to avoid confusion ([#3367]). - The `dns.bogus_nxdomain` configuration file parameter now supports CIDR notation alongside IP addresses ([#1730]). +#### Configuration Changes + +In this release, the schema version has changed from 12 to 13. + +- Parameter `local_domain_name`, which in schema versions 12 and earlier used to + be a part of the `dns` object, is now a part of the `dhcp` object: + + ```yaml + # BEFORE: + 'dns': + # … + 'local_domain_name': 'lan' + + # AFTER: + 'dhcp': + # … + 'local_domain_name': 'lan' + ``` + + To rollback this change, move the parameter back into `dns` and change the + `schema_version` back to `12`. + ### Deprecated <!-- @@ -38,6 +62,7 @@ and this project adheres to [#1730]: https://github.com/AdguardTeam/AdGuardHome/issues/1730 [#3057]: https://github.com/AdguardTeam/AdGuardHome/issues/3057 +[#3367]: https://github.com/AdguardTeam/AdGuardHome/issues/3367 diff --git a/internal/aghnet/dhcp_unix.go b/internal/aghnet/dhcp_unix.go index 554d68c6..fd2b9b93 100644 --- a/internal/aghnet/dhcp_unix.go +++ b/internal/aghnet/dhcp_unix.go @@ -111,7 +111,7 @@ func discover4(iface *net.Interface, dstAddr *net.UDPAddr, hostname string) (ok // is spoiled. // // It's also known that listening on the specified interface's address - // ignores broadcasted packets when reading. + // ignores broadcast packets when reading. var c net.PacketConn if c, err = listenPacketReusable(iface.Name, "udp4", ":68"); err != nil { return false, fmt.Errorf("couldn't listen on :68: %w", err) diff --git a/internal/dhcpd/dhcpd.go b/internal/dhcpd/dhcpd.go index 341aa3d6..55c56c18 100644 --- a/internal/dhcpd/dhcpd.go +++ b/internal/dhcpd/dhcpd.go @@ -119,23 +119,28 @@ func (l *Lease) UnmarshalJSON(data []byte) (err error) { return nil } -// ServerConfig - DHCP server configuration -// field ordering is important -- yaml fields will mirror ordering from here +// ServerConfig is the configuration for the DHCP server. The order of YAML +// fields is important, since the YAML configuration file follows it. type ServerConfig struct { - Enabled bool `yaml:"enabled"` - InterfaceName string `yaml:"interface_name"` - - Conf4 V4ServerConf `yaml:"dhcpv4"` - Conf6 V6ServerConf `yaml:"dhcpv6"` - - WorkDir string `yaml:"-"` - DBFilePath string `yaml:"-"` // path to DB file - // Called when the configuration is changed by HTTP request ConfigModified func() `yaml:"-"` // Register an HTTP handler HTTPRegister func(string, string, func(http.ResponseWriter, *http.Request)) `yaml:"-"` + + Enabled bool `yaml:"enabled"` + InterfaceName string `yaml:"interface_name"` + + // LocalDomainName is the domain name used for DHCP hosts. For example, + // a DHCP client with the hostname "myhost" can be addressed as "myhost.lan" + // when LocalDomainName is "lan". + LocalDomainName string `yaml:"local_domain_name"` + + Conf4 V4ServerConf `yaml:"dhcpv4"` + Conf6 V6ServerConf `yaml:"dhcpv6"` + + WorkDir string `yaml:"-"` + DBFilePath string `yaml:"-"` } // OnLeaseChangedT is a callback for lease changes. @@ -156,7 +161,9 @@ type Server struct { srv4 DHCPServer srv6 DHCPServer - conf ServerConfig + // 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 @@ -181,14 +188,21 @@ type ServerInterface interface { } // Create - create object -func Create(conf ServerConfig) (s *Server, err error) { - s = &Server{} +func Create(conf *ServerConfig) (s *Server, err error) { + s = &Server{ + conf: &ServerConfig{ + ConfigModified: conf.ConfigModified, - s.conf.Enabled = conf.Enabled - s.conf.InterfaceName = conf.InterfaceName - s.conf.HTTPRegister = conf.HTTPRegister - s.conf.ConfigModified = conf.ConfigModified - s.conf.DBFilePath = filepath.Join(conf.WorkDir, dbFilename) + HTTPRegister: conf.HTTPRegister, + + Enabled: conf.Enabled, + InterfaceName: conf.InterfaceName, + + LocalDomainName: conf.LocalDomainName, + + DBFilePath: filepath.Join(conf.WorkDir, dbFilename), + }, + } if !webHandlersRegistered && s.conf.HTTPRegister != nil { if runtime.GOOS == "windows" { @@ -305,6 +319,7 @@ func (s *Server) notify(flags int) { 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) } diff --git a/internal/dhcpd/dhcpd_test.go b/internal/dhcpd/dhcpd_test.go index b8fc5fa0..b704cbb4 100644 --- a/internal/dhcpd/dhcpd_test.go +++ b/internal/dhcpd/dhcpd_test.go @@ -27,7 +27,7 @@ func testNotify(flags uint32) { func TestDB(t *testing.T) { var err error s := Server{ - conf: ServerConfig{ + conf: &ServerConfig{ DBFilePath: dbFilename, }, } @@ -140,27 +140,27 @@ func TestNormalizeLeases(t *testing.T) { func TestV4Server_badRange(t *testing.T) { testCases := []struct { name string + wantErrMsg string gatewayIP net.IP subnetMask net.IP - wantErrMsg string }{{ - name: "gateway_in_range", - gatewayIP: net.IP{192, 168, 10, 120}, - subnetMask: net.IP{255, 255, 255, 0}, + name: "gateway_in_range", wantErrMsg: "dhcpv4: gateway ip 192.168.10.120 in the ip range: " + "192.168.10.20-192.168.10.200", + gatewayIP: net.IP{192, 168, 10, 120}, + subnetMask: net.IP{255, 255, 255, 0}, }, { - name: "outside_range_start", - gatewayIP: net.IP{192, 168, 10, 1}, - subnetMask: net.IP{255, 255, 255, 240}, + name: "outside_range_start", wantErrMsg: "dhcpv4: range start 192.168.10.20 is outside network " + "192.168.10.1/28", - }, { - name: "outside_range_end", gatewayIP: net.IP{192, 168, 10, 1}, - subnetMask: net.IP{255, 255, 255, 224}, + subnetMask: net.IP{255, 255, 255, 240}, + }, { + name: "outside_range_end", wantErrMsg: "dhcpv4: range end 192.168.10.200 is outside network " + "192.168.10.1/27", + gatewayIP: net.IP{192, 168, 10, 1}, + subnetMask: net.IP{255, 255, 255, 224}, }} for _, tc := range testCases { diff --git a/internal/dhcpd/http.go b/internal/dhcpd/http.go index 78016010..e340addb 100644 --- a/internal/dhcpd/http.go +++ b/internal/dhcpd/http.go @@ -575,12 +575,15 @@ func (s *Server) handleReset(w http.ResponseWriter, r *http.Request) { log.Error("dhcp: removing db: %s", err) } - oldconf := s.conf - s.conf = ServerConfig{ - WorkDir: oldconf.WorkDir, - HTTPRegister: oldconf.HTTPRegister, - ConfigModified: oldconf.ConfigModified, - DBFilePath: oldconf.DBFilePath, + s.conf = &ServerConfig{ + ConfigModified: s.conf.ConfigModified, + + HTTPRegister: s.conf.HTTPRegister, + + LocalDomainName: s.conf.LocalDomainName, + + WorkDir: s.conf.WorkDir, + DBFilePath: s.conf.DBFilePath, } v4conf := V4ServerConf{ diff --git a/internal/dhcpd/nullbool_test.go b/internal/dhcpd/nullbool_test.go index 32c9f9f4..549df608 100644 --- a/internal/dhcpd/nullbool_test.go +++ b/internal/dhcpd/nullbool_test.go @@ -12,33 +12,33 @@ import ( func TestNullBool_UnmarshalJSON(t *testing.T) { testCases := []struct { name string - data []byte wantErrMsg string + data []byte want nullBool }{{ name: "empty", - data: []byte{}, wantErrMsg: "", + data: []byte{}, want: nbNull, }, { name: "null", - data: []byte("null"), wantErrMsg: "", + data: []byte("null"), want: nbNull, }, { name: "true", - data: []byte("true"), wantErrMsg: "", + data: []byte("true"), want: nbTrue, }, { name: "false", - data: []byte("false"), wantErrMsg: "", + data: []byte("false"), want: nbFalse, }, { name: "invalid", - data: []byte("flase"), - wantErrMsg: `invalid nullBool value "flase"`, + wantErrMsg: `invalid nullBool value "invalid"`, + data: []byte("invalid"), want: nbNull, }} diff --git a/internal/dhcpd/v4.go b/internal/dhcpd/v4.go index e7dc0571..4e2a3a25 100644 --- a/internal/dhcpd/v4.go +++ b/internal/dhcpd/v4.go @@ -969,11 +969,10 @@ func (s *v4Server) send(peer net.Addr, conn net.PacketConn, req, resp *dhcpv4.DH Port: dhcpv4.ServerPort, } if mtype == dhcpv4.MessageTypeNak { - // Set the broadcast bit in the DHCPNAK, so that the - // relay agent broadcasted it to the client, because the - // client may not have a correct network address or - // subnet mask, and the client may not be answering ARP - // requests. + // Set the broadcast bit in the DHCPNAK, so that the relay agent + // broadcasts it to the client, because the client may not have + // a correct network address or subnet mask, and the client may not + // be answering ARP requests. resp.SetBroadcast() } case mtype == dhcpv4.MessageTypeNak: diff --git a/internal/dnsforward/dns.go b/internal/dnsforward/dns.go index d9e04b80..c11171d9 100644 --- a/internal/dnsforward/dns.go +++ b/internal/dnsforward/dns.go @@ -215,9 +215,8 @@ func (s *Server) onDHCPLeaseChanged(flags int) { ipToHost = netutil.NewIPMap(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. + // TODO(a.garipov): Remove this after we're finished with the client + // hostname validations in the DHCP server code. err = netutil.ValidateDomainName(l.Hostname) if err != nil { log.Debug( @@ -301,6 +300,8 @@ func (s *Server) processInternalHosts(dctx *dnsContext) (rc resultCode) { } reqHost := strings.ToLower(q.Name) + // TODO(a.garipov): Move everything related to DHCP local domain to the DHCP + // server. host := strings.TrimSuffix(reqHost, s.localDomainSuffix) if host == reqHost { return resultCodeSuccess diff --git a/internal/home/clients_test.go b/internal/home/clients_test.go index 93bf3360..6cc5d46f 100644 --- a/internal/home/clients_test.go +++ b/internal/home/clients_test.go @@ -3,10 +3,12 @@ package home import ( "net" "os" + "runtime" "testing" "time" "github.com/AdguardTeam/AdGuardHome/internal/dhcpd" + "github.com/AdguardTeam/golibs/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -271,12 +273,18 @@ func TestClientsAddExisting(t *testing.T) { }) t.Run("complicated", func(t *testing.T) { + // TODO(a.garipov): Properly decouple the DHCP server from the client + // storage. + if runtime.GOOS == "windows" { + t.Skip("skipping dhcp test on windows") + } + var err error ip := net.IP{1, 2, 3, 4} // First, init a DHCP server with a single static lease. - config := dhcpd.ServerConfig{ + config := &dhcpd.ServerConfig{ Enabled: true, DBFilePath: "leases.db", Conf4: dhcpd.V4ServerConf{ @@ -290,10 +298,9 @@ func TestClientsAddExisting(t *testing.T) { clients.dhcpServer, err = dhcpd.Create(config) require.NoError(t, err) - // TODO(e.burkov): leases.db isn't created on Windows so removing it - // causes an error. Split the test to make it run properly on different - // operating systems. - t.Cleanup(func() { _ = os.Remove("leases.db") }) + testutil.CleanupAndRequireSuccess(t, func() (err error) { + return os.Remove("leases.db") + }) err = clients.dhcpServer.AddStaticLease(&dhcpd.Lease{ HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, diff --git a/internal/home/config.go b/internal/home/config.go index a6247164..6ecb4ccb 100644 --- a/internal/home/config.go +++ b/internal/home/config.go @@ -83,7 +83,7 @@ type configuration struct { WhitelistFilters []filter `yaml:"whitelist_filters"` UserRules []string `yaml:"user_rules"` - DHCP dhcpd.ServerConfig `yaml:"dhcp"` + DHCP *dhcpd.ServerConfig `yaml:"dhcp"` // Clients contains the YAML representations of the persistent clients. // This field is only used for reading and writing persistent client data. @@ -123,11 +123,6 @@ type dnsConfig struct { // UpstreamTimeout is the timeout for querying upstream servers. UpstreamTimeout timeutil.Duration `yaml:"upstream_timeout"` - // LocalDomainName is the domain name used for known internal hosts. - // For example, a machine called "myhost" can be addressed as - // "myhost.lan" when LocalDomainName is "lan". - LocalDomainName string `yaml:"local_domain_name"` - // ResolveClients enables and disables resolving clients with RDNS. ResolveClients bool `yaml:"resolve_clients"` @@ -199,7 +194,6 @@ var config = &configuration{ FilteringEnabled: true, // whether or not use filter lists FiltersUpdateIntervalHours: 24, UpstreamTimeout: timeutil.Duration{Duration: dnsforward.DefaultTimeout}, - LocalDomainName: "lan", ResolveClients: true, UsePrivateRDNS: true, }, @@ -208,6 +202,9 @@ var config = &configuration{ PortDNSOverTLS: defaultPortTLS, // needs to be passed through to dnsproxy PortDNSOverQUIC: defaultPortQUIC, }, + DHCP: &dhcpd.ServerConfig{ + LocalDomainName: "lan", + }, logSettings: logSettings{ LogCompress: false, LogLocalTime: false, @@ -389,8 +386,8 @@ func (c *configuration) write() error { } if Context.dhcpServer != nil { - c := dhcpd.ServerConfig{} - Context.dhcpServer.WriteDiskConfig(&c) + c := &dhcpd.ServerConfig{} + Context.dhcpServer.WriteDiskConfig(c) config.DHCP = c } diff --git a/internal/home/dns.go b/internal/home/dns.go index 8775e152..3ac7a9ab 100644 --- a/internal/home/dns.go +++ b/internal/home/dns.go @@ -83,7 +83,7 @@ func initDNSServer() (err error) { QueryLog: Context.queryLog, SubnetDetector: Context.subnetDetector, Anonymizer: anonymizer, - LocalDomain: config.DNS.LocalDomainName, + LocalDomain: config.DHCP.LocalDomainName, } if Context.dhcpServer != nil { p.DHCPServer = Context.dhcpServer diff --git a/internal/home/upgrade.go b/internal/home/upgrade.go index 89d41556..34297470 100644 --- a/internal/home/upgrade.go +++ b/internal/home/upgrade.go @@ -21,7 +21,7 @@ import ( ) // currentSchemaVersion is the current schema version. -const currentSchemaVersion = 12 +const currentSchemaVersion = 13 // These aliases are provided for convenience. type ( @@ -85,6 +85,7 @@ func upgradeConfigSchema(oldVersion int, diskConf yobj) (err error) { upgradeSchema9to10, upgradeSchema10to11, upgradeSchema11to12, + upgradeSchema12to13, } n := 0 @@ -690,6 +691,52 @@ func upgradeSchema11to12(diskConf yobj) (err error) { return nil } +// upgradeSchema12to13 performs the following changes: +// +// # BEFORE: +// 'dns': +// # … +// 'local_domain_name': 'lan' +// +// # AFTER: +// 'dhcp': +// # … +// 'local_domain_name': 'lan' +// +func upgradeSchema12to13(diskConf yobj) (err error) { + log.Printf("Upgrade yaml: 12 to 13") + diskConf["schema_version"] = 13 + + dnsVal, ok := diskConf["dns"] + if !ok { + return nil + } + + var dns yobj + dns, ok = dnsVal.(yobj) + if !ok { + return fmt.Errorf("unexpected type of dns: %T", dnsVal) + } + + dhcpVal, ok := diskConf["dhcp"] + if !ok { + return nil + } + + var dhcp yobj + dhcp, ok = dhcpVal.(yobj) + if !ok { + return fmt.Errorf("unexpected type of dhcp: %T", dnsVal) + } + + const field = "local_domain_name" + + dhcp[field] = dns[field] + delete(dns, field) + + return nil +} + // TODO(a.garipov): Replace with log.Output when we port it to our logging // package. func funcName() string { diff --git a/internal/home/upgrade_test.go b/internal/home/upgrade_test.go index 171ce3b2..c63bc443 100644 --- a/internal/home/upgrade_test.go +++ b/internal/home/upgrade_test.go @@ -55,7 +55,7 @@ func TestUpgradeSchema2to3(t *testing.T) { require.Len(t, v, 1) require.Equal(t, "8.8.8.8:53", v[0]) default: - t.Fatalf("wrong type for bootsrap dns: %T", v) + t.Fatalf("wrong type for bootstrap dns: %T", v) } excludedEntries := []string{"bootstrap_dns"} @@ -511,3 +511,48 @@ func TestUpgradeSchema11to12(t *testing.T) { assert.Equal(t, 90*24*time.Hour, ivlVal.Duration) }) } + +func TestUpgradeSchema12to13(t *testing.T) { + t.Run("no_dns", func(t *testing.T) { + conf := yobj{} + + err := upgradeSchema12to13(conf) + require.NoError(t, err) + + assert.Equal(t, conf["schema_version"], 13) + }) + + t.Run("no_dhcp", func(t *testing.T) { + conf := yobj{ + "dns": yobj{}, + } + + err := upgradeSchema12to13(conf) + require.NoError(t, err) + + assert.Equal(t, conf["schema_version"], 13) + }) + + t.Run("good", func(t *testing.T) { + conf := yobj{ + "dns": yobj{ + "local_domain_name": "lan", + }, + "dhcp": yobj{}, + "schema_version": 12, + } + + wantConf := yobj{ + "dns": yobj{}, + "dhcp": yobj{ + "local_domain_name": "lan", + }, + "schema_version": 13, + } + + err := upgradeSchema12to13(conf) + require.NoError(t, err) + + assert.Equal(t, wantConf, conf) + }) +}