Pull request 2506: AGDNS-3334-imp-home

Squashed commit of the following:

commit ae110ad162ea7f30854750255a378f3b6484de94
Merge: 965d052f9 3bebde610
Author: f.setrakov <f.setrakov@adguard.com>
Date:   Sat Nov 1 13:05:43 2025 +0300

    Merge branch 'master' into AGDNS-3334-imp-home

commit 965d052f96
Author: f.setrakov <f.setrakov@adguard.com>
Date:   Mon Oct 27 15:58:20 2025 +0300

    home: imp docs

commit e53565e055
Merge: 3bafcd30c 8b8bfb21e
Author: f.setrakov <f.setrakov@adguard.com>
Date:   Mon Oct 27 15:50:12 2025 +0300

    Merge branch 'master' into AGDNS-3334-imp-home

commit 3bafcd30cd
Author: f.setrakov <f.setrakov@adguard.com>
Date:   Fri Oct 24 16:53:48 2025 +0300

    all: imp style

commit b762eaa51c
Author: f.setrakov <f.setrakov@adguard.com>
Date:   Thu Oct 23 17:08:48 2025 +0300

    home: imp docs

commit c54a961c2e
Author: f.setrakov <f.setrakov@adguard.com>
Date:   Thu Oct 23 11:21:03 2025 +0300

    home: fix tls server

commit 0801b367f8
Author: f.setrakov <f.setrakov@adguard.com>
Date:   Wed Oct 22 18:33:48 2025 +0300

    home: imp style

commit f148198fe0
Author: f.setrakov <f.setrakov@adguard.com>
Date:   Wed Oct 22 14:35:12 2025 +0300

    home: imp allocs

commit dbe768ec96
Author: f.setrakov <f.setrakov@adguard.com>
Date:   Wed Oct 22 14:20:38 2025 +0300

    home: fix docs, style

commit 74e88ba044
Author: f.setrakov <f.setrakov@adguard.com>
Date:   Wed Oct 22 14:07:24 2025 +0300

    home: imp docs, imp maintain

commit 6260af1ace
Author: f.setrakov <f.setrakov@adguard.com>
Date:   Tue Oct 21 20:00:30 2025 +0300

    scripts: changed gocognit value for home pkg

commit 4a5dfaa618
Author: f.setrakov <f.setrakov@adguard.com>
Date:   Tue Oct 21 19:51:38 2025 +0300

    all: imp style

commit 18f9d9a022
Author: f.setrakov <f.setrakov@adguard.com>
Date:   Mon Oct 20 19:17:36 2025 +0300

    home: imp cognit complexity
This commit is contained in:
Fedor Setrakov
2025-11-01 13:22:48 +03:00
parent 3bebde610b
commit 594849b5af
7 changed files with 466 additions and 274 deletions

View File

@@ -374,32 +374,11 @@ func (mw *authMiddlewareDefault) Wrap(h http.Handler) (wrapped http.Handler) {
}
path := r.URL.Path
u, err := mw.userFromRequest(ctx, r)
if err != nil {
mw.logger.ErrorContext(ctx, "retrieving user from request", slogutil.KeyError, err)
}
if u != nil {
if path == "/login.html" {
http.Redirect(w, r, "/", http.StatusFound)
if mw.handleAuthenticatedUser(ctx, w, r, h, path) {
return
}
h.ServeHTTP(w, r.WithContext(withWebUser(ctx, u)))
return
}
if isPublicResource(path) {
h.ServeHTTP(w, r)
return
}
if path == "/" || path == "/index.html" {
http.Redirect(w, r, "login.html", http.StatusFound)
if mw.handlePublicAccess(w, r, h, path) {
return
}
@@ -407,6 +386,58 @@ func (mw *authMiddlewareDefault) Wrap(h http.Handler) (wrapped http.Handler) {
})
}
// handleAuthenticatedUser tries to get user from request and processes request
// if user was successfully authenticated. Returns true if request was handled.
func (mw *authMiddlewareDefault) handleAuthenticatedUser(
ctx context.Context,
w http.ResponseWriter,
r *http.Request,
h http.Handler,
path string,
) (ok bool) {
u, err := mw.userFromRequest(ctx, r)
if err != nil {
mw.logger.ErrorContext(ctx, "retrieving user from request", slogutil.KeyError, err)
}
if u == nil {
return false
}
if path == "/login.html" {
http.Redirect(w, r, "/", http.StatusFound)
return true
}
h.ServeHTTP(w, r.WithContext(withWebUser(ctx, u)))
return true
}
// handlePublicAccess handles request if user is trying to access public or root
// pages.
func (mw *authMiddlewareDefault) handlePublicAccess(
w http.ResponseWriter,
r *http.Request,
h http.Handler,
path string,
) (ok bool) {
if isPublicResource(path) {
h.ServeHTTP(w, r)
return true
}
if path == "/" || path == "/index.html" {
http.Redirect(w, r, "login.html", http.StatusFound)
return true
}
return false
}
// needsAuthentication returns true if there are stored web users and requests
// should be authenticated first.
func (mw *authMiddlewareDefault) needsAuthentication(ctx context.Context) (ok bool) {

View File

@@ -228,56 +228,60 @@ func finishUpdate(
cleanup(ctx)
cleanupAlways()
var err error
if runtime.GOOS == "windows" {
if runningAsService {
// NOTE: We can't restart the service via "kardianos/service"
// package, because it kills the process first we can't start a new
// instance, because Windows doesn't allow it.
//
// TODO(a.garipov): Recheck the claim above.
var cmd executil.Command
cmd, err = cmdCons.New(ctx, &executil.CommandConfig{
Path: "cmd",
Args: []string{"/c", "net stop AdGuardHome & net start AdGuardHome"},
})
if err != nil {
panic(fmt.Errorf("constructing cmd: %w", err))
}
err = cmd.Start(ctx)
if err != nil {
panic(fmt.Errorf("restarting service: %w", err))
}
os.Exit(osutil.ExitCodeSuccess)
}
l.InfoContext(ctx, "restarting", "exec_path", execPath, "args", os.Args[1:])
var cmd executil.Command
cmd, err = cmdCons.New(ctx, &executil.CommandConfig{
Path: execPath,
Args: os.Args[1:],
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
})
if err != nil {
panic(fmt.Errorf("constructing cmd: %w", err))
}
err = cmd.Start(ctx)
if err != nil {
panic(fmt.Errorf("restarting: %w", err))
}
finalizeWindowsUpdate(ctx, l, cmdCons, execPath, runningAsService)
os.Exit(osutil.ExitCodeSuccess)
}
var err error
l.InfoContext(ctx, "restarting", "exec_path", execPath, "args", os.Args[1:])
err = syscall.Exec(execPath, os.Args, os.Environ())
if err != nil {
panic(fmt.Errorf("restarting: %w", err))
}
}
// finalizeWindowsUpdate completes an update procedure on windows. l and
// cmdCons must not be nil.
func finalizeWindowsUpdate(ctx context.Context,
l *slog.Logger,
cmdCons executil.CommandConstructor,
execPath string,
runningAsService bool,
) {
var commandConf *executil.CommandConfig
if runningAsService {
// NOTE: We can't restart the service via "kardianos/service" package,
// because it kills the process first we can't start a new instance,
// because Windows doesn't allow it.
//
// TODO(a.garipov): Recheck the claim above.
commandConf = &executil.CommandConfig{
Path: "cmd",
Args: []string{"/c", "net stop AdGuardHome & net start AdGuardHome"},
}
} else {
commandConf = &executil.CommandConfig{
Path: execPath,
Args: os.Args[1:],
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
}
}
l.InfoContext(ctx, "restarting", "exec_path", execPath, "args", os.Args[1:])
var cmd executil.Command
cmd, err := cmdCons.New(ctx, commandConf)
if err != nil {
panic(fmt.Errorf("constructing cmd: %w", err))
}
err = cmd.Start(ctx)
if err != nil {
panic(fmt.Errorf("restarting: %w", err))
}
}

View File

@@ -695,100 +695,29 @@ func run(
workDir string,
confPath string,
) {
ls := getLogSettings(ctx, slogLogger, opts, workDir, confPath)
// Configure log level and output.
err := configureLogger(ls, workDir)
fatalOnError(err)
// Print the first message after logger is configured.
slogLogger.InfoContext(ctx, "starting adguard home", "version", version.Full())
slogLogger.DebugContext(ctx, "current working directory", "path", workDir)
if opts.runningAsService {
slogLogger.InfoContext(ctx, "adguard home is running as a service")
}
aghtls.Init(ctx, slogLogger.With(slogutil.KeyPrefix, "aghtls"))
initEnvironment(ctx, opts, slogLogger, workDir, confPath)
isFirstRun := detectFirstRun(ctx, slogLogger, workDir, confPath)
err = setupContext(ctx, slogLogger, opts, workDir, confPath, isFirstRun)
fatalOnError(err)
err = configureOS(config)
fatalOnError(err)
// Clients package uses filtering package's static data
// (filtering.BlockedSvcKnown()), so we have to initialize filtering static
// data first, but also to avoid relying on automatic Go init() function.
filtering.InitModule(ctx, slogLogger)
confModifier := newDefaultConfigModifier(
config,
slogLogger.With(slogutil.KeyPrefix, "config_modifier"),
workDir,
confPath,
)
mw := &webMw{}
mux := http.NewServeMux()
httpReg := aghhttp.NewDefaultRegistrar(mux, mw.wrap)
err = initContextClients(ctx, slogLogger, sigHdlr, confModifier, httpReg, workDir)
fatalOnError(err)
tlsMgrLogger := slogLogger.With(slogutil.KeyPrefix, "tls_manager")
tlsMgr, err := newTLSManager(ctx, &tlsManagerConfig{
logger: tlsMgrLogger,
confModifier: confModifier,
httpReg: httpReg,
tlsSettings: config.TLS,
servePlainDNS: config.DNS.ServePlainDNS,
})
if err != nil {
tlsMgrLogger.ErrorContext(ctx, "initializing", slogutil.KeyError, err)
confModifier.Apply(ctx)
}
confModifier.setTLSManager(tlsMgr)
err = setupDNSFilteringConf(
confModifier, tlsMgr := initFiltering(
ctx,
slogLogger,
config.Filtering,
tlsMgr,
confModifier,
httpReg,
opts,
isFirstRun,
sigHdlr,
workDir,
confPath,
httpReg,
)
fatalOnError(err)
err = setupOpts(opts)
fatalOnError(err)
execPath, err := os.Executable()
fatalOnError(errors.Annotate(err, "getting executable path: %w"))
updLogger := slogLogger.With(slogutil.KeyPrefix, "updater")
upd, isCustomURL := newUpdater(ctx, updLogger, config, workDir, confPath, execPath)
// TODO(e.burkov): This could be made earlier, probably as the option's
// effect.
cmdlineUpdate(ctx, updLogger, opts, upd, tlsMgr, isFirstRun)
if !isFirstRun {
// Save the updated config.
err = config.write(ctx, slogLogger, nil, nil, workDir, confPath)
fatalOnError(err)
if config.HTTPConfig.Pprof.Enabled {
startPprof(slogLogger, config.HTTPConfig.Pprof.Port)
}
}
upd, isCustomURL := initUpdate(ctx, slogLogger, opts, tlsMgr, isFirstRun, workDir, confPath)
dataDirPath := filepath.Join(workDir, dataDir)
err = os.MkdirAll(dataDirPath, aghos.DefaultPermDir)
err := os.MkdirAll(dataDirPath, aghos.DefaultPermDir)
fatalOnError(errors.Annotate(err, "creating DNS data dir at %s: %w", dataDirPath))
auth, err := initUsers(ctx, slogLogger, workDir, opts.glinetMode)
@@ -825,7 +754,31 @@ func run(
fatalOnError(err)
if !isFirstRun {
err = initDNS(ctx, slogLogger, tlsMgr, confModifier, httpReg, statsDir, querylogDir)
runDNSServer(ctx, slogLogger, tlsMgr, confModifier, statsDir, querylogDir, httpReg)
}
if !opts.noPermCheck {
checkPermissions(ctx, slogLogger, workDir, confPath, dataDirPath, statsDir, querylogDir)
}
web.start(ctx)
// Wait for other goroutines to complete their job.
<-done
}
// runDNSServer initializes and starts DNS and DHCP servers if this is not the
// first run. httpReg, slogLogger, tlsMgr and confModifier must not be nil.
func runDNSServer(
ctx context.Context,
slogLogger *slog.Logger,
tlsMgr *tlsManager,
confModifier *defaultConfigModifier,
statsDir string,
querylogDir string,
httpReg *aghhttp.DefaultRegistrar,
) {
err := initDNS(ctx, slogLogger, tlsMgr, confModifier, httpReg, statsDir, querylogDir)
fatalOnError(err)
tlsMgr.start(ctx)
@@ -844,16 +797,139 @@ func run(
slogLogger.ErrorContext(ctx, "starting dhcp server", slogutil.KeyError, err)
}
}
}
// initFiltering configures the core filtering and TLS subsystems. Returns a
// configuration modifier and the initialized TLS manager. slogLogger, sigHdlr
// and httpReg must not be nil.
func initFiltering(
ctx context.Context,
slogLogger *slog.Logger,
opts options,
isFirstRun bool,
sigHdlr *signalHandler,
workDir string,
confPath string,
httpReg *aghhttp.DefaultRegistrar,
) (confModifier *defaultConfigModifier, tlsMgr *tlsManager) {
err := setupContext(ctx, slogLogger, opts, workDir, confPath, isFirstRun)
fatalOnError(err)
err = configureOS(config)
fatalOnError(err)
// Clients package uses filtering package's static data
// (filtering.BlockedSvcKnown()), so we have to initialize filtering static
// data first, but also to avoid relying on automatic Go init() function.
filtering.InitModule(ctx, slogLogger)
confModifier = newDefaultConfigModifier(
config,
slogLogger.With(slogutil.KeyPrefix, "config_modifier"),
workDir,
confPath,
)
err = initContextClients(ctx, slogLogger, sigHdlr, confModifier, httpReg, workDir)
fatalOnError(err)
tlsMgrLogger := slogLogger.With(slogutil.KeyPrefix, "tls_manager")
tlsMgr, err = newTLSManager(ctx, &tlsManagerConfig{
logger: tlsMgrLogger,
confModifier: confModifier,
httpReg: httpReg,
tlsSettings: config.TLS,
servePlainDNS: config.DNS.ServePlainDNS,
})
if err != nil {
tlsMgrLogger.ErrorContext(ctx, "initializing", slogutil.KeyError, err)
confModifier.Apply(ctx)
}
if !opts.noPermCheck {
checkPermissions(ctx, slogLogger, workDir, confPath, dataDirPath, statsDir, querylogDir)
confModifier.setTLSManager(tlsMgr)
err = setupDNSFilteringConf(
ctx,
slogLogger,
config.Filtering,
tlsMgr,
confModifier,
httpReg,
workDir,
)
fatalOnError(err)
err = setupOpts(opts)
fatalOnError(err)
return confModifier, tlsMgr
}
// initUpdate configures and runs update of this application. slogLogger and
// tlsMgr must not be nil.
func initUpdate(
ctx context.Context,
slogLogger *slog.Logger,
opts options,
tlsMgr *tlsManager,
isFirstRun bool,
workDir string,
confPath string,
) (upd *updater.Updater, isCustomURL bool) {
execPath, err := os.Executable()
fatalOnError(errors.Annotate(err, "getting executable path: %w"))
updLogger := slogLogger.With(slogutil.KeyPrefix, "updater")
upd, isCustomURL = newUpdater(
ctx,
updLogger,
config,
workDir,
confPath,
execPath,
)
// TODO(e.burkov): This could be made earlier, probably as the option's
// effect.
cmdlineUpdate(ctx, updLogger, opts, upd, tlsMgr, isFirstRun)
if !isFirstRun {
// Save the updated config.
err = config.write(ctx, slogLogger, nil, nil, workDir, confPath)
fatalOnError(err)
if config.HTTPConfig.Pprof.Enabled {
startPprof(slogLogger, config.HTTPConfig.Pprof.Port)
}
}
web.start(ctx)
return upd, isCustomURL
}
// Wait for other goroutines to complete their job.
<-done
// initEnvironment inits working environment. opts and slogLogger must not be
// nil.
func initEnvironment(
ctx context.Context,
opts options,
slogLogger *slog.Logger,
workDir,
confPath string,
) {
ls := getLogSettings(ctx, slogLogger, opts, workDir, confPath)
// Configure log level and output.
err := configureLogger(ls, workDir)
fatalOnError(err)
// Print the first message after logger is configured.
slogLogger.InfoContext(ctx, "starting adguard home", "version", version.Full())
slogLogger.DebugContext(ctx, "current working directory", "path", workDir)
if opts.runningAsService {
slogLogger.InfoContext(ctx, "adguard home is running as a service")
}
aghtls.Init(ctx, slogLogger.With(slogutil.KeyPrefix, "aghtls"))
}
// newUpdater creates a new AdGuard Home updater. l and conf must not be nil.

View File

@@ -2,8 +2,10 @@ package home
import (
"fmt"
"iter"
"net/netip"
"os"
"slices"
"strconv"
"strings"
@@ -384,41 +386,89 @@ func printHelp(exec string) {
// parseCmdOpts parses the command-line arguments into options and effects.
func parseCmdOpts(cmdName string, args []string) (o options, eff effect, err error) {
// Don't use range since the loop changes the loop variable.
argsLen := len(args)
for i := 0; i < len(args); i++ {
arg := args[i]
isKnown := false
for _, opt := range cmdLineOpts {
isKnown = argMatches(opt, arg)
if !isKnown {
continue
next, stop := iter.Pull2(slices.All(args))
defer stop()
for i, arg, ok := next(); ok; i, arg, ok = next() {
o, eff, err = parseArg(cmdName, next, o, eff, arg)
if err != nil {
return o, eff, fmt.Errorf("parsing arg at index %d: %w", i, err)
}
}
return o, eff, nil
}
// parseArg parses command-line argument into options and effects. next and
// eff must not be nil.
func parseArg(
cmdName string,
next func() (int, string, bool),
o options,
eff effect,
arg string,
) (newOpt options, newEff effect, err error) {
opt, found := findMatchingOpt(arg)
if !found {
return o, eff, fmt.Errorf("unknown option %s", arg)
}
if opt.updateWithValue != nil {
i++
if i >= argsLen {
return o, eff, fmt.Errorf("got %s without argument", arg)
return applyOptWithValue(opt, next, o, eff, arg)
}
o, err = opt.updateWithValue(o, args[i])
} else {
o, eff, err = updateOptsNoValue(o, eff, opt, cmdName)
}
return applyOptNoValue(opt, cmdName, o, eff, arg)
}
// applyOptNoValue applies option with no value. eff must not be
// nil.
func applyOptNoValue(
opt cmdLineOpt,
cmdName string,
o options,
eff effect,
arg string,
) (newOpt options, newEff effect, err error) {
newOpts, newEff, err := updateOptsNoValue(o, eff, opt, cmdName)
if err != nil {
return o, eff, fmt.Errorf("applying option %s: %w", arg, err)
}
break
return newOpts, newEff, nil
}
// applyOptWithValue applies argument with value. next and eff must not
// be nil.
func applyOptWithValue(
opt cmdLineOpt,
next func() (int, string, bool),
o options,
eff effect,
arg string,
) (newOpt options, newEff effect, err error) {
_, val, ok := next()
if !ok {
return o, eff, fmt.Errorf("got %s without argument", arg)
}
if !isKnown {
return o, eff, fmt.Errorf("unknown option %s", arg)
newOpts, err := opt.updateWithValue(o, val)
if err != nil {
return o, eff, fmt.Errorf("applying option %s: %w", arg, err)
}
return newOpts, eff, nil
}
// findMatchingOpt returns cmdLineOpt which matches the given arg. ok indicates
// whether the appropriate option was found.
func findMatchingOpt(arg string) (opt cmdLineOpt, ok bool) {
for _, opt := range cmdLineOpts {
if argMatches(opt, arg) {
return opt, true
}
}
return o, eff, err
return cmdLineOpt{}, false
}
// argMatches returns true if arg matches command-line option opt.

View File

@@ -390,16 +390,11 @@ func handleServiceInstallCommand(
}
}
// handleServiceUninstallCommand handles service "uninstall" command.
// handleServiceUninstallCommand handles service "uninstall" command. l and s
// must not be nil.
func handleServiceUninstallCommand(ctx context.Context, l *slog.Logger, s service.Service) {
if aghos.IsOpenWrt() {
// On OpenWrt it is important to run disable command first
// as it will remove the symlink
_, err := runInitdCommand(ctx, "disable")
if err != nil {
l.ErrorContext(ctx, "running init disable", slogutil.KeyError, err)
os.Exit(osutil.ExitCodeFailure)
}
handleOpenWrtUninstall(ctx, l)
}
if err := svcAction(ctx, l, s, "stop"); err != nil {
@@ -408,10 +403,30 @@ func handleServiceUninstallCommand(ctx context.Context, l *slog.Logger, s servic
if err := svcAction(ctx, l, s, "uninstall"); err != nil {
l.ErrorContext(ctx, "executing action uninstall", slogutil.KeyError, err)
os.Exit(osutil.ExitCodeFailure)
}
if runtime.GOOS == "darwin" {
handleDarwinUninstall(ctx, l)
}
}
// handleOpenWrtUninstall handles service "uninstall" command for OpenWrt.
// Exits on error. l must not be nil.
func handleOpenWrtUninstall(ctx context.Context, l *slog.Logger) {
// On OpenWrt it is important to run disable command first as it will remove
// the symlink.
_, err := runInitdCommand(ctx, "disable")
if err != nil {
l.ErrorContext(ctx, "running init disable", slogutil.KeyError, err)
os.Exit(osutil.ExitCodeFailure)
}
}
// handleDarwinUninstall handles service "uninstall" command for Darwin. l
// must not be nil.
func handleDarwinUninstall(ctx context.Context, l *slog.Logger) {
// Remove log files on cleanup and log errors.
err := os.Remove(launchdStdoutPath)
if err != nil && !errors.Is(err, os.ErrNotExist) {
@@ -422,7 +437,6 @@ func handleServiceUninstallCommand(ctx context.Context, l *slog.Logger, s servic
if err != nil && !errors.Is(err, os.ErrNotExist) {
l.WarnContext(ctx, "removing stderr file", slogutil.KeyError, err)
}
}
}
// configureService defines additional settings of the service

View File

@@ -318,26 +318,24 @@ func (web *webAPI) close(ctx context.Context) {
web.logger.InfoContext(ctx, "stopped http server")
}
// tlsServerLoop implements retry logic for http server start.
func (web *webAPI) tlsServerLoop(ctx context.Context) {
defer slogutil.RecoverAndExit(ctx, web.logger, osutil.ExitCodeFailure)
for {
web.httpsServer.cond.L.Lock()
if web.httpsServer.inShutdown {
web.httpsServer.cond.L.Unlock()
break
}
// this mechanism doesn't let us through until all conditions are met
for !web.httpsServer.enabled { // sleep until necessary data is supplied
web.httpsServer.cond.Wait()
if web.httpsServer.inShutdown {
web.httpsServer.cond.L.Unlock()
shouldContinue := web.serveTLS(ctx)
if !shouldContinue {
return
}
}
}
web.httpsServer.cond.L.Unlock()
// serveTLS initializes and starts the HTTPS server. Returns true when next
// retry is necessary.
func (web *webAPI) serveTLS(ctx context.Context) (next bool) {
if !web.waitForTLSReady() {
return false
}
var portHTTPS uint16
func() {
@@ -381,7 +379,29 @@ func (web *webAPI) tlsServerLoop(ctx context.Context) {
cleanupAlways()
panic(fmt.Errorf("https: %w", err))
}
return true
}
// waitForTLSReady blocks until the HTTPS server is enabled or a shutdown signal
// is received. Returns true when server is ready.
func (web *webAPI) waitForTLSReady() (ok bool) {
web.httpsServer.cond.L.Lock()
defer web.httpsServer.cond.L.Unlock()
if web.httpsServer.inShutdown {
return false
}
// this mechanism doesn't let us through until all conditions are met
for !web.httpsServer.enabled { // sleep until necessary data is supplied
web.httpsServer.cond.Wait()
if web.httpsServer.inShutdown {
return false
}
}
return true
}
func (web *webAPI) mustStartHTTP3(ctx context.Context, address string) {

View File

@@ -172,10 +172,6 @@ run_linter gocognit --over='20' \
./internal/querylog/ \
;
run_linter gocognit --over='19' \
./internal/home/ \
;
run_linter gocognit --over='14' \
./internal/dhcpd \
;
@@ -195,6 +191,7 @@ run_linter gocognit --over='10' \
./internal/dhcpsvc \
./internal/dnsforward/ \
./internal/filtering/ \
./internal/home/ \
./internal/ipset \
./internal/next/ \
./internal/rdns/ \