all: sync with master

This commit is contained in:
Eugene Burkov
2025-01-20 14:41:16 +03:00
parent ceb178fcd5
commit 2e52a2c8a0
61 changed files with 2533 additions and 2711 deletions

View File

@@ -13,6 +13,7 @@ import (
type ServiceWithConfig[ConfigType any] interface {
service.Interface
// Config returns a deep clone of the configuration of the service.
Config() (c ConfigType)
}

View File

@@ -1,15 +1,17 @@
# AdGuard Home v0.108.0 Changelog DRAFT
This changelog should be merged into the main one once the next API matures
enough.
This changelog should be merged into the main one once the next API matures enough.
## [v0.108.0] - TODO
### Added
- The ability to change the port of the pprof debug API.
- The ability to log to stderr using `--logFile=stderr`.
- The new `--web-addr` flag to set the Web UI address in a `host:port` form.
- `SIGHUP` now reloads all configuration from the configuration file ([#5676]).
### Changed
@@ -20,20 +22,21 @@ enough.
#### Other changes
- `-h` is now an alias for `--help` instead of the removed `--host`, see below.
Use `--web-addr=host:port` to set an address on which to serve the Web UI.
- `-h` is now an alias for `--help` instead of the removed `--host`, see below. Use `--web-addr=host:port` to set an address on which to serve the Web UI.
### Fixed
- `--check-config` breaking the configuration file ([#4067]).
- Inconsistent application of `--work-dir/-w` ([#2598], [#2902]).
- The order of `-v/--verbose` and `--version` being significant ([#2893]).
### Removed
- The deprecated `--no-mem-optimization` and `--no-etc-hosts` flags.
- `--host` and `-p/--port` flags. Use `--web-addr=host:port` to set an address
on which to serve the Web UI. `-h` is now an alias for `--help`, see above.
- `--host` and `-p/--port` flags. Use `--web-addr=host:port` to set an address on which to serve the Web UI. `-h` is now an alias for `--help`, see above.
[#2598]: https://github.com/AdguardTeam/AdGuardHome/issues/2598
[#2893]: https://github.com/AdguardTeam/AdGuardHome/issues/2893

View File

@@ -1,12 +1,12 @@
package configmgr
import (
"fmt"
"net/netip"
"github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/timeutil"
"github.com/AdguardTeam/golibs/validate"
)
// config is the top-level on-disk configuration structure.
@@ -19,10 +19,10 @@ type config struct {
}
// type check
var _ validator = (*config)(nil)
var _ validate.Interface = (*config)(nil)
// validate implements the [validator] interface for *config.
func (c *config) validate() (err error) {
// Validate implements the [validate.Interface] interface for *config.
func (c *config) Validate() (err error) {
if c == nil {
return errors.ErrNoValue
}
@@ -30,7 +30,7 @@ func (c *config) validate() (err error) {
// TODO(a.garipov): Add more validations.
// Keep this in the same order as the fields in the config.
validators := container.KeyValues[string, validator]{{
validators := container.KeyValues[string, validate.Interface]{{
Key: "dns",
Value: c.DNS,
}, {
@@ -41,14 +41,12 @@ func (c *config) validate() (err error) {
Value: c.Log,
}}
var errs []error
for _, kv := range validators {
err = kv.Value.validate()
if err != nil {
return fmt.Errorf("%s: %w", kv.Key, err)
}
errs = validate.Append(errs, kv.Key, kv.Value)
}
return nil
return errors.Join(errs...)
}
// dnsConfig is the on-disk DNS configuration.
@@ -63,21 +61,19 @@ type dnsConfig struct {
}
// type check
var _ validator = (*dnsConfig)(nil)
var _ validate.Interface = (*dnsConfig)(nil)
// validate implements the [validator] interface for *dnsConfig.
// Validate implements the [validate.Interface] interface for *dnsConfig.
//
// TODO(a.garipov): Add more validations.
func (c *dnsConfig) validate() (err error) {
// TODO(a.garipov): Add more validations.
switch {
case c == nil:
func (c *dnsConfig) Validate() (err error) {
if c == nil {
return errors.ErrNoValue
case c.UpstreamTimeout.Duration <= 0:
return newErrNotPositive("upstream_timeout", c.UpstreamTimeout)
default:
return nil
}
// TODO(a.garipov): Add more validations.
return validate.Positive("upstream_timeout", c.UpstreamTimeout)
}
// httpConfig is the on-disk web API configuration.
@@ -92,20 +88,23 @@ type httpConfig struct {
}
// type check
var _ validator = (*httpConfig)(nil)
var _ validate.Interface = (*httpConfig)(nil)
// validate implements the [validator] interface for *httpConfig.
// Validate implements the [validate.Interface] interface for *httpConfig.
//
// TODO(a.garipov): Add more validations.
func (c *httpConfig) validate() (err error) {
switch {
case c == nil:
func (c *httpConfig) Validate() (err error) {
if c == nil {
return errors.ErrNoValue
case c.Timeout.Duration <= 0:
return newErrNotPositive("timeout", c.Timeout)
default:
return c.Pprof.validate()
}
errs := []error{
validate.Positive("timeout", c.Timeout),
}
errs = validate.Append(errs, "pprof", c.Pprof)
return errors.Join(errs...)
}
// httpPprofConfig is the on-disk pprof configuration.
@@ -115,10 +114,10 @@ type httpPprofConfig struct {
}
// type check
var _ validator = (*httpPprofConfig)(nil)
var _ validate.Interface = (*httpPprofConfig)(nil)
// validate implements the [validator] interface for *httpPprofConfig.
func (c *httpPprofConfig) validate() (err error) {
// Validate implements the [validate.Interface] interface for *httpPprofConfig.
func (c *httpPprofConfig) Validate() (err error) {
if c == nil {
return errors.ErrNoValue
}
@@ -128,17 +127,17 @@ func (c *httpPprofConfig) validate() (err error) {
// logConfig is the on-disk web API configuration.
type logConfig struct {
// TODO(a.garipov): Use.
// TODO(a.garipov): Use.
Verbose bool `yaml:"verbose"`
}
// type check
var _ validator = (*logConfig)(nil)
var _ validate.Interface = (*logConfig)(nil)
// validate implements the [validator] interface for *logConfig.
// Validate implements the [validate.Interface] interface for *logConfig.
//
// TODO(a.garipov): Add more validations.
func (c *logConfig) validate() (err error) {
func (c *logConfig) Validate() (err error) {
if c == nil {
return errors.ErrNoValue
}

View File

@@ -63,7 +63,7 @@ func Validate(fileName string) (err error) {
return err
}
err = conf.validate()
err = conf.Validate()
if err != nil {
return fmt.Errorf("validating config: %w", err)
}
@@ -105,7 +105,7 @@ func New(ctx context.Context, c *Config) (m *Manager, err error) {
return nil, err
}
err = conf.validate()
err = conf.Validate()
if err != nil {
return nil, fmt.Errorf("validating config: %w", err)
}
@@ -162,7 +162,7 @@ func (m *Manager) assemble(
BootstrapServers: conf.DNS.BootstrapDNS,
UpstreamServers: conf.DNS.UpstreamDNS,
DNS64Prefixes: conf.DNS.DNS64Prefixes,
UpstreamTimeout: conf.DNS.UpstreamTimeout.Duration,
UpstreamTimeout: time.Duration(conf.DNS.UpstreamTimeout),
BootstrapPreferIPv6: conf.DNS.BootstrapPreferIPv6,
UseDNS64: conf.DNS.UseDNS64,
}
@@ -185,7 +185,7 @@ func (m *Manager) assemble(
Addresses: conf.HTTP.Addresses,
SecureAddresses: conf.HTTP.SecureAddresses,
OverrideAddress: webAddr,
Timeout: conf.HTTP.Timeout.Duration,
Timeout: time.Duration(conf.HTTP.Timeout),
ForceHTTPS: conf.HTTP.ForceHTTPS,
}
@@ -266,7 +266,7 @@ func (m *Manager) updateCurrentDNS(c *dnssvc.Config) {
m.current.DNS.BootstrapDNS = slices.Clone(c.BootstrapServers)
m.current.DNS.UpstreamDNS = slices.Clone(c.UpstreamServers)
m.current.DNS.DNS64Prefixes = slices.Clone(c.DNS64Prefixes)
m.current.DNS.UpstreamTimeout = timeutil.Duration{Duration: c.UpstreamTimeout}
m.current.DNS.UpstreamTimeout = timeutil.Duration(c.UpstreamTimeout)
m.current.DNS.BootstrapPreferIPv6 = c.BootstrapPreferIPv6
m.current.DNS.UseDNS64 = c.UseDNS64
}
@@ -318,6 +318,6 @@ func (m *Manager) updateCurrentWeb(c *websvc.Config) {
m.current.HTTP.Addresses = slices.Clone(c.Addresses)
m.current.HTTP.SecureAddresses = slices.Clone(c.SecureAddresses)
m.current.HTTP.Timeout = timeutil.Duration{Duration: c.Timeout}
m.current.HTTP.Timeout = timeutil.Duration(c.Timeout)
m.current.HTTP.ForceHTTPS = c.ForceHTTPS
}

View File

@@ -1,31 +0,0 @@
package configmgr
import (
"fmt"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/timeutil"
"golang.org/x/exp/constraints"
)
// validator is the interface for configuration entities that can validate
// themselves.
type validator interface {
// validate returns an error if the entity isn't valid.
validate() (err error)
}
// numberOrDuration is the constraint for integer types along with
// timeutil.Duration.
type numberOrDuration interface {
constraints.Integer | timeutil.Duration
}
// newErrNotPositive returns an error about the value that must be positive but
// isn't. prop is the name of the property to mention in the error message.
//
// TODO(a.garipov): Consider moving such helpers to golibs and use in AdGuardDNS
// as well.
func newErrNotPositive[T numberOrDuration](prop string, v T) (err error) {
return fmt.Errorf("%s: %w, got %v", prop, errors.ErrNotPositive, v)
}

View File

@@ -0,0 +1,43 @@
// Package jsonpatch contains utilities for JSON Merge Patch APIs.
//
// See https://www.rfc-editor.org/rfc/rfc7396.
package jsonpatch
import (
"bytes"
"encoding/json"
"github.com/AdguardTeam/golibs/errors"
)
// NonRemovable is a type that prevents JSON null from being used to try and
// remove a value.
type NonRemovable[T any] struct {
Value T
IsSet bool
}
// type check
var _ json.Unmarshaler = (*NonRemovable[struct{}])(nil)
// UnmarshalJSON implements the [json.Unmarshaler] interface for *NonRemovable.
func (v *NonRemovable[T]) UnmarshalJSON(b []byte) (err error) {
if v == nil {
return errors.Error("jsonpatch.NonRemovable is nil")
}
if bytes.Equal(b, []byte("null")) {
return errors.Error("property cannot be removed")
}
v.IsSet = true
return json.Unmarshal(b, &v.Value)
}
// Set sets ptr if the value has been provided.
func (v NonRemovable[T]) Set(ptr *T) {
if v.IsSet {
*ptr = v.Value
}
}

View File

@@ -0,0 +1,29 @@
package jsonpatch_test
import (
"encoding/json"
"testing"
"github.com/AdguardTeam/AdGuardHome/internal/next/jsonpatch"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
)
func TestNonRemovable(t *testing.T) {
type T struct {
Value jsonpatch.NonRemovable[int] `json:"value"`
}
var v T
err := json.Unmarshal([]byte(`{"value":null}`), &v)
testutil.AssertErrorMsg(t, "property cannot be removed", err)
err = json.Unmarshal([]byte(`{"value":42}`), &v)
assert.NoError(t, err)
var got int
v.Value.Set(&got)
assert.Equal(t, 42, got)
}

View File

@@ -5,10 +5,9 @@ import (
"fmt"
"net/http"
"net/netip"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
"github.com/AdguardTeam/AdGuardHome/internal/next/jsonpatch"
)
// ReqPatchSettingsDNS describes the request to the PATCH /api/v1/settings/dns
@@ -16,13 +15,15 @@ import (
type ReqPatchSettingsDNS struct {
// TODO(a.garipov): Add more as we go.
Addresses []netip.AddrPort `json:"addresses"`
BootstrapServers []string `json:"bootstrap_servers"`
UpstreamServers []string `json:"upstream_servers"`
DNS64Prefixes []netip.Prefix `json:"dns64_prefixes"`
UpstreamTimeout aghhttp.JSONDuration `json:"upstream_timeout"`
BootstrapPreferIPv6 bool `json:"bootstrap_prefer_ipv6"`
UseDNS64 bool `json:"use_dns64"`
Addresses jsonpatch.NonRemovable[[]netip.AddrPort] `json:"addresses"`
BootstrapServers jsonpatch.NonRemovable[[]string] `json:"bootstrap_servers"`
UpstreamServers jsonpatch.NonRemovable[[]string] `json:"upstream_servers"`
DNS64Prefixes jsonpatch.NonRemovable[[]netip.Prefix] `json:"dns64_prefixes"`
UpstreamTimeout jsonpatch.NonRemovable[aghhttp.JSONDuration] `json:"upstream_timeout"`
BootstrapPreferIPv6 jsonpatch.NonRemovable[bool] `json:"bootstrap_prefer_ipv6"`
UseDNS64 jsonpatch.NonRemovable[bool] `json:"use_dns64"`
}
// HTTPAPIDNSSettings are the DNS settings as used by the HTTP API. See the
@@ -42,13 +43,7 @@ type HTTPAPIDNSSettings struct {
// handlePatchSettingsDNS is the handler for the PATCH /api/v1/settings/dns HTTP
// API.
func (svc *Service) handlePatchSettingsDNS(w http.ResponseWriter, r *http.Request) {
req := &ReqPatchSettingsDNS{
Addresses: []netip.AddrPort{},
BootstrapServers: []string{},
UpstreamServers: []string{},
}
// TODO(a.garipov): Validate nulls and proper JSON patch.
req := &ReqPatchSettingsDNS{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
@@ -57,16 +52,20 @@ func (svc *Service) handlePatchSettingsDNS(w http.ResponseWriter, r *http.Reques
return
}
newConf := &dnssvc.Config{
Logger: svc.logger,
Addresses: req.Addresses,
BootstrapServers: req.BootstrapServers,
UpstreamServers: req.UpstreamServers,
DNS64Prefixes: req.DNS64Prefixes,
UpstreamTimeout: time.Duration(req.UpstreamTimeout),
BootstrapPreferIPv6: req.BootstrapPreferIPv6,
UseDNS64: req.UseDNS64,
}
dnsSvc := svc.confMgr.DNS()
newConf := dnsSvc.Config()
// TODO(a.garipov): Add more as we go.
req.Addresses.Set(&newConf.Addresses)
req.BootstrapServers.Set(&newConf.BootstrapServers)
req.UpstreamServers.Set(&newConf.UpstreamServers)
req.DNS64Prefixes.Set(&newConf.DNS64Prefixes)
req.UpstreamTimeout.Set((*aghhttp.JSONDuration)(&newConf.UpstreamTimeout))
req.BootstrapPreferIPv6.Set(&newConf.BootstrapPreferIPv6)
req.UseDNS64.Set(&newConf.UseDNS64)
ctx := r.Context()
err = svc.confMgr.UpdateDNS(ctx, newConf)

View File

@@ -41,7 +41,7 @@ func TestService_HandlePatchSettingsDNS(t *testing.T) {
return nil
},
OnShutdown: func(_ context.Context) (err error) { panic("not implemented") },
OnConfig: func() (c *dnssvc.Config) { panic("not implemented") },
OnConfig: func() (c *dnssvc.Config) { return &dnssvc.Config{} },
}
}
confMgr.onUpdateDNS = func(ctx context.Context, c *dnssvc.Config) (err error) {

View File

@@ -10,6 +10,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/AdGuardHome/internal/next/jsonpatch"
"github.com/AdguardTeam/golibs/logutil/slogutil"
)
@@ -20,9 +21,12 @@ type ReqPatchSettingsHTTP struct {
//
// TODO(a.garipov): Add wait time.
Addresses []netip.AddrPort `json:"addresses"`
SecureAddresses []netip.AddrPort `json:"secure_addresses"`
Timeout aghhttp.JSONDuration `json:"timeout"`
Addresses jsonpatch.NonRemovable[[]netip.AddrPort] `json:"addresses"`
SecureAddresses jsonpatch.NonRemovable[[]netip.AddrPort] `json:"secure_addresses"`
Timeout jsonpatch.NonRemovable[aghhttp.JSONDuration] `json:"timeout"`
ForceHTTPS jsonpatch.NonRemovable[bool] `json:"force_https"`
}
// HTTPAPIHTTPSettings are the HTTP settings as used by the HTTP API. See the
@@ -41,8 +45,6 @@ type HTTPAPIHTTPSettings struct {
func (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Request) {
req := &ReqPatchSettingsHTTP{}
// TODO(a.garipov): Validate nulls and proper JSON patch.
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
aghhttp.WriteJSONResponseError(w, r, fmt.Errorf("decoding: %w", err))
@@ -50,20 +52,14 @@ func (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Reque
return
}
newConf := &Config{
Logger: svc.logger,
Pprof: &PprofConfig{
Port: svc.pprofPort,
Enabled: svc.pprof != nil,
},
ConfigManager: svc.confMgr,
Frontend: svc.frontend,
TLS: svc.tls,
Addresses: req.Addresses,
SecureAddresses: req.SecureAddresses,
Timeout: time.Duration(req.Timeout),
ForceHTTPS: svc.forceHTTPS,
}
newConf := svc.Config()
// TODO(a.garipov): Add more as we go.
req.Addresses.Set(&newConf.Addresses)
req.SecureAddresses.Set(&newConf.SecureAddresses)
req.Timeout.Set((*aghhttp.JSONDuration)(&newConf.Timeout))
req.ForceHTTPS.Set(&newConf.ForceHTTPS)
aghhttp.WriteJSONResponseOK(w, r, &HTTPAPIHTTPSettings{
Addresses: newConf.Addresses,