all: sync with master; upd chlog

This commit is contained in:
Ainar Garipov
2023-04-12 14:48:42 +03:00
parent 0dad53b5f7
commit d9c57cdd9a
181 changed files with 6992 additions and 3430 deletions

View File

@@ -16,6 +16,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/timeutil"
@@ -379,9 +380,9 @@ func (a *Auth) newCookie(req loginJSON, addr string) (c *http.Cookie, err error)
// TODO(a.garipov): Support header Forwarded from RFC 7329.
func realIP(r *http.Request) (ip net.IP, err error) {
proxyHeaders := []string{
"CF-Connecting-IP",
"True-Client-IP",
"X-Real-IP",
httphdr.CFConnectingIP,
httphdr.TrueClientIP,
httphdr.XRealIP,
}
for _, h := range proxyHeaders {
@@ -394,7 +395,7 @@ func realIP(r *http.Request) (ip net.IP, err error) {
// If none of the above yielded any results, get the leftmost IP address
// from the X-Forwarded-For header.
s := r.Header.Get("X-Forwarded-For")
s := r.Header.Get(httphdr.XForwardedFor)
ipStrs := strings.SplitN(s, ", ", 2)
ip = net.ParseIP(ipStrs[0])
if ip != nil {
@@ -411,6 +412,21 @@ func realIP(r *http.Request) (ip net.IP, err error) {
return net.ParseIP(ipStr), nil
}
// writeErrorWithIP is like [aghhttp.Error], but includes the remote IP address
// when it writes to the log.
func writeErrorWithIP(
r *http.Request,
w http.ResponseWriter,
code int,
remoteIP string,
format string,
args ...any,
) {
text := fmt.Sprintf(format, args...)
log.Error("%s %s %s: from ip %s: %s", r.Method, r.Host, r.URL, remoteIP, text)
http.Error(w, text, code)
}
func handleLogin(w http.ResponseWriter, r *http.Request) {
req := loginJSON{}
err := json.NewDecoder(r.Body).Decode(&req)
@@ -420,31 +436,45 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
return
}
var remoteAddr string
var remoteIP string
// realIP cannot be used here without taking TrustedProxies into account due
// to security issues.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/2799.
//
// TODO(e.burkov): Use realIP when the issue will be fixed.
if remoteAddr, err = netutil.SplitHost(r.RemoteAddr); err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "auth: getting remote address: %s", err)
if remoteIP, err = netutil.SplitHost(r.RemoteAddr); err != nil {
writeErrorWithIP(
r,
w,
http.StatusBadRequest,
r.RemoteAddr,
"auth: getting remote address: %s",
err,
)
return
}
if rateLimiter := Context.auth.raleLimiter; rateLimiter != nil {
if left := rateLimiter.check(remoteAddr); left > 0 {
w.Header().Set("Retry-After", strconv.Itoa(int(left.Seconds())))
aghhttp.Error(r, w, http.StatusTooManyRequests, "auth: blocked for %s", left)
if left := rateLimiter.check(remoteIP); left > 0 {
w.Header().Set(httphdr.RetryAfter, strconv.Itoa(int(left.Seconds())))
writeErrorWithIP(
r,
w,
http.StatusTooManyRequests,
remoteIP,
"auth: blocked for %s",
left,
)
return
}
}
cookie, err := Context.auth.newCookie(req, remoteAddr)
cookie, err := Context.auth.newCookie(req, remoteIP)
if err != nil {
aghhttp.Error(r, w, http.StatusForbidden, "%s", err)
writeErrorWithIP(r, w, http.StatusForbidden, remoteIP, "%s", err)
return
}
@@ -452,10 +482,7 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
// Use realIP here, since this IP address is only used for logging.
ip, err := realIP(r)
if err != nil {
log.Error("auth: getting real ip from request: %s", err)
} else if ip == nil {
// Technically shouldn't happen.
log.Error("auth: unknown ip")
log.Error("auth: getting real ip from request with remote ip %s: %s", remoteIP, err)
}
log.Info("auth: user %q successfully logged in from ip %v", req.Name, ip)
@@ -463,9 +490,9 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, cookie)
h := w.Header()
h.Set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate")
h.Set("Pragma", "no-cache")
h.Set("Expires", "0")
h.Set(httphdr.CacheControl, "no-store, no-cache, must-revalidate, proxy-revalidate")
h.Set(httphdr.Pragma, "no-cache")
h.Set(httphdr.Expires, "0")
aghhttp.OK(w)
}
@@ -476,7 +503,7 @@ func handleLogout(w http.ResponseWriter, r *http.Request) {
if err != nil {
// The only error that is returned from r.Cookie is [http.ErrNoCookie].
// The user is already logged out.
respHdr.Set("Location", "/login.html")
respHdr.Set(httphdr.Location, "/login.html")
w.WriteHeader(http.StatusFound)
return
@@ -494,8 +521,8 @@ func handleLogout(w http.ResponseWriter, r *http.Request) {
SameSite: http.SameSiteLaxMode,
}
respHdr.Set("Location", "/login.html")
respHdr.Set("Set-Cookie", c.String())
respHdr.Set(httphdr.Location, "/login.html")
respHdr.Set(httphdr.SetCookie, c.String())
w.WriteHeader(http.StatusFound)
}
@@ -543,8 +570,7 @@ func optionalAuthThird(w http.ResponseWriter, r *http.Request) (mustAuth bool) {
log.Debug("auth: redirected to login page by GL-Inet submodule")
} else {
log.Debug("auth: redirected to login page")
w.Header().Set("Location", "/login.html")
w.WriteHeader(http.StatusFound)
http.Redirect(w, r, "login.html", http.StatusFound)
}
} else {
log.Debug("auth: responded with forbidden to %s %s", r.Method, p)
@@ -569,8 +595,7 @@ func optionalAuth(
// Redirect to the dashboard if already authenticated.
res := Context.auth.checkSession(cookie.Value)
if res == checkSessionOK {
w.Header().Set("Location", "/")
w.WriteHeader(http.StatusFound)
http.Redirect(w, r, "", http.StatusFound)
return
}

View File

@@ -12,6 +12,7 @@ import (
"testing"
"time"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -135,11 +136,11 @@ func TestAuthHTTP(t *testing.T) {
handlerCalled = false
handler2(&w, &r)
assert.Equal(t, http.StatusFound, w.statusCode)
assert.NotEmpty(t, w.hdr.Get("Location"))
assert.NotEmpty(t, w.hdr.Get(httphdr.Location))
assert.False(t, handlerCalled)
// go to login page
loginURL := w.hdr.Get("Location")
loginURL := w.hdr.Get(httphdr.Location)
r.URL = &url.URL{Path: loginURL}
handlerCalled = false
handler2(&w, &r)
@@ -153,13 +154,13 @@ func TestAuthHTTP(t *testing.T) {
// get /
handler2 = optionalAuth(handler)
w.hdr = make(http.Header)
r.Header.Set("Cookie", cookie.String())
r.Header.Set(httphdr.Cookie, cookie.String())
r.URL = &url.URL{Path: "/"}
handlerCalled = false
handler2(&w, &r)
assert.True(t, handlerCalled)
r.Header.Del("Cookie")
r.Header.Del(httphdr.Cookie)
// get / with basic auth
handler2 = optionalAuth(handler)
@@ -169,28 +170,28 @@ func TestAuthHTTP(t *testing.T) {
handlerCalled = false
handler2(&w, &r)
assert.True(t, handlerCalled)
r.Header.Del("Authorization")
r.Header.Del(httphdr.Authorization)
// get login page with a valid cookie - we're redirected to /
handler2 = optionalAuth(handler)
w.hdr = make(http.Header)
r.Header.Set("Cookie", cookie.String())
r.Header.Set(httphdr.Cookie, cookie.String())
r.URL = &url.URL{Path: loginURL}
handlerCalled = false
handler2(&w, &r)
assert.NotEmpty(t, w.hdr.Get("Location"))
assert.NotEmpty(t, w.hdr.Get(httphdr.Location))
assert.False(t, handlerCalled)
r.Header.Del("Cookie")
r.Header.Del(httphdr.Cookie)
// get login page with an invalid cookie
handler2 = optionalAuth(handler)
w.hdr = make(http.Header)
r.Header.Set("Cookie", "bad")
r.Header.Set(httphdr.Cookie, "bad")
r.URL = &url.URL{Path: loginURL}
handlerCalled = false
handler2(&w, &r)
assert.True(t, handlerCalled)
r.Header.Del("Cookie")
r.Header.Del(httphdr.Cookie)
Context.auth.Close()
}
@@ -213,7 +214,7 @@ func TestRealIP(t *testing.T) {
}, {
name: "success_proxy",
header: http.Header{
textproto.CanonicalMIMEHeaderKey("X-Real-IP"): []string{"1.2.3.5"},
textproto.CanonicalMIMEHeaderKey(httphdr.XRealIP): []string{"1.2.3.5"},
},
remoteAddr: remoteAddr,
wantErrMsg: "",
@@ -221,7 +222,7 @@ func TestRealIP(t *testing.T) {
}, {
name: "success_proxy_multiple",
header: http.Header{
textproto.CanonicalMIMEHeaderKey("X-Forwarded-For"): []string{
textproto.CanonicalMIMEHeaderKey(httphdr.XForwardedFor): []string{
"1.2.3.6, 1.2.3.5",
},
},

View File

@@ -10,8 +10,8 @@ import (
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghio"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/log"
"github.com/josharian/native"
)
// GLMode - enable GL-Inet compatibility mode
@@ -102,7 +102,7 @@ func glGetTokenDate(file string) uint32 {
buf := bytes.NewBuffer(bs)
err = binary.Read(buf, aghos.NativeEndian, &dateToken)
err = binary.Read(buf, native.Endian, &dateToken)
if err != nil {
log.Error("decoding token: %s", err)

View File

@@ -6,7 +6,7 @@ import (
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/josharian/native"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -19,13 +19,13 @@ func TestAuthGL(t *testing.T) {
glFilePrefix = dir + "/gl_token_"
data := make([]byte, 4)
aghos.NativeEndian.PutUint32(data, 1)
native.Endian.PutUint32(data, 1)
require.NoError(t, os.WriteFile(glFilePrefix+"test", data, 0o644))
assert.False(t, glCheckToken("test"))
data = make([]byte, 4)
aghos.NativeEndian.PutUint32(data, uint32(time.Now().UTC().Unix()+60))
native.Endian.PutUint32(data, uint32(time.Now().UTC().Unix()+60))
require.NoError(t, os.WriteFile(glFilePrefix+"test", data, 0o644))
r, _ := http.NewRequest(http.MethodGet, "http://localhost/", nil)

View File

@@ -3,7 +3,10 @@ package home
import (
"encoding"
"fmt"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
"github.com/AdguardTeam/dnsproxy/proxy"
)
@@ -15,6 +18,9 @@ type Client struct {
// these upstream must be used.
upstreamConfig *proxy.UpstreamConfig
safeSearchConf filtering.SafeSearchConfig
SafeSearch filtering.SafeSearch
Name string
IDs []string
@@ -24,10 +30,11 @@ type Client struct {
UseOwnSettings bool
FilteringEnabled bool
SafeSearchEnabled bool
SafeBrowsingEnabled bool
ParentalEnabled bool
UseOwnBlockedServices bool
IgnoreQueryLog bool
IgnoreStatistics bool
}
// closeUpstreams closes the client-specific upstream config of c if any.
@@ -42,6 +49,23 @@ func (c *Client) closeUpstreams() (err error) {
return nil
}
// setSafeSearch initializes and sets the safe search filter for this client.
func (c *Client) setSafeSearch(
conf filtering.SafeSearchConfig,
cacheSize uint,
cacheTTL time.Duration,
) (err error) {
ss, err := safesearch.NewDefault(conf, fmt.Sprintf("client %q", c.Name), cacheSize, cacheTTL)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
c.SafeSearch = ss
return nil
}
// clientSource represents the source from which the information about the
// client has been obtained.
type clientSource uint

View File

@@ -18,7 +18,6 @@ import (
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/stringutil"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
@@ -52,9 +51,17 @@ type clientsContainer struct {
// lock protects all fields.
//
// TODO(a.garipov): Use a pointer and describe which fields are protected in
// more detail.
// more detail. Use sync.RWMutex.
lock sync.Mutex
// safeSearchCacheSize is the size of the safe search cache to use for
// persistent clients.
safeSearchCacheSize uint
// safeSearchCacheTTL is the TTL of the safe search cache to use for
// persistent clients.
safeSearchCacheTTL time.Duration
// testing is a flag that disables some features for internal tests.
//
// TODO(a.garipov): Awful. Remove.
@@ -69,10 +76,12 @@ func (clients *clientsContainer) Init(
dhcpServer dhcpd.Interface,
etcHosts *aghnet.HostsContainer,
arpdb aghnet.ARPDB,
filteringConf *filtering.Config,
) {
if clients.list != nil {
log.Fatal("clients.list != nil")
}
clients.list = make(map[string]*Client)
clients.idIndex = make(map[string]*Client)
clients.ipToRC = map[netip.Addr]*RuntimeClient{}
@@ -82,7 +91,10 @@ func (clients *clientsContainer) Init(
clients.dhcpServer = dhcpServer
clients.etcHosts = etcHosts
clients.arpdb = arpdb
clients.addFromConfig(objects)
clients.addFromConfig(objects, filteringConf)
clients.safeSearchCacheSize = filteringConf.SafeSearchCacheSize
clients.safeSearchCacheTTL = time.Minute * time.Duration(filteringConf.CacheTime)
if clients.testing {
return
@@ -133,6 +145,8 @@ func (clients *clientsContainer) reloadARP() {
// clientObject is the YAML representation of a persistent client.
type clientObject struct {
SafeSearchConf filtering.SafeSearchConfig `yaml:"safe_search"`
Name string `yaml:"name"`
Tags []string `yaml:"tags"`
@@ -143,14 +157,16 @@ type clientObject struct {
UseGlobalSettings bool `yaml:"use_global_settings"`
FilteringEnabled bool `yaml:"filtering_enabled"`
ParentalEnabled bool `yaml:"parental_enabled"`
SafeSearchEnabled bool `yaml:"safesearch_enabled"`
SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"`
UseGlobalBlockedServices bool `yaml:"use_global_blocked_services"`
IgnoreQueryLog bool `yaml:"ignore_querylog"`
IgnoreStatistics bool `yaml:"ignore_statistics"`
}
// addFromConfig initializes the clients container with objects from the
// configuration file.
func (clients *clientsContainer) addFromConfig(objects []*clientObject) {
func (clients *clientsContainer) addFromConfig(objects []*clientObject, filteringConf *filtering.Config) {
for _, o := range objects {
cli := &Client{
Name: o.Name,
@@ -161,9 +177,26 @@ func (clients *clientsContainer) addFromConfig(objects []*clientObject) {
UseOwnSettings: !o.UseGlobalSettings,
FilteringEnabled: o.FilteringEnabled,
ParentalEnabled: o.ParentalEnabled,
SafeSearchEnabled: o.SafeSearchEnabled,
safeSearchConf: o.SafeSearchConf,
SafeBrowsingEnabled: o.SafeBrowsingEnabled,
UseOwnBlockedServices: !o.UseGlobalBlockedServices,
IgnoreQueryLog: o.IgnoreQueryLog,
IgnoreStatistics: o.IgnoreStatistics,
}
if o.SafeSearchConf.Enabled {
o.SafeSearchConf.CustomResolver = safeSearchResolver{}
err := cli.setSafeSearch(
o.SafeSearchConf,
filteringConf.SafeSearchCacheSize,
time.Minute*time.Duration(filteringConf.CacheTime),
)
if err != nil {
log.Error("clients: init client safesearch %q: %s", cli.Name, err)
continue
}
}
for _, s := range o.BlockedServices {
@@ -210,9 +243,11 @@ func (clients *clientsContainer) forConfig() (objs []*clientObject) {
UseGlobalSettings: !cli.UseOwnSettings,
FilteringEnabled: cli.FilteringEnabled,
ParentalEnabled: cli.ParentalEnabled,
SafeSearchEnabled: cli.SafeSearchEnabled,
SafeSearchConf: cli.safeSearchConf,
SafeBrowsingEnabled: cli.SafeBrowsingEnabled,
UseGlobalBlockedServices: !cli.UseOwnBlockedServices,
IgnoreQueryLog: cli.IgnoreQueryLog,
IgnoreStatistics: cli.IgnoreStatistics,
}
objs = append(objs, o)
@@ -324,7 +359,8 @@ func (clients *clientsContainer) clientOrArtificial(
client, ok := clients.Find(id)
if ok {
return &querylog.Client{
Name: client.Name,
Name: client.Name,
IgnoreQueryLog: client.IgnoreQueryLog,
}, false
}
@@ -359,6 +395,20 @@ func (clients *clientsContainer) Find(id string) (c *Client, ok bool) {
return c, true
}
// shouldCountClient is a wrapper around Find to make it a valid client
// information finder for the statistics. If no information about the client
// is found, it returns true.
func (clients *clientsContainer) shouldCountClient(ids []string) (y bool) {
for _, id := range ids {
client, ok := clients.Find(id)
if ok {
return !client.IgnoreStatistics
}
}
return true
}
// findUpstreams returns upstreams configured for the client, identified either
// by its IP address or its ClientID. upsConf is nil if the client isn't found
// or if the client has no custom upstreams.
@@ -389,6 +439,7 @@ func (clients *clientsContainer) findUpstreams(
Bootstrap: config.DNS.BootstrapDNS,
Timeout: config.DNS.UpstreamTimeout.Duration,
HTTPVersions: dnsforward.UpstreamHTTPVersions(config.DNS.UseHTTP3Upstreams),
PreferIPv6: config.DNS.BootstrapPreferIPv6,
},
)
if err != nil {
@@ -839,15 +890,7 @@ func (clients *clientsContainer) updateFromDHCP(add bool) {
continue
}
// TODO(a.garipov): Remove once we switch to netip.Addr more fully.
ipAddr, err := netutil.IPToAddrNoMapped(l.IP)
if err != nil {
log.Error("clients: bad client ip %v from dhcp: %s", l.IP, err)
continue
}
ok := clients.addHostLocked(ipAddr, l.Hostname, ClientSourceDHCP)
ok := clients.addHostLocked(l.IP, l.Hostname, ClientSourceDHCP)
if ok {
n++
}

View File

@@ -9,17 +9,27 @@ import (
"time"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestClients(t *testing.T) {
clients := clientsContainer{}
clients.testing = true
// newClientsContainer is a helper that creates a new clients container for
// tests.
func newClientsContainer() (c *clientsContainer) {
c = &clientsContainer{
testing: true,
}
clients.Init(nil, nil, nil, nil)
c.Init(nil, nil, nil, nil, &filtering.Config{})
return c
}
func TestClients(t *testing.T) {
clients := newClientsContainer()
t.Run("add_success", func(t *testing.T) {
var (
@@ -198,10 +208,7 @@ func TestClients(t *testing.T) {
}
func TestClientsWHOIS(t *testing.T) {
clients := clientsContainer{
testing: true,
}
clients.Init(nil, nil, nil, nil)
clients := newClientsContainer()
whois := &RuntimeClientWHOISInfo{
Country: "AU",
Orgname: "Example Org",
@@ -247,10 +254,7 @@ func TestClientsWHOIS(t *testing.T) {
}
func TestClientsAddExisting(t *testing.T) {
clients := clientsContainer{
testing: true,
}
clients.Init(nil, nil, nil, nil)
clients := newClientsContainer()
t.Run("simple", func(t *testing.T) {
ip := netip.MustParseAddr("1.1.1.1")
@@ -275,7 +279,7 @@ func TestClientsAddExisting(t *testing.T) {
t.Skip("skipping dhcp test on windows")
}
ip := net.IP{1, 2, 3, 4}
ip := netip.MustParseAddr("1.2.3.4")
// First, init a DHCP server with a single static lease.
config := &dhcpd.ServerConfig{
@@ -325,10 +329,7 @@ func TestClientsAddExisting(t *testing.T) {
}
func TestClientsCustomUpstream(t *testing.T) {
clients := clientsContainer{
testing: true,
}
clients.Init(nil, nil, nil, nil)
clients := newClientsContainer()
// Add client with upstreams.
ok, err := clients.Add(&Client{

View File

@@ -7,6 +7,7 @@ import (
"net/netip"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
)
// clientJSON is a common structure used by several handlers to deal with
@@ -26,7 +27,8 @@ type clientJSON struct {
// the allowlist.
DisallowedRule *string `json:"disallowed_rule,omitempty"`
WHOISInfo *RuntimeClientWHOISInfo `json:"whois_info,omitempty"`
WHOISInfo *RuntimeClientWHOISInfo `json:"whois_info,omitempty"`
SafeSearchConf *filtering.SafeSearchConfig `json:"safe_search"`
Name string `json:"name"`
@@ -35,9 +37,10 @@ type clientJSON struct {
Tags []string `json:"tags"`
Upstreams []string `json:"upstreams"`
FilteringEnabled bool `json:"filtering_enabled"`
ParentalEnabled bool `json:"parental_enabled"`
SafeBrowsingEnabled bool `json:"safebrowsing_enabled"`
FilteringEnabled bool `json:"filtering_enabled"`
ParentalEnabled bool `json:"parental_enabled"`
SafeBrowsingEnabled bool `json:"safebrowsing_enabled"`
// Deprecated: use safeSearchConf.
SafeSearchEnabled bool `json:"safesearch_enabled"`
UseGlobalBlockedServices bool `json:"use_global_blocked_services"`
UseGlobalSettings bool `json:"use_global_settings"`
@@ -46,8 +49,8 @@ type clientJSON struct {
type runtimeClientJSON struct {
WHOISInfo *RuntimeClientWHOISInfo `json:"whois_info"`
Name string `json:"name"`
IP netip.Addr `json:"ip"`
Name string `json:"name"`
Source clientSource `json:"source"`
}
@@ -57,7 +60,7 @@ type clientListJSON struct {
Tags []string `json:"supported_tags"`
}
// respond with information about configured clients
// handleGetClients is the handler for GET /control/clients HTTP API.
func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http.Request) {
data := clientListJSON{}
@@ -86,27 +89,67 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http
_ = aghhttp.WriteJSONResponse(w, r, data)
}
// Convert JSON object to Client object
func jsonToClient(cj clientJSON) (c *Client) {
return &Client{
Name: cj.Name,
IDs: cj.IDs,
Tags: cj.Tags,
UseOwnSettings: !cj.UseGlobalSettings,
FilteringEnabled: cj.FilteringEnabled,
ParentalEnabled: cj.ParentalEnabled,
SafeSearchEnabled: cj.SafeSearchEnabled,
SafeBrowsingEnabled: cj.SafeBrowsingEnabled,
// jsonToClient converts JSON object to Client object.
func (clients *clientsContainer) jsonToClient(cj clientJSON) (c *Client, err error) {
var safeSearchConf filtering.SafeSearchConfig
if cj.SafeSearchConf != nil {
safeSearchConf = *cj.SafeSearchConf
} else {
// TODO(d.kolyshev): Remove after cleaning the deprecated
// [clientJSON.SafeSearchEnabled] field.
safeSearchConf = filtering.SafeSearchConfig{
Enabled: cj.SafeSearchEnabled,
}
UseOwnBlockedServices: !cj.UseGlobalBlockedServices,
BlockedServices: cj.BlockedServices,
Upstreams: cj.Upstreams,
// Set default service flags for enabled safesearch.
if safeSearchConf.Enabled {
safeSearchConf.Bing = true
safeSearchConf.DuckDuckGo = true
safeSearchConf.Google = true
safeSearchConf.Pixabay = true
safeSearchConf.Yandex = true
safeSearchConf.YouTube = true
}
}
c = &Client{
safeSearchConf: safeSearchConf,
Name: cj.Name,
IDs: cj.IDs,
Tags: cj.Tags,
BlockedServices: cj.BlockedServices,
Upstreams: cj.Upstreams,
UseOwnSettings: !cj.UseGlobalSettings,
FilteringEnabled: cj.FilteringEnabled,
ParentalEnabled: cj.ParentalEnabled,
SafeBrowsingEnabled: cj.SafeBrowsingEnabled,
UseOwnBlockedServices: !cj.UseGlobalBlockedServices,
}
if safeSearchConf.Enabled {
err = c.setSafeSearch(
safeSearchConf,
clients.safeSearchCacheSize,
clients.safeSearchCacheTTL,
)
if err != nil {
return nil, fmt.Errorf("creating safesearch for client %q: %w", c.Name, err)
}
}
return c, nil
}
// Convert Client object to JSON
// clientToJSON converts Client object to JSON.
func clientToJSON(c *Client) (cj *clientJSON) {
// TODO(d.kolyshev): Remove after cleaning the deprecated
// [clientJSON.SafeSearchEnabled] field.
cloneVal := c.safeSearchConf
safeSearchConf := &cloneVal
return &clientJSON{
Name: c.Name,
IDs: c.IDs,
@@ -114,7 +157,8 @@ func clientToJSON(c *Client) (cj *clientJSON) {
UseGlobalSettings: !c.UseOwnSettings,
FilteringEnabled: c.FilteringEnabled,
ParentalEnabled: c.ParentalEnabled,
SafeSearchEnabled: c.SafeSearchEnabled,
SafeSearchEnabled: safeSearchConf.Enabled,
SafeSearchConf: safeSearchConf,
SafeBrowsingEnabled: c.SafeBrowsingEnabled,
UseGlobalBlockedServices: !c.UseOwnBlockedServices,
@@ -124,7 +168,7 @@ func clientToJSON(c *Client) (cj *clientJSON) {
}
}
// Add a new client
// handleAddClient is the handler for POST /control/clients/add HTTP API.
func (clients *clientsContainer) handleAddClient(w http.ResponseWriter, r *http.Request) {
cj := clientJSON{}
err := json.NewDecoder(r.Body).Decode(&cj)
@@ -134,7 +178,13 @@ func (clients *clientsContainer) handleAddClient(w http.ResponseWriter, r *http.
return
}
c := jsonToClient(cj)
c, err := clients.jsonToClient(cj)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
return
}
ok, err := clients.Add(c)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
@@ -151,7 +201,7 @@ func (clients *clientsContainer) handleAddClient(w http.ResponseWriter, r *http.
onConfigModified()
}
// Remove client
// handleDelClient is the handler for POST /control/clients/delete HTTP API.
func (clients *clientsContainer) handleDelClient(w http.ResponseWriter, r *http.Request) {
cj := clientJSON{}
err := json.NewDecoder(r.Body).Decode(&cj)
@@ -181,7 +231,7 @@ type updateJSON struct {
Data clientJSON `json:"data"`
}
// Update client's properties
// handleUpdateClient is the handler for POST /control/clients/update HTTP API.
func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *http.Request) {
dj := updateJSON{}
err := json.NewDecoder(r.Body).Decode(&dj)
@@ -197,7 +247,13 @@ func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *ht
return
}
c := jsonToClient(dj.Data)
c, err := clients.jsonToClient(dj.Data)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
return
}
err = clients.Update(dj.Name, c)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
@@ -208,7 +264,7 @@ func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *ht
onConfigModified()
}
// Get the list of clients by IP address list
// handleFindClient is the handler for GET /control/clients/find HTTP API.
func (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
data := []map[string]*clientJSON{}

View File

@@ -228,34 +228,32 @@ type tlsConfigSettings struct {
}
type queryLogConfig struct {
// Ignored is the list of host names, which should not be written to log.
Ignored []string `yaml:"ignored"`
// Interval is the interval for query log's files rotation.
Interval timeutil.Duration `yaml:"interval"`
// MemSize is the number of entries kept in memory before they are flushed
// to disk.
MemSize uint32 `yaml:"size_memory"`
// Enabled defines if the query log is enabled.
Enabled bool `yaml:"enabled"`
// FileEnabled defines, if the query log is written to the file.
FileEnabled bool `yaml:"file_enabled"`
// Interval is the interval for query log's files rotation.
Interval timeutil.Duration `yaml:"interval"`
// MemSize is the number of entries kept in memory before they are
// flushed to disk.
MemSize uint32 `yaml:"size_memory"`
// Ignored is the list of host names, which should not be written to
// log.
Ignored []string `yaml:"ignored"`
}
type statsConfig struct {
// Enabled defines if the statistics are enabled.
Enabled bool `yaml:"enabled"`
// Interval is the time interval for flushing statistics to the disk in
// days.
Interval uint32 `yaml:"interval"`
// Ignored is the list of host names, which should not be counted.
Ignored []string `yaml:"ignored"`
// Interval is the retention interval for statistics.
Interval timeutil.Duration `yaml:"interval"`
// Enabled defines if the statistics are enabled.
Enabled bool `yaml:"enabled"`
}
// config is the global configuration structure.
@@ -286,7 +284,7 @@ var config = &configuration{
CacheSize: 4 * 1024 * 1024,
EDNSClientSubnet: &dnsforward.EDNSClientSubnet{
CustomIP: "",
CustomIP: netip.Addr{},
Enabled: false,
UseCustom: false,
},
@@ -322,7 +320,7 @@ var config = &configuration{
},
Stats: statsConfig{
Enabled: true,
Interval: 1,
Interval: timeutil.Duration{Duration: 1 * timeutil.Day},
Ignored: []string{},
},
// NOTE: Keep these parameters in sync with the one put into
@@ -503,7 +501,7 @@ func (c *configuration) write() (err error) {
if Context.stats != nil {
statsConf := stats.Config{}
Context.stats.WriteDiskConfig(&statsConf)
config.Stats.Interval = statsConf.LimitDays
config.Stats.Interval = timeutil.Duration{Duration: statsConf.Limit}
config.Stats.Enabled = statsConf.Enabled
config.Stats.Ignored = statsConf.Ignored.Values()
slices.Sort(config.Stats.Ignored)

View File

@@ -13,7 +13,9 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/version"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/mathutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/NYTimes/gziphandler"
)
@@ -97,12 +99,17 @@ func collectDNSAddresses() (addrs []string, err error) {
// statusResponse is a response for /control/status endpoint.
type statusResponse struct {
Version string `json:"version"`
Language string `json:"language"`
DNSAddrs []string `json:"dns_addresses"`
DNSPort int `json:"dns_port"`
HTTPPort int `json:"http_port"`
IsProtectionEnabled bool `json:"protection_enabled"`
Version string `json:"version"`
Language string `json:"language"`
DNSAddrs []string `json:"dns_addresses"`
DNSPort int `json:"dns_port"`
HTTPPort int `json:"http_port"`
// ProtectionDisabledDuration is the duration of the protection pause in
// milliseconds.
ProtectionDisabledDuration int64 `json:"protection_disabled_duration"`
ProtectionEnabled bool `json:"protection_enabled"`
// TODO(e.burkov): Inspect if front-end doesn't requires this field as
// openapi.yaml declares.
IsDHCPAvailable bool `json:"dhcp_available"`
@@ -119,28 +126,45 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
return
}
var (
fltConf *dnsforward.FilteringConfig
protectionDisabledUntil *time.Time
protectionEnabled bool
)
if Context.dnsServer != nil {
fltConf = &dnsforward.FilteringConfig{}
Context.dnsServer.WriteDiskConfig(fltConf)
protectionEnabled, protectionDisabledUntil = Context.dnsServer.UpdatedProtectionStatus()
}
var resp statusResponse
func() {
config.RLock()
defer config.RUnlock()
var protectionDisabledDuration int64
if protectionDisabledUntil != nil {
// Make sure that we don't send negative numbers to the frontend,
// since enough time might have passed to make the difference less
// than zero.
protectionDisabledDuration = mathutil.Max(
0,
time.Until(*protectionDisabledUntil).Milliseconds(),
)
}
resp = statusResponse{
Version: version.Version(),
DNSAddrs: dnsAddrs,
DNSPort: config.DNS.Port,
HTTPPort: config.BindPort,
Language: config.Language,
IsRunning: isRunning(),
Version: version.Version(),
Language: config.Language,
DNSAddrs: dnsAddrs,
DNSPort: config.DNS.Port,
HTTPPort: config.BindPort,
ProtectionDisabledDuration: protectionDisabledDuration,
ProtectionEnabled: protectionEnabled,
IsRunning: isRunning(),
}
}()
var c *dnsforward.FilteringConfig
if Context.dnsServer != nil {
c = &dnsforward.FilteringConfig{}
Context.dnsServer.WriteDiskConfig(c)
resp.IsProtectionEnabled = c.ProtectionEnabled
}
// IsDHCPAvailable field is now false by default for Windows.
if runtime.GOOS != "windows" {
resp.IsDHCPAvailable = Context.dhcpServer != nil
@@ -219,7 +243,7 @@ func modifiesData(m string) (ok bool) {
func ensureContentType(w http.ResponseWriter, r *http.Request) (ok bool) {
const statusUnsup = http.StatusUnsupportedMediaType
cType := r.Header.Get(aghhttp.HdrNameContentType)
cType := r.Header.Get(httphdr.ContentType)
if r.ContentLength == 0 {
if cType == "" {
return true
@@ -308,13 +332,17 @@ func handleHTTPSRedirect(w http.ResponseWriter, r *http.Request) (ok bool) {
return false
}
var serveHTTP3 bool
var portHTTPS int
var (
forceHTTPS bool
serveHTTP3 bool
portHTTPS int
)
func() {
config.RLock()
defer config.RUnlock()
serveHTTP3, portHTTPS = config.DNS.ServeHTTP3, config.TLS.PortHTTPS
forceHTTPS = config.TLS.ForceHTTPS && config.TLS.Enabled && config.TLS.PortHTTPS != 0
}()
respHdr := w.Header()
@@ -327,13 +355,13 @@ func handleHTTPSRedirect(w http.ResponseWriter, r *http.Request) (ok bool) {
// default is 24 hours.
if serveHTTP3 {
altSvc := fmt.Sprintf(`h3=":%d"`, portHTTPS)
respHdr.Set(aghhttp.HdrNameAltSvc, altSvc)
respHdr.Set(httphdr.AltSvc, altSvc)
}
if r.TLS == nil && web.forceHTTPS {
if r.TLS == nil && forceHTTPS {
hostPort := host
if port := web.conf.PortHTTPS; port != defaultPortHTTPS {
hostPort = netutil.JoinHostPort(host, port)
if portHTTPS != defaultPortHTTPS {
hostPort = netutil.JoinHostPort(host, portHTTPS)
}
httpsURL := &url.URL{
@@ -357,8 +385,8 @@ func handleHTTPSRedirect(w http.ResponseWriter, r *http.Request) (ok bool) {
Host: r.Host,
}
respHdr.Set(aghhttp.HdrNameAccessControlAllowOrigin, originURL.String())
respHdr.Set(aghhttp.HdrNameVary, aghhttp.HdrNameOrigin)
respHdr.Set(httphdr.AccessControlAllowOrigin, originURL.String())
respHdr.Set(httphdr.Vary, httphdr.Origin)
return true
}
@@ -371,7 +399,7 @@ func postInstall(handler func(http.ResponseWriter, *http.Request)) func(http.Res
path := r.URL.Path
if Context.firstRun && !strings.HasPrefix(path, "/install.") &&
!strings.HasPrefix(path, "/assets/") {
http.Redirect(w, r, "/install.html", http.StatusFound)
http.Redirect(w, r, "install.html", http.StatusFound)
return
}

View File

@@ -39,7 +39,7 @@ type getAddrsResponse struct {
}
// handleInstallGetAddresses is the handler for /install/get_addresses endpoint.
func (web *Web) handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) {
func (web *webAPI) handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) {
data := getAddrsResponse{
Version: version.Version(),
@@ -167,7 +167,7 @@ func (req *checkConfReq) validateDNS(
}
// handleInstallCheckConfig handles the /check_config endpoint.
func (web *Web) handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) {
func (web *webAPI) handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) {
req := &checkConfReq{}
err := json.NewDecoder(r.Body).Decode(req)
@@ -375,7 +375,7 @@ func shutdownSrv3(srv *http3.Server) {
const PasswordMinRunes = 8
// Apply new configuration, start DNS server, restart Web server
func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
func (web *webAPI) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
req, restartHTTP, err := decodeApplyConfigReq(r.Body)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
@@ -503,7 +503,7 @@ func decodeApplyConfigReq(r io.Reader) (req *applyConfigReq, restartHTTP bool, e
return req, restartHTTP, err
}
func (web *Web) registerInstallHandlers() {
func (web *webAPI) registerInstallHandlers() {
Context.mux.HandleFunc("/control/install/get_addresses", preInstall(ensureGET(web.handleInstallGetAddresses)))
Context.mux.HandleFunc("/control/install/check_config", preInstall(ensurePOST(web.handleInstallCheckConfig)))
Context.mux.HandleFunc("/control/install/configure", preInstall(ensurePOST(web.handleInstallConfigure)))

View File

@@ -1,13 +1,13 @@
package home
import (
"context"
"fmt"
"net"
"net/netip"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
@@ -21,7 +21,6 @@ import (
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/stringutil"
"github.com/ameshkov/dnscrypt/v2"
yaml "gopkg.in/yaml.v3"
)
@@ -52,14 +51,15 @@ func initDNS() (err error) {
anonymizer := config.anonymizer()
statsConf := stats.Config{
Filename: filepath.Join(baseDir, "stats.db"),
LimitDays: config.Stats.Interval,
ConfigModified: onConfigModified,
HTTPRegister: httpRegister,
Enabled: config.Stats.Enabled,
Filename: filepath.Join(baseDir, "stats.db"),
Limit: config.Stats.Interval.Duration,
ConfigModified: onConfigModified,
HTTPRegister: httpRegister,
Enabled: config.Stats.Enabled,
ShouldCountClient: Context.clients.shouldCountClient,
}
set, err := nonDupEmptyHostNames(config.Stats.Ignored)
set, err := aghnet.NewDomainNameSet(config.Stats.Ignored)
if err != nil {
return fmt.Errorf("statistics: ignored list: %w", err)
}
@@ -83,13 +83,16 @@ func initDNS() (err error) {
FileEnabled: config.QueryLog.FileEnabled,
}
set, err = nonDupEmptyHostNames(config.QueryLog.Ignored)
set, err = aghnet.NewDomainNameSet(config.QueryLog.Ignored)
if err != nil {
return fmt.Errorf("querylog: ignored list: %w", err)
}
conf.Ignored = set
Context.queryLog = querylog.New(conf)
Context.queryLog, err = querylog.New(conf)
if err != nil {
return fmt.Errorf("init querylog: %w", err)
}
Context.filters, err = filtering.New(config.DNS.DnsfilterConf, nil)
if err != nil {
@@ -426,7 +429,8 @@ func applyAdditionalFiltering(clientIP net.IP, clientID string, setts *filtering
}
setts.FilteringEnabled = c.FilteringEnabled
setts.SafeSearchEnabled = c.SafeSearchEnabled
setts.SafeSearchEnabled = c.safeSearchConf.Enabled
setts.ClientSafeSearch = c.SafeSearch
setts.SafeBrowsingEnabled = c.SafeBrowsingEnabled
setts.ParentalEnabled = c.ParentalEnabled
}
@@ -533,26 +537,30 @@ func closeDNSServer() {
log.Debug("all dns modules are closed")
}
// nonDupEmptyHostNames returns nil and error, if list has duplicate or empty
// host name. Otherwise returns a set, which contains lowercase host names
// without dot at the end, and nil error.
func nonDupEmptyHostNames(list []string) (set *stringutil.Set, err error) {
set = stringutil.NewSet()
// safeSearchResolver is a [filtering.Resolver] implementation used for safe
// search.
type safeSearchResolver struct{}
for _, v := range list {
host := strings.ToLower(strings.TrimSuffix(v, "."))
// TODO(a.garipov): Think about ignoring empty (".") names in
// the future.
if host == "" {
return nil, errors.Error("host name is empty")
}
// type check
var _ filtering.Resolver = safeSearchResolver{}
if set.Has(host) {
return nil, fmt.Errorf("duplicate host name %q", host)
}
set.Add(host)
// LookupIP implements [filtering.Resolver] interface for safeSearchResolver.
// It returns the slice of net.IP with IPv4 and IPv6 instances.
//
// TODO(a.garipov): Support network.
func (r safeSearchResolver) LookupIP(_ context.Context, _, host string) (ips []net.IP, err error) {
addrs, err := Context.dnsServer.Resolve(host)
if err != nil {
return nil, err
}
return set, nil
if len(addrs) == 0 {
return nil, fmt.Errorf("couldn't lookup host: %s", host)
}
for _, a := range addrs {
ips = append(ips, a.IP)
}
return ips, nil
}

View File

@@ -9,7 +9,6 @@ import (
"io/fs"
"net"
"net/http"
"net/http/pprof"
"net/netip"
"net/url"
"os"
@@ -28,6 +27,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
"github.com/AdguardTeam/AdGuardHome/internal/stats"
"github.com/AdguardTeam/AdGuardHome/internal/updater"
@@ -58,13 +58,12 @@ type homeContext struct {
dhcpServer dhcpd.Interface // DHCP module
auth *Auth // HTTP authentication module
filters *filtering.DNSFilter // DNS filtering module
web *Web // Web (HTTP, HTTPS) module
web *webAPI // Web (HTTP, HTTPS) module
tls *tlsManager // TLS module
// etcHosts is an IP-hostname pairs set taken from system configuration
// (e.g. /etc/hosts) files.
// etcHosts contains IP-hostname mappings taken from the OS-specific hosts
// configuration files, for example /etc/hosts.
etcHosts *aghnet.HostsContainer
// hostsWatcher is the watcher to detect changes in the hosts files.
hostsWatcher aghos.FSWatcher
updater *updater.Updater
@@ -79,7 +78,6 @@ type homeContext struct {
pidFileName string // PID file name. Empty if no PID file was created.
controlLock sync.Mutex
tlsRoots *x509.CertPool // list of root CAs for TLSv1.2
transport *http.Transport
client *http.Client
appSignalChannel chan os.Signal // Channel for receiving OS signals by the console app
@@ -149,18 +147,17 @@ func setupContext(opts options) {
setupContextFlags(opts)
Context.tlsRoots = aghtls.SystemRootCAs()
Context.transport = &http.Transport{
DialContext: customDialContext,
Proxy: getHTTPProxy,
TLSClientConfig: &tls.Config{
RootCAs: Context.tlsRoots,
CipherSuites: Context.tlsCipherIDs,
MinVersion: tls.VersionTLS12,
},
}
Context.client = &http.Client{
Timeout: time.Minute * 5,
Transport: Context.transport,
Timeout: time.Minute * 5,
Transport: &http.Transport{
DialContext: customDialContext,
Proxy: getHTTPProxy,
TLSClientConfig: &tls.Config{
RootCAs: Context.tlsRoots,
CipherSuites: Context.tlsCipherIDs,
MinVersion: tls.VersionTLS12,
},
},
}
if !Context.firstRun {
@@ -263,7 +260,7 @@ func configureOS(conf *configuration) (err error) {
// setupHostsContainer initializes the structures to keep up-to-date the hosts
// provided by the OS.
func setupHostsContainer() (err error) {
Context.hostsWatcher, err = aghos.NewOSWritesWatcher()
hostsWatcher, err := aghos.NewOSWritesWatcher()
if err != nil {
return fmt.Errorf("initing hosts watcher: %w", err)
}
@@ -271,18 +268,18 @@ func setupHostsContainer() (err error) {
Context.etcHosts, err = aghnet.NewHostsContainer(
filtering.SysHostsListID,
aghos.RootDirFS(),
Context.hostsWatcher,
hostsWatcher,
aghnet.DefaultHostsPaths()...,
)
if err != nil {
cerr := Context.hostsWatcher.Close()
if errors.Is(err, aghnet.ErrNoHostsPaths) && cerr == nil {
closeErr := hostsWatcher.Close()
if errors.Is(err, aghnet.ErrNoHostsPaths) && closeErr == nil {
log.Info("warning: initing hosts container: %s", err)
return nil
}
return errors.WithDeferred(fmt.Errorf("initing hosts container: %w", err), cerr)
return errors.WithDeferred(fmt.Errorf("initing hosts container: %w", err), closeErr)
}
return nil
@@ -298,6 +295,17 @@ func setupConfig(opts options) (err error) {
config.DNS.DnsfilterConf.UserRules = slices.Clone(config.UserRules)
config.DNS.DnsfilterConf.HTTPClient = Context.client
config.DNS.DnsfilterConf.SafeSearchConf.CustomResolver = safeSearchResolver{}
config.DNS.DnsfilterConf.SafeSearch, err = safesearch.NewDefault(
config.DNS.DnsfilterConf.SafeSearchConf,
"default",
config.DNS.DnsfilterConf.SafeSearchCacheSize,
time.Minute*time.Duration(config.DNS.DnsfilterConf.CacheTime),
)
if err != nil {
return fmt.Errorf("initializing safesearch: %w", err)
}
config.DHCP.WorkDir = Context.workDir
config.DHCP.HTTPRegister = httpRegister
config.DHCP.ConfigModified = onConfigModified
@@ -328,33 +336,16 @@ func setupConfig(opts options) (err error) {
arpdb = aghnet.NewARPDB()
}
Context.clients.Init(config.Clients.Persistent, Context.dhcpServer, Context.etcHosts, arpdb)
Context.clients.Init(config.Clients.Persistent, Context.dhcpServer, Context.etcHosts, arpdb, config.DNS.DnsfilterConf)
if opts.bindPort != 0 {
tcpPorts := aghalg.UniqChecker[tcpPort]{}
addPorts(tcpPorts, tcpPort(opts.bindPort))
udpPorts := aghalg.UniqChecker[udpPort]{}
addPorts(udpPorts, udpPort(config.DNS.Port))
if config.TLS.Enabled {
addPorts(
tcpPorts,
tcpPort(config.TLS.PortHTTPS),
tcpPort(config.TLS.PortDNSOverTLS),
tcpPort(config.TLS.PortDNSCrypt),
)
addPorts(udpPorts, udpPort(config.TLS.PortDNSOverQUIC))
}
if err = tcpPorts.Validate(); err != nil {
return fmt.Errorf("validating tcp ports: %w", err)
} else if err = udpPorts.Validate(); err != nil {
return fmt.Errorf("validating udp ports: %w", err)
}
config.BindPort = opts.bindPort
err = checkPorts()
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
}
// override bind host/port from the console
@@ -368,7 +359,35 @@ func setupConfig(opts options) (err error) {
return nil
}
func initWeb(opts options, clientBuildFS fs.FS) (web *Web, err error) {
// checkPorts is a helper for ports validation in config.
func checkPorts() (err error) {
tcpPorts := aghalg.UniqChecker[tcpPort]{}
addPorts(tcpPorts, tcpPort(config.BindPort))
udpPorts := aghalg.UniqChecker[udpPort]{}
addPorts(udpPorts, udpPort(config.DNS.Port))
if config.TLS.Enabled {
addPorts(
tcpPorts,
tcpPort(config.TLS.PortHTTPS),
tcpPort(config.TLS.PortDNSOverTLS),
tcpPort(config.TLS.PortDNSCrypt),
)
addPorts(udpPorts, udpPort(config.TLS.PortDNSOverQUIC))
}
if err = tcpPorts.Validate(); err != nil {
return fmt.Errorf("validating tcp ports: %w", err)
} else if err = udpPorts.Validate(); err != nil {
return fmt.Errorf("validating udp ports: %w", err)
}
return nil
}
func initWeb(opts options, clientBuildFS fs.FS) (web *webAPI, err error) {
var clientFS fs.FS
if opts.localFrontend {
log.Info("warning: using local frontend files")
@@ -395,7 +414,7 @@ func initWeb(opts options, clientBuildFS fs.FS) (web *Web, err error) {
serveHTTP3: config.DNS.ServeHTTP3,
}
web = newWeb(&webConf)
web = newWebAPI(&webConf)
if web == nil {
return nil, fmt.Errorf("initializing web: %w", err)
}
@@ -450,26 +469,8 @@ func run(opts options, clientBuildFS fs.FS) {
fatalOnError(err)
if config.DebugPProf {
mux := http.NewServeMux()
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
// See profileSupportsDelta in src/net/http/pprof/pprof.go.
mux.Handle("/debug/pprof/allocs", pprof.Handler("allocs"))
mux.Handle("/debug/pprof/block", pprof.Handler("block"))
mux.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine"))
mux.Handle("/debug/pprof/heap", pprof.Handler("heap"))
mux.Handle("/debug/pprof/mutex", pprof.Handler("mutex"))
mux.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate"))
go func() {
log.Info("pprof: listening on localhost:6060")
lerr := http.ListenAndServe("localhost:6060", mux)
log.Error("Error while running the pprof server: %s", lerr)
}()
// TODO(a.garipov): Make the address configurable.
startPprof("localhost:6060")
}
}
@@ -532,7 +533,7 @@ func run(opts options, clientBuildFS fs.FS) {
}
}
Context.web.Start()
Context.web.start()
// wait indefinitely for other go-routines to complete their job
select {}
@@ -712,7 +713,7 @@ func cleanup(ctx context.Context) {
log.Info("stopping AdGuard Home")
if Context.web != nil {
Context.web.Close(ctx)
Context.web.close(ctx)
Context.web = nil
}
if Context.auth != nil {
@@ -733,13 +734,6 @@ func cleanup(ctx context.Context) {
}
if Context.etcHosts != nil {
// Currently Context.hostsWatcher is only used in Context.etcHosts and
// needs closing only in case of the successful initialization of
// Context.etcHosts.
if err = Context.hostsWatcher.Close(); err != nil {
log.Error("closing hosts watcher: %s", err)
}
if err = Context.etcHosts.Close(); err != nil {
log.Error("closing hosts container: %s", err)
}
@@ -857,8 +851,10 @@ func detectFirstRun() bool {
// Connect to a remote server resolving hostname using our own DNS server.
//
// TODO(e.burkov): This messy logic should be decomposed and clarified.
//
// TODO(a.garipov): Support network.
func customDialContext(ctx context.Context, network, addr string) (conn net.Conn, err error) {
log.Tracef("network:%v addr:%v", network, addr)
log.Debug("home: customdial: dialing addr %q for network %s", addr, network)
host, port, err := net.SplitHostPort(addr)
if err != nil {

View File

@@ -11,6 +11,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/log"
"github.com/google/uuid"
"howett.net/plist"
@@ -170,7 +171,7 @@ func handleMobileConfig(w http.ResponseWriter, r *http.Request, dnsp string) {
return
}
w.Header().Set("Content-Type", "application/xml")
w.Header().Set(httphdr.ContentType, "application/xml")
const (
dohContDisp = `attachment; filename=doh.mobileconfig`
@@ -182,7 +183,7 @@ func handleMobileConfig(w http.ResponseWriter, r *http.Request, dnsp string) {
contDisp = dotContDisp
}
w.Header().Set("Content-Disposition", contDisp)
w.Header().Set(httphdr.ContentDisposition, contDisp)
_, _ = w.Write(mobileconfig)
}

39
internal/home/pprof.go Normal file
View File

@@ -0,0 +1,39 @@
package home
import (
"net/http"
"net/http/pprof"
"runtime"
"github.com/AdguardTeam/golibs/log"
)
// startPprof launches the debug and profiling server on addr.
func startPprof(addr string) {
runtime.SetBlockProfileRate(1)
runtime.SetMutexProfileFraction(1)
mux := http.NewServeMux()
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
// See profileSupportsDelta in src/net/http/pprof/pprof.go.
mux.Handle("/debug/pprof/allocs", pprof.Handler("allocs"))
mux.Handle("/debug/pprof/block", pprof.Handler("block"))
mux.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine"))
mux.Handle("/debug/pprof/heap", pprof.Handler("heap"))
mux.Handle("/debug/pprof/mutex", pprof.Handler("mutex"))
mux.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate"))
go func() {
defer log.OnPanic("pprof server")
log.Info("pprof: listening on %q", addr)
err := http.ListenAndServe(addr, mux)
log.Info("pprof server errors: %v", err)
}()
}

View File

@@ -108,7 +108,7 @@ func (m *tlsManager) start() {
// The background context is used because the TLSConfigChanged wraps context
// with timeout on its own and shuts down the server, which handles current
// request.
Context.web.TLSConfigChanged(context.Background(), tlsConf)
Context.web.tlsConfigChanged(context.Background(), tlsConf)
}
// reload updates the configuration and restarts t.
@@ -156,7 +156,7 @@ func (m *tlsManager) reload() {
// The background context is used because the TLSConfigChanged wraps context
// with timeout on its own and shuts down the server, which handles current
// request.
Context.web.TLSConfigChanged(context.Background(), tlsConf)
Context.web.tlsConfigChanged(context.Background(), tlsConf)
}
// loadTLSConf loads and validates the TLS configuration. The returned error is
@@ -454,7 +454,7 @@ func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request)
// same reason.
if restartHTTPS {
go func() {
Context.web.TLSConfigChanged(context.Background(), req.tlsConfigSettings)
Context.web.tlsConfigChanged(context.Background(), req.tlsConfigSettings)
}()
}
}

View File

@@ -22,7 +22,7 @@ import (
)
// currentSchemaVersion is the current schema version.
const currentSchemaVersion = 17
const currentSchemaVersion = 20
// These aliases are provided for convenience.
type (
@@ -90,6 +90,9 @@ func upgradeConfigSchema(oldVersion int, diskConf yobj) (err error) {
upgradeSchema14to15,
upgradeSchema15to16,
upgradeSchema16to17,
upgradeSchema17to18,
upgradeSchema18to19,
upgradeSchema19to20,
}
n := 0
@@ -836,9 +839,9 @@ func upgradeSchema14to15(diskConf yobj) (err error) {
}
type temp struct {
val any
from string
to string
val any
}
replaces := []temp{
{from: "querylog_enabled", to: "enabled", val: true},
@@ -873,6 +876,18 @@ func upgradeSchema14to15(diskConf yobj) (err error) {
// 'enabled': true
// 'interval': 1
// 'ignored': []
//
// If statistics were disabled:
//
// # BEFORE:
// 'dns':
// 'statistics_interval': 0
//
// # AFTER:
// 'statistics':
// 'enabled': false
// 'interval': 1
// 'ignored': []
func upgradeSchema15to16(diskConf yobj) (err error) {
log.Printf("Upgrade yaml: 15 to 16")
diskConf["schema_version"] = 16
@@ -894,10 +909,23 @@ func upgradeSchema15to16(diskConf yobj) (err error) {
}
const field = "statistics_interval"
v, has := dns[field]
statsIvlVal, has := dns[field]
if has {
stats["enabled"] = v != 0
stats["interval"] = v
var statsIvl int
statsIvl, ok = statsIvlVal.(int)
if !ok {
return fmt.Errorf("unexpected type of dns.statistics_interval: %T", statsIvlVal)
}
if statsIvl == 0 {
// Set the interval to the default value of one day to make sure
// that it passes the validations.
stats["interval"] = 1
stats["enabled"] = false
} else {
stats["interval"] = statsIvl
stats["enabled"] = true
}
}
delete(dns, field)
@@ -943,6 +971,172 @@ func upgradeSchema16to17(diskConf yobj) (err error) {
return nil
}
// upgradeSchema17to18 performs the following changes:
//
// # BEFORE:
// 'dns':
// 'safesearch_enabled': true
//
// # AFTER:
// 'dns':
// 'safe_search':
// 'enabled': true
// 'bing': true
// 'duckduckgo': true
// 'google': true
// 'pixabay': true
// 'yandex': true
// 'youtube': true
func upgradeSchema17to18(diskConf yobj) (err error) {
log.Printf("Upgrade yaml: 17 to 18")
diskConf["schema_version"] = 18
dnsVal, ok := diskConf["dns"]
if !ok {
return nil
}
dns, ok := dnsVal.(yobj)
if !ok {
return fmt.Errorf("unexpected type of dns: %T", dnsVal)
}
safeSearch := yobj{
"enabled": true,
"bing": true,
"duckduckgo": true,
"google": true,
"pixabay": true,
"yandex": true,
"youtube": true,
}
const safeSearchKey = "safesearch_enabled"
v, has := dns[safeSearchKey]
if has {
safeSearch["enabled"] = v
}
delete(dns, safeSearchKey)
dns["safe_search"] = safeSearch
return nil
}
// upgradeSchema18to19 performs the following changes:
//
// # BEFORE:
// 'clients':
// 'persistent':
// - 'name': 'client-name'
// 'safesearch_enabled': true
//
// # AFTER:
// 'clients':
// 'persistent':
// - 'name': 'client-name'
// 'safe_search':
// 'enabled': true
// 'bing': true
// 'duckduckgo': true
// 'google': true
// 'pixabay': true
// 'yandex': true
// 'youtube': true
func upgradeSchema18to19(diskConf yobj) (err error) {
log.Printf("Upgrade yaml: 18 to 19")
diskConf["schema_version"] = 19
clientsVal, ok := diskConf["clients"]
if !ok {
return nil
}
clients, ok := clientsVal.(yobj)
if !ok {
return fmt.Errorf("unexpected type of clients: %T", clientsVal)
}
persistent, ok := clients["persistent"].([]yobj)
if !ok {
return nil
}
const safeSearchKey = "safesearch_enabled"
for i := range persistent {
c := persistent[i]
safeSearch := yobj{
"enabled": true,
"bing": true,
"duckduckgo": true,
"google": true,
"pixabay": true,
"yandex": true,
"youtube": true,
}
v, has := c[safeSearchKey]
if has {
safeSearch["enabled"] = v
}
delete(c, safeSearchKey)
c["safe_search"] = safeSearch
}
return nil
}
// upgradeSchema19to20 performs the following changes:
//
// # BEFORE:
// 'statistics':
// 'interval': 1
//
// # AFTER:
// 'statistics':
// 'interval': 24h
func upgradeSchema19to20(diskConf yobj) (err error) {
log.Printf("Upgrade yaml: 19 to 20")
diskConf["schema_version"] = 20
statsVal, ok := diskConf["statistics"]
if !ok {
return nil
}
var stats yobj
stats, ok = statsVal.(yobj)
if !ok {
return fmt.Errorf("unexpected type of stats: %T", statsVal)
}
const field = "interval"
// Set the initial value from the global configuration structure.
statsIvl := 1
statsIvlVal, ok := stats[field]
if ok {
statsIvl, ok = statsIvlVal.(int)
if !ok {
return fmt.Errorf("unexpected type of %s: %T", field, statsIvlVal)
}
// The initial version of upgradeSchema16to17 did not set the zero
// interval to a non-zero one. So, reset it now.
if statsIvl == 0 {
statsIvl = 1
}
}
stats[field] = timeutil.Duration{Duration: time.Duration(statsIvl) * timeutil.Day}
return nil
}
// TODO(a.garipov): Replace with log.Output when we port it to our logging
// package.
func funcName() string {

View File

@@ -729,7 +729,7 @@ func TestUpgradeSchema15to16(t *testing.T) {
want: yobj{
"statistics": map[string]any{
"enabled": false,
"interval": 0,
"interval": 1,
"ignored": []any{},
},
"dns": map[string]any{},
@@ -808,3 +808,246 @@ func TestUpgradeSchema16to17(t *testing.T) {
})
}
}
func TestUpgradeSchema17to18(t *testing.T) {
const newSchemaVer = 18
defaultWantObj := yobj{
"dns": yobj{
"safe_search": yobj{
"enabled": true,
"bing": true,
"duckduckgo": true,
"google": true,
"pixabay": true,
"yandex": true,
"youtube": true,
},
},
"schema_version": newSchemaVer,
}
testCases := []struct {
in yobj
want yobj
name string
}{{
in: yobj{"dns": yobj{}},
want: defaultWantObj,
name: "default_values",
}, {
in: yobj{"dns": yobj{"safesearch_enabled": true}},
want: defaultWantObj,
name: "enabled",
}, {
in: yobj{"dns": yobj{"safesearch_enabled": false}},
want: yobj{
"dns": yobj{
"safe_search": map[string]any{
"enabled": false,
"bing": true,
"duckduckgo": true,
"google": true,
"pixabay": true,
"yandex": true,
"youtube": true,
},
},
"schema_version": newSchemaVer,
},
name: "disabled",
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := upgradeSchema17to18(tc.in)
require.NoError(t, err)
assert.Equal(t, tc.want, tc.in)
})
}
}
func TestUpgradeSchema18to19(t *testing.T) {
const newSchemaVer = 19
defaultWantObj := yobj{
"clients": yobj{
"persistent": []yobj{{
"name": "localhost",
"safe_search": yobj{
"enabled": true,
"bing": true,
"duckduckgo": true,
"google": true,
"pixabay": true,
"yandex": true,
"youtube": true,
},
}},
},
"schema_version": newSchemaVer,
}
testCases := []struct {
in yobj
want yobj
name string
}{{
in: yobj{
"clients": yobj{},
},
want: yobj{
"clients": yobj{},
"schema_version": newSchemaVer,
},
name: "no_clients",
}, {
in: yobj{
"clients": yobj{
"persistent": []yobj{{"name": "localhost"}},
},
},
want: defaultWantObj,
name: "default_values",
}, {
in: yobj{
"clients": yobj{
"persistent": []yobj{{"name": "localhost", "safesearch_enabled": true}},
},
},
want: defaultWantObj,
name: "enabled",
}, {
in: yobj{
"clients": yobj{
"persistent": []yobj{{"name": "localhost", "safesearch_enabled": false}},
},
},
want: yobj{
"clients": yobj{"persistent": []yobj{{
"name": "localhost",
"safe_search": yobj{
"enabled": false,
"bing": true,
"duckduckgo": true,
"google": true,
"pixabay": true,
"yandex": true,
"youtube": true,
},
}}},
"schema_version": newSchemaVer,
},
name: "disabled",
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := upgradeSchema18to19(tc.in)
require.NoError(t, err)
assert.Equal(t, tc.want, tc.in)
})
}
}
func TestUpgradeSchema19to20(t *testing.T) {
testCases := []struct {
ivl any
want any
wantErr string
name string
}{{
ivl: 1,
want: timeutil.Duration{Duration: timeutil.Day},
wantErr: "",
name: "success",
}, {
ivl: 0,
want: timeutil.Duration{Duration: timeutil.Day},
wantErr: "",
name: "success",
}, {
ivl: 0.25,
want: 0,
wantErr: "unexpected type of interval: float64",
name: "fail",
}}
for _, tc := range testCases {
conf := yobj{
"statistics": yobj{
"interval": tc.ivl,
},
"schema_version": 19,
}
t.Run(tc.name, func(t *testing.T) {
err := upgradeSchema19to20(conf)
if tc.wantErr != "" {
require.Error(t, err)
assert.Equal(t, tc.wantErr, err.Error())
return
}
require.NoError(t, err)
require.Equal(t, conf["schema_version"], 20)
statsVal, ok := conf["statistics"]
require.True(t, ok)
var stats yobj
stats, ok = statsVal.(yobj)
require.True(t, ok)
var newIvl timeutil.Duration
newIvl, ok = stats["interval"].(timeutil.Duration)
require.True(t, ok)
assert.Equal(t, tc.want, newIvl)
})
}
t.Run("no_stats", func(t *testing.T) {
err := upgradeSchema19to20(yobj{})
assert.NoError(t, err)
})
t.Run("bad_stats", func(t *testing.T) {
err := upgradeSchema19to20(yobj{
"statistics": 0,
})
testutil.AssertErrorMsg(t, "unexpected type of stats: int", err)
})
t.Run("no_field", func(t *testing.T) {
conf := yobj{
"statistics": yobj{},
}
err := upgradeSchema19to20(conf)
require.NoError(t, err)
statsVal, ok := conf["statistics"]
require.True(t, ok)
var stats yobj
stats, ok = statsVal.(yobj)
require.True(t, ok)
var ivl any
ivl, ok = stats["interval"]
require.True(t, ok)
var ivlVal timeutil.Duration
ivlVal, ok = ivl.(timeutil.Duration)
require.True(t, ok)
assert.Equal(t, 24*time.Hour, ivlVal.Duration)
})
}

View File

@@ -35,9 +35,8 @@ const (
type webConfig struct {
clientFS fs.FS
BindHost netip.Addr
BindPort int
PortHTTPS int
BindHost netip.Addr
BindPort int
// ReadTimeout is an option to pass to http.Server for setting an
// appropriate field.
@@ -72,8 +71,8 @@ type httpsServer struct {
enabled bool
}
// Web is the web UI and API server.
type Web struct {
// webAPI is the web UI and API server.
type webAPI struct {
conf *webConfig
// TODO(a.garipov): Refactor all these servers.
@@ -82,15 +81,13 @@ type Web struct {
// httpsServer is the server that handles HTTPS traffic. If it is not nil,
// [Web.http3Server] must also not be nil.
httpsServer httpsServer
forceHTTPS bool
}
// newWeb creates a new instance of the web UI and API server.
func newWeb(conf *webConfig) (w *Web) {
// newWebAPI creates a new instance of the web UI and API server.
func newWebAPI(conf *webConfig) (w *webAPI) {
log.Info("web: initializing")
w = &Web{
w = &webAPI{
conf: conf,
}
@@ -125,12 +122,10 @@ func webCheckPortAvailable(port int) (ok bool) {
return aghnet.CheckPort("tcp", netip.AddrPortFrom(config.BindHost, uint16(port))) == nil
}
// TLSConfigChanged updates the TLS configuration and restarts the HTTPS server
// tlsConfigChanged updates the TLS configuration and restarts the HTTPS server
// if necessary.
func (web *Web) TLSConfigChanged(ctx context.Context, tlsConf tlsConfigSettings) {
func (web *webAPI) tlsConfigChanged(ctx context.Context, tlsConf tlsConfigSettings) {
log.Debug("web: applying new tls configuration")
web.conf.PortHTTPS = tlsConf.PortHTTPS
web.forceHTTPS = (tlsConf.ForceHTTPS && tlsConf.Enabled && tlsConf.PortHTTPS != 0)
enabled := tlsConf.Enabled &&
tlsConf.PortHTTPS != 0 &&
@@ -161,8 +156,8 @@ func (web *Web) TLSConfigChanged(ctx context.Context, tlsConf tlsConfigSettings)
web.httpsServer.cond.L.Unlock()
}
// Start - start serving HTTP requests
func (web *Web) Start() {
// start - start serving HTTP requests
func (web *webAPI) start() {
log.Println("AdGuard Home is available at the following addresses:")
// for https, we have a separate goroutine loop
@@ -203,8 +198,8 @@ func (web *Web) Start() {
}
}
// Close gracefully shuts down the HTTP servers.
func (web *Web) Close(ctx context.Context) {
// close gracefully shuts down the HTTP servers.
func (web *webAPI) close(ctx context.Context) {
log.Info("stopping http server...")
web.httpsServer.cond.L.Lock()
@@ -222,7 +217,7 @@ func (web *Web) Close(ctx context.Context) {
log.Info("stopped http server")
}
func (web *Web) tlsServerLoop() {
func (web *webAPI) tlsServerLoop() {
for {
web.httpsServer.cond.L.Lock()
if web.httpsServer.inShutdown {
@@ -241,7 +236,15 @@ func (web *Web) tlsServerLoop() {
web.httpsServer.cond.L.Unlock()
addr := netutil.JoinHostPort(web.conf.BindHost.String(), web.conf.PortHTTPS)
var portHTTPS int
func() {
config.RLock()
defer config.RUnlock()
portHTTPS = config.TLS.PortHTTPS
}()
addr := netutil.JoinHostPort(web.conf.BindHost.String(), portHTTPS)
web.httpsServer.server = &http.Server{
ErrorLog: log.StdLog("web: https", log.DEBUG),
Addr: addr,
@@ -272,7 +275,7 @@ func (web *Web) tlsServerLoop() {
}
}
func (web *Web) mustStartHTTP3(address string) {
func (web *webAPI) mustStartHTTP3(address string) {
defer log.OnPanic("web: http3")
web.httpsServer.server3 = &http3.Server{