mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2024-12-18 05:41:55 +03:00
39f5c50acd
Updates #2860. Squashed commit of the following: commit 0d55a99d5c0b9f1d8c9497775dd69929e5091eaa Merge: 73a203ac8d4a4bda64
Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Thu Jun 29 16:25:36 2023 +0400 Merge remote-tracking branch 'origin/master' into http-yaml-conf commit 73a203ac8acf083fa289015e1f301d05bf320ea7 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Thu Jun 29 16:21:48 2023 +0400 home: imp docs commit a4819ace94bfe4427f70f1b8341c9babc9234740 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Thu Jun 29 11:45:30 2023 +0400 snap: imp script commit b0913c7ac5c6c46d6a73790fd57d8c5f9d7ace75 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Wed Jun 28 17:34:03 2023 +0400 all: docs commit 14820d6d56f958081d9f236277fd34f356bdab33 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Wed Jun 28 13:21:43 2023 +0400 home: imp tests commit 9db800d3ce39c36da7959e37b4a46736f4217e5c Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Wed Jun 28 13:17:34 2023 +0400 all: docs commit 9174a0ae710da51d85b4e1b1af79eda6a61dd3a2 Merge: ca8c4ae95d88181343
Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Wed Jun 28 10:19:01 2023 +0400 Merge remote-tracking branch 'origin/master' into http-yaml-conf # Conflicts: # CHANGELOG.md # internal/home/upgrade.go # internal/home/upgrade_test.go commit ca8c4ae954ece25d78ef2f873bb3ba71fa4b8fa9 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Wed Jun 28 10:07:15 2023 +0400 snap: imp script commit d84473f8e07b2c6e65023613eb4032fd01951521 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Wed Jun 28 09:59:57 2023 +0400 snap: imp script commit 8a0808e42ddbff7d9d3345d758f91b14bb4453be Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Tue Jun 27 15:03:53 2023 +0400 home: http conf commit e8fbb89cc5748f9d8fa4be9e702756bd8b869de9 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Tue Jun 27 14:59:37 2023 +0400 home: imp code commit 46541aabc421118562d564675dfd7e594d2056aa Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Tue Jun 27 12:36:14 2023 +0400 snap: bind port commit cecda5fcfd8c473db42f235b4f586b2193086997 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Tue Jun 27 12:12:39 2023 +0400 docker: bind port commit 8d8945b70366c6b018616a32421c77eb281a6ea1 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Tue Jun 27 11:06:32 2023 +0400 home: imp code commit ae5e8c1c4333d7b752c08605d80e41f55ee50e59 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Tue Jun 27 11:02:09 2023 +0400 home: imp code commit c9ee460f37e32941b84ea5fa94d21b186d6dd82b Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Mon Jun 26 17:11:10 2023 +0400 home: imp code commit 44c72445112ef38d6ec9c25b197c119edd6c959f Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Mon Jun 26 11:52:19 2023 +0400 all: docs commit e3bf5faeb748f347b1202a496788739ff9219ed0 Merge: 38cc0f639e7e638443
Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Mon Jun 26 11:39:12 2023 +0400 Merge remote-tracking branch 'origin/master' into http-yaml-conf commit 38cc0f6399040f1fa39d9da31ad6db65a6bdd4cc Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Mon Jun 26 11:38:17 2023 +0400 snap: bind port commit 3b9cb9e8cc89a67e55cecc7a2040c150f8675b4c Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Mon Jun 26 11:25:03 2023 +0400 docker: bind port ... and 4 more commits
1104 lines
28 KiB
Go
1104 lines
28 KiB
Go
// Package home contains AdGuard Home's HTTP API methods.
|
|
package home
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"io/fs"
|
|
"net"
|
|
"net/http"
|
|
"net/netip"
|
|
"net/url"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
|
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtls"
|
|
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
|
|
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering/hashprefix"
|
|
"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"
|
|
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
|
"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/slices"
|
|
"gopkg.in/natefinch/lumberjack.v2"
|
|
)
|
|
|
|
const (
|
|
// Used in config to indicate that syslog or eventlog (win) should be used for logger output
|
|
configSyslog = "syslog"
|
|
)
|
|
|
|
// Global context
|
|
type homeContext struct {
|
|
// Modules
|
|
// --
|
|
|
|
clients clientsContainer // per-client-settings module
|
|
stats stats.Interface // statistics module
|
|
queryLog querylog.QueryLog // query log module
|
|
dnsServer *dnsforward.Server // DNS module
|
|
rdns *RDNS // rDNS module
|
|
dhcpServer dhcpd.Interface // DHCP module
|
|
auth *Auth // HTTP authentication module
|
|
filters *filtering.DNSFilter // DNS filtering module
|
|
web *webAPI // Web (HTTP, HTTPS) module
|
|
tls *tlsManager // TLS module
|
|
|
|
// etcHosts contains IP-hostname mappings taken from the OS-specific hosts
|
|
// configuration files, for example /etc/hosts.
|
|
etcHosts *aghnet.HostsContainer
|
|
|
|
updater *updater.Updater
|
|
|
|
// mux is our custom http.ServeMux.
|
|
mux *http.ServeMux
|
|
|
|
// Runtime properties
|
|
// --
|
|
|
|
configFilename string // Config filename (can be overridden via the command line arguments)
|
|
workDir string // Location of our directory, used to protect against CWD being somewhere else
|
|
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
|
|
client *http.Client
|
|
appSignalChannel chan os.Signal // Channel for receiving OS signals by the console app
|
|
|
|
// whoisCh is the channel for receiving IPs for WHOIS processing.
|
|
whoisCh chan netip.Addr
|
|
|
|
// tlsCipherIDs are the ID of the cipher suites that AdGuard Home must use.
|
|
tlsCipherIDs []uint16
|
|
|
|
// disableUpdate, if true, tells AdGuard Home to not check for updates.
|
|
disableUpdate bool
|
|
|
|
// firstRun, if true, tells AdGuard Home to only start the web interface
|
|
// service, and only serve the first-run APIs.
|
|
firstRun bool
|
|
|
|
// runningAsService flag is set to true when options are passed from the service runner
|
|
runningAsService bool
|
|
}
|
|
|
|
// getDataDir returns path to the directory where we store databases and filters
|
|
func (c *homeContext) getDataDir() string {
|
|
return filepath.Join(c.workDir, dataDir)
|
|
}
|
|
|
|
// Context - a global context object
|
|
var Context homeContext
|
|
|
|
// Main is the entry point
|
|
func Main(clientBuildFS fs.FS) {
|
|
initCmdLineOpts()
|
|
|
|
// The configuration file path can be overridden, but other command-line
|
|
// options have to override config values. Therefore, do it manually
|
|
// instead of using package flag.
|
|
//
|
|
// TODO(a.garipov): The comment above is most likely false. Replace with
|
|
// package flag.
|
|
opts := loadCmdLineOpts()
|
|
|
|
Context.appSignalChannel = make(chan os.Signal)
|
|
signal.Notify(Context.appSignalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
|
|
go func() {
|
|
for {
|
|
sig := <-Context.appSignalChannel
|
|
log.Info("Received signal %q", sig)
|
|
switch sig {
|
|
case syscall.SIGHUP:
|
|
Context.clients.reloadARP()
|
|
Context.tls.reload()
|
|
default:
|
|
cleanup(context.Background())
|
|
cleanupAlways()
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
}()
|
|
|
|
if opts.serviceControlAction != "" {
|
|
handleServiceControlAction(opts, clientBuildFS)
|
|
|
|
return
|
|
}
|
|
|
|
// run the protection
|
|
run(opts, clientBuildFS)
|
|
}
|
|
|
|
// setupContext initializes [Context] fields. It also reads and upgrades
|
|
// config file if necessary.
|
|
func setupContext(opts options) (err error) {
|
|
setupContextFlags(opts)
|
|
|
|
Context.tlsRoots = aghtls.SystemRootCAs()
|
|
Context.client = &http.Client{
|
|
Timeout: time.Minute * 5,
|
|
Transport: &http.Transport{
|
|
DialContext: customDialContext,
|
|
Proxy: getHTTPProxy,
|
|
TLSClientConfig: &tls.Config{
|
|
RootCAs: Context.tlsRoots,
|
|
CipherSuites: Context.tlsCipherIDs,
|
|
MinVersion: tls.VersionTLS12,
|
|
},
|
|
},
|
|
}
|
|
|
|
Context.mux = http.NewServeMux()
|
|
|
|
if !Context.firstRun {
|
|
// Do the upgrade if necessary.
|
|
err = upgradeConfig()
|
|
if err != nil {
|
|
// Don't wrap the error, because it's informative enough as is.
|
|
return err
|
|
}
|
|
|
|
if err = parseConfig(); err != nil {
|
|
log.Error("parsing configuration file: %s", err)
|
|
|
|
os.Exit(1)
|
|
}
|
|
|
|
if opts.checkConfig {
|
|
log.Info("configuration file is ok")
|
|
|
|
os.Exit(0)
|
|
}
|
|
|
|
if !opts.noEtcHosts && config.Clients.Sources.HostsFile {
|
|
err = setupHostsContainer()
|
|
if err != nil {
|
|
// Don't wrap the error, because it's informative enough as is.
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// setupContextFlags sets global flags and prints their status to the log.
|
|
func setupContextFlags(opts options) {
|
|
Context.firstRun = detectFirstRun()
|
|
if Context.firstRun {
|
|
log.Info("This is the first time AdGuard Home is launched")
|
|
checkPermissions()
|
|
}
|
|
|
|
Context.runningAsService = opts.runningAsService
|
|
// Don't print the runningAsService flag, since that has already been done
|
|
// in [run].
|
|
|
|
Context.disableUpdate = opts.disableUpdate || version.Channel() == version.ChannelDevelopment
|
|
if Context.disableUpdate {
|
|
log.Info("AdGuard Home updates are disabled")
|
|
}
|
|
}
|
|
|
|
// logIfUnsupported logs a formatted warning if the error is one of the
|
|
// unsupported errors and returns nil. If err is nil, logIfUnsupported returns
|
|
// nil. Otherwise, it returns err.
|
|
func logIfUnsupported(msg string, err error) (outErr error) {
|
|
if errors.As(err, new(*aghos.UnsupportedError)) {
|
|
log.Debug(msg, err)
|
|
|
|
return nil
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// configureOS sets the OS-related configuration.
|
|
func configureOS(conf *configuration) (err error) {
|
|
osConf := conf.OSConfig
|
|
if osConf == nil {
|
|
return nil
|
|
}
|
|
|
|
if osConf.Group != "" {
|
|
err = aghos.SetGroup(osConf.Group)
|
|
err = logIfUnsupported("warning: setting group", err)
|
|
if err != nil {
|
|
return fmt.Errorf("setting group: %w", err)
|
|
}
|
|
|
|
log.Info("group set to %s", osConf.Group)
|
|
}
|
|
|
|
if osConf.User != "" {
|
|
err = aghos.SetUser(osConf.User)
|
|
err = logIfUnsupported("warning: setting user", err)
|
|
if err != nil {
|
|
return fmt.Errorf("setting user: %w", err)
|
|
}
|
|
|
|
log.Info("user set to %s", osConf.User)
|
|
}
|
|
|
|
if osConf.RlimitNoFile != 0 {
|
|
err = aghos.SetRlimit(osConf.RlimitNoFile)
|
|
err = logIfUnsupported("warning: setting rlimit", err)
|
|
if err != nil {
|
|
return fmt.Errorf("setting rlimit: %w", err)
|
|
}
|
|
|
|
log.Info("rlimit_nofile set to %d", osConf.RlimitNoFile)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// setupHostsContainer initializes the structures to keep up-to-date the hosts
|
|
// provided by the OS.
|
|
func setupHostsContainer() (err error) {
|
|
hostsWatcher, err := aghos.NewOSWritesWatcher()
|
|
if err != nil {
|
|
return fmt.Errorf("initing hosts watcher: %w", err)
|
|
}
|
|
|
|
Context.etcHosts, err = aghnet.NewHostsContainer(
|
|
filtering.SysHostsListID,
|
|
aghos.RootDirFS(),
|
|
hostsWatcher,
|
|
aghnet.DefaultHostsPaths()...,
|
|
)
|
|
if err != 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), closeErr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// setupOpts sets up command-line options.
|
|
func setupOpts(opts options) (err error) {
|
|
err = setupBindOpts(opts)
|
|
if err != nil {
|
|
// Don't wrap the error, because it's informative enough as is.
|
|
return err
|
|
}
|
|
|
|
if len(opts.pidFile) != 0 && writePIDFile(opts.pidFile) {
|
|
Context.pidFileName = opts.pidFile
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// initContextClients initializes Context clients and related fields.
|
|
func initContextClients() (err error) {
|
|
err = setupDNSFilteringConf(config.DNS.DnsfilterConf)
|
|
if err != nil {
|
|
// Don't wrap the error, because it's informative enough as is.
|
|
return err
|
|
}
|
|
|
|
//lint:ignore SA1019 Migration is not over.
|
|
config.DHCP.WorkDir = Context.workDir
|
|
config.DHCP.DataDir = Context.getDataDir()
|
|
config.DHCP.HTTPRegister = httpRegister
|
|
config.DHCP.ConfigModified = onConfigModified
|
|
|
|
Context.dhcpServer, err = dhcpd.Create(config.DHCP)
|
|
if Context.dhcpServer == nil || err != nil {
|
|
// TODO(a.garipov): There are a lot of places in the code right
|
|
// now which assume that the DHCP server can be nil despite this
|
|
// condition. Inspect them and perhaps rewrite them to use
|
|
// Enabled() instead.
|
|
return fmt.Errorf("initing dhcp: %w", err)
|
|
}
|
|
|
|
Context.updater = updater.NewUpdater(&updater.Config{
|
|
Client: Context.client,
|
|
Version: version.Version(),
|
|
Channel: version.Channel(),
|
|
GOARCH: runtime.GOARCH,
|
|
GOOS: runtime.GOOS,
|
|
GOARM: version.GOARM(),
|
|
GOMIPS: version.GOMIPS(),
|
|
WorkDir: Context.workDir,
|
|
ConfName: config.getConfigFilename(),
|
|
})
|
|
|
|
var arpdb aghnet.ARPDB
|
|
if config.Clients.Sources.ARP {
|
|
arpdb = aghnet.NewARPDB()
|
|
}
|
|
|
|
err = Context.clients.Init(
|
|
config.Clients.Persistent,
|
|
Context.dhcpServer,
|
|
Context.etcHosts,
|
|
arpdb,
|
|
config.DNS.DnsfilterConf,
|
|
)
|
|
if err != nil {
|
|
// Don't wrap the error, because it's informative enough as is.
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// setupBindOpts overrides bind host/port from the opts.
|
|
func setupBindOpts(opts options) (err error) {
|
|
bindAddr := opts.bindAddr
|
|
if bindAddr != (netip.AddrPort{}) {
|
|
config.HTTPConfig.Address = bindAddr
|
|
|
|
if config.HTTPConfig.Address.Port() != 0 {
|
|
err = checkPorts()
|
|
if err != nil {
|
|
// Don't wrap the error, because it's informative enough as is.
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
if opts.bindPort != 0 {
|
|
config.HTTPConfig.Address = netip.AddrPortFrom(
|
|
config.HTTPConfig.Address.Addr(),
|
|
uint16(opts.bindPort),
|
|
)
|
|
|
|
err = checkPorts()
|
|
if err != nil {
|
|
// Don't wrap the error, because it's informative enough as is.
|
|
return err
|
|
}
|
|
}
|
|
|
|
if opts.bindHost.IsValid() {
|
|
config.HTTPConfig.Address = netip.AddrPortFrom(
|
|
opts.bindHost,
|
|
config.HTTPConfig.Address.Port(),
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// setupDNSFilteringConf sets up DNS filtering configuration settings.
|
|
func setupDNSFilteringConf(conf *filtering.Config) (err error) {
|
|
const (
|
|
dnsTimeout = 3 * time.Second
|
|
|
|
sbService = "safe browsing"
|
|
defaultSafeBrowsingServer = `https://family.adguard-dns.com/dns-query`
|
|
sbTXTSuffix = `sb.dns.adguard.com.`
|
|
|
|
pcService = "parental control"
|
|
defaultParentalServer = `https://family.adguard-dns.com/dns-query`
|
|
pcTXTSuffix = `pc.dns.adguard.com.`
|
|
)
|
|
|
|
conf.EtcHosts = Context.etcHosts
|
|
conf.ConfigModified = onConfigModified
|
|
conf.HTTPRegister = httpRegister
|
|
conf.DataDir = Context.getDataDir()
|
|
conf.Filters = slices.Clone(config.Filters)
|
|
conf.WhitelistFilters = slices.Clone(config.WhitelistFilters)
|
|
conf.UserRules = slices.Clone(config.UserRules)
|
|
conf.HTTPClient = Context.client
|
|
|
|
cacheTime := time.Duration(conf.CacheTime) * time.Minute
|
|
|
|
upsOpts := &upstream.Options{
|
|
Timeout: dnsTimeout,
|
|
ServerIPAddrs: []net.IP{
|
|
{94, 140, 14, 15},
|
|
{94, 140, 15, 16},
|
|
net.ParseIP("2a10:50c0::bad1:ff"),
|
|
net.ParseIP("2a10:50c0::bad2:ff"),
|
|
},
|
|
}
|
|
|
|
sbUps, err := upstream.AddressToUpstream(defaultSafeBrowsingServer, upsOpts)
|
|
if err != nil {
|
|
return fmt.Errorf("converting safe browsing server: %w", err)
|
|
}
|
|
|
|
conf.SafeBrowsingChecker = hashprefix.New(&hashprefix.Config{
|
|
Upstream: sbUps,
|
|
ServiceName: sbService,
|
|
TXTSuffix: sbTXTSuffix,
|
|
CacheTime: cacheTime,
|
|
CacheSize: conf.SafeBrowsingCacheSize,
|
|
})
|
|
|
|
parUps, err := upstream.AddressToUpstream(defaultParentalServer, upsOpts)
|
|
if err != nil {
|
|
return fmt.Errorf("converting parental server: %w", err)
|
|
}
|
|
|
|
conf.ParentalControlChecker = hashprefix.New(&hashprefix.Config{
|
|
Upstream: parUps,
|
|
ServiceName: pcService,
|
|
TXTSuffix: pcTXTSuffix,
|
|
CacheTime: cacheTime,
|
|
CacheSize: conf.SafeBrowsingCacheSize,
|
|
})
|
|
|
|
conf.SafeSearchConf.CustomResolver = safeSearchResolver{}
|
|
conf.SafeSearch, err = safesearch.NewDefault(
|
|
conf.SafeSearchConf,
|
|
"default",
|
|
conf.SafeSearchCacheSize,
|
|
cacheTime,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("initializing safesearch: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkPorts is a helper for ports validation in config.
|
|
func checkPorts() (err error) {
|
|
tcpPorts := aghalg.UniqChecker[tcpPort]{}
|
|
addPorts(tcpPorts, tcpPort(config.HTTPConfig.Address.Port()))
|
|
|
|
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")
|
|
|
|
clientFS = os.DirFS("build/static")
|
|
} else {
|
|
clientFS, err = fs.Sub(clientBuildFS, "build/static")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getting embedded client subdir: %w", err)
|
|
}
|
|
}
|
|
|
|
webConf := webConfig{
|
|
firstRun: Context.firstRun,
|
|
BindHost: config.HTTPConfig.Address.Addr(),
|
|
BindPort: int(config.HTTPConfig.Address.Port()),
|
|
|
|
ReadTimeout: readTimeout,
|
|
ReadHeaderTimeout: readHdrTimeout,
|
|
WriteTimeout: writeTimeout,
|
|
|
|
clientFS: clientFS,
|
|
|
|
serveHTTP3: config.DNS.ServeHTTP3,
|
|
}
|
|
|
|
web = newWebAPI(&webConf)
|
|
if web == nil {
|
|
return nil, fmt.Errorf("initializing web: %w", err)
|
|
}
|
|
|
|
return web, nil
|
|
}
|
|
|
|
func fatalOnError(err error) {
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// run configures and starts AdGuard Home.
|
|
func run(opts options, clientBuildFS fs.FS) {
|
|
// Configure config filename.
|
|
initConfigFilename(opts)
|
|
|
|
// Configure working dir and config path.
|
|
err := initWorkingDir(opts)
|
|
fatalOnError(err)
|
|
|
|
// Configure log level and output.
|
|
err = configureLogger(opts)
|
|
fatalOnError(err)
|
|
|
|
// Print the first message after logger is configured.
|
|
log.Info(version.Full())
|
|
log.Debug("current working directory is %s", Context.workDir)
|
|
if opts.runningAsService {
|
|
log.Info("AdGuard Home is running as a service")
|
|
}
|
|
|
|
err = setupContext(opts)
|
|
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()
|
|
|
|
err = initContextClients()
|
|
fatalOnError(err)
|
|
|
|
err = setupOpts(opts)
|
|
fatalOnError(err)
|
|
|
|
// TODO(e.burkov): This could be made earlier, probably as the option's
|
|
// effect.
|
|
cmdlineUpdate(opts)
|
|
|
|
if !Context.firstRun {
|
|
// Save the updated config.
|
|
err = config.write()
|
|
fatalOnError(err)
|
|
|
|
if config.DebugPProf {
|
|
// TODO(a.garipov): Make the address configurable.
|
|
startPprof("localhost:6060")
|
|
}
|
|
}
|
|
|
|
dir := Context.getDataDir()
|
|
err = os.MkdirAll(dir, 0o755)
|
|
fatalOnError(errors.Annotate(err, "creating DNS data dir at %s: %w", dir))
|
|
|
|
GLMode = opts.glinetMode
|
|
|
|
// Init auth module.
|
|
Context.auth, err = initUsers()
|
|
fatalOnError(err)
|
|
|
|
Context.tls, err = newTLSManager(config.TLS)
|
|
if err != nil {
|
|
log.Error("initializing tls: %s", err)
|
|
onConfigModified()
|
|
}
|
|
|
|
Context.web, err = initWeb(opts, clientBuildFS)
|
|
fatalOnError(err)
|
|
|
|
if !Context.firstRun {
|
|
err = initDNS()
|
|
fatalOnError(err)
|
|
|
|
Context.tls.start()
|
|
|
|
go func() {
|
|
sErr := startDNSServer()
|
|
if sErr != nil {
|
|
closeDNSServer()
|
|
fatalOnError(sErr)
|
|
}
|
|
}()
|
|
|
|
if Context.dhcpServer != nil {
|
|
err = Context.dhcpServer.Start()
|
|
if err != nil {
|
|
log.Error("starting dhcp server: %s", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
Context.web.start()
|
|
|
|
// Wait indefinitely for other goroutines to complete their job.
|
|
select {}
|
|
}
|
|
|
|
// initUsers initializes context auth module. Clears config users field.
|
|
func initUsers() (auth *Auth, err error) {
|
|
sessFilename := filepath.Join(Context.getDataDir(), "sessions.db")
|
|
|
|
var rateLimiter *authRateLimiter
|
|
if config.AuthAttempts > 0 && config.AuthBlockMin > 0 {
|
|
blockDur := time.Duration(config.AuthBlockMin) * time.Minute
|
|
rateLimiter = newAuthRateLimiter(blockDur, config.AuthAttempts)
|
|
} else {
|
|
log.Info("authratelimiter is disabled")
|
|
}
|
|
|
|
sessionTTL := config.HTTPConfig.SessionTTL.Seconds()
|
|
auth = InitAuth(sessFilename, config.Users, uint32(sessionTTL), rateLimiter)
|
|
if auth == nil {
|
|
return nil, errors.Error("initializing auth module failed")
|
|
}
|
|
|
|
config.Users = nil
|
|
|
|
return auth, nil
|
|
}
|
|
|
|
func (c *configuration) anonymizer() (ipmut *aghnet.IPMut) {
|
|
var anonFunc aghnet.IPMutFunc
|
|
if c.DNS.AnonymizeClientIP {
|
|
anonFunc = querylog.AnonymizeIP
|
|
}
|
|
|
|
return aghnet.NewIPMut(anonFunc)
|
|
}
|
|
|
|
// startMods initializes and starts the DNS server after installation.
|
|
func startMods() (err error) {
|
|
err = initDNS()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
Context.tls.start()
|
|
|
|
err = startDNSServer()
|
|
if err != nil {
|
|
closeDNSServer()
|
|
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Check if the current user permissions are enough to run AdGuard Home
|
|
func checkPermissions() {
|
|
log.Info("Checking if AdGuard Home has necessary permissions")
|
|
|
|
if ok, err := aghnet.CanBindPrivilegedPorts(); !ok || err != nil {
|
|
log.Fatal("This is the first launch of AdGuard Home. You must run it as Administrator.")
|
|
}
|
|
|
|
// We should check if AdGuard Home is able to bind to port 53
|
|
err := aghnet.CheckPort("tcp", netip.AddrPortFrom(netutil.IPv4Localhost(), defaultPortDNS))
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrPermission) {
|
|
log.Fatal(`Permission check failed.
|
|
|
|
AdGuard Home is not allowed to bind to privileged ports (for instance, port 53).
|
|
Please note, that this is crucial for a server to be able to use privileged ports.
|
|
|
|
You have two options:
|
|
1. Run AdGuard Home with root privileges
|
|
2. On Linux you can grant the CAP_NET_BIND_SERVICE capability:
|
|
https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#running-without-superuser`)
|
|
}
|
|
|
|
log.Info(
|
|
"AdGuard failed to bind to port 53: %s\n\n"+
|
|
"Please note, that this is crucial for a DNS server to be able to use that port.",
|
|
err,
|
|
)
|
|
}
|
|
|
|
log.Info("AdGuard Home can bind to port 53")
|
|
}
|
|
|
|
// Write PID to a file
|
|
func writePIDFile(fn string) bool {
|
|
data := fmt.Sprintf("%d", os.Getpid())
|
|
err := os.WriteFile(fn, []byte(data), 0o644)
|
|
if err != nil {
|
|
log.Error("Couldn't write PID to file %s: %v", fn, err)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// initConfigFilename sets up context config file path. This file path can be
|
|
// overridden by command-line arguments, or is set to default.
|
|
func initConfigFilename(opts options) {
|
|
Context.configFilename = stringutil.Coalesce(opts.confFilename, "AdGuardHome.yaml")
|
|
}
|
|
|
|
// initWorkingDir initializes the workDir. If no command-line arguments are
|
|
// specified, the directory with the binary file is used.
|
|
func initWorkingDir(opts options) (err error) {
|
|
execPath, err := os.Executable()
|
|
if err != nil {
|
|
// Don't wrap the error, because it's informative enough as is.
|
|
return err
|
|
}
|
|
|
|
if opts.workDir != "" {
|
|
// If there is a custom config file, use it's directory as our working dir
|
|
Context.workDir = opts.workDir
|
|
} else {
|
|
Context.workDir = filepath.Dir(execPath)
|
|
}
|
|
|
|
workDir, err := filepath.EvalSymlinks(Context.workDir)
|
|
if err != nil {
|
|
// Don't wrap the error, because it's informative enough as is.
|
|
return err
|
|
}
|
|
|
|
Context.workDir = workDir
|
|
|
|
return nil
|
|
}
|
|
|
|
// configureLogger configures logger level and output.
|
|
func configureLogger(opts options) (err error) {
|
|
ls := getLogSettings(opts)
|
|
|
|
// Configure logger level.
|
|
if ls.Verbose {
|
|
log.SetLevel(log.DEBUG)
|
|
}
|
|
|
|
// Make sure that we see the microseconds in logs, as networking stuff can
|
|
// happen pretty quickly.
|
|
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
|
|
|
|
// Write logs to stdout by default.
|
|
if ls.File == "" {
|
|
return nil
|
|
}
|
|
|
|
if ls.File == configSyslog {
|
|
// Use syslog where it is possible and eventlog on Windows.
|
|
err = aghos.ConfigureSyslog(serviceName)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot initialize syslog: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
logFilePath := ls.File
|
|
if !filepath.IsAbs(logFilePath) {
|
|
logFilePath = filepath.Join(Context.workDir, logFilePath)
|
|
}
|
|
|
|
log.SetOutput(&lumberjack.Logger{
|
|
Filename: logFilePath,
|
|
Compress: ls.Compress,
|
|
LocalTime: ls.LocalTime,
|
|
MaxBackups: ls.MaxBackups,
|
|
MaxSize: ls.MaxSize,
|
|
MaxAge: ls.MaxAge,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// getLogSettings returns a log settings object properly initialized from opts.
|
|
func getLogSettings(opts options) (ls *logSettings) {
|
|
ls = readLogSettings()
|
|
|
|
// Command-line arguments can override config settings.
|
|
if opts.verbose || config.Verbose {
|
|
ls.Verbose = true
|
|
}
|
|
|
|
ls.File = stringutil.Coalesce(opts.logFile, config.File, ls.File)
|
|
|
|
// Handle default log settings overrides.
|
|
ls.Compress = config.Compress
|
|
ls.LocalTime = config.LocalTime
|
|
ls.MaxBackups = config.MaxBackups
|
|
ls.MaxSize = config.MaxSize
|
|
ls.MaxAge = config.MaxAge
|
|
|
|
if opts.runningAsService && ls.File == "" && runtime.GOOS == "windows" {
|
|
// When running as a Windows service, use eventlog by default if
|
|
// nothing else is configured. Otherwise, we'll lose the log output.
|
|
ls.File = configSyslog
|
|
}
|
|
|
|
return ls
|
|
}
|
|
|
|
// cleanup stops and resets all the modules.
|
|
func cleanup(ctx context.Context) {
|
|
log.Info("stopping AdGuard Home")
|
|
|
|
if Context.web != nil {
|
|
Context.web.close(ctx)
|
|
Context.web = nil
|
|
}
|
|
if Context.auth != nil {
|
|
Context.auth.Close()
|
|
Context.auth = nil
|
|
}
|
|
|
|
err := stopDNSServer()
|
|
if err != nil {
|
|
log.Error("stopping dns server: %s", err)
|
|
}
|
|
|
|
if Context.dhcpServer != nil {
|
|
err = Context.dhcpServer.Stop()
|
|
if err != nil {
|
|
log.Error("stopping dhcp server: %s", err)
|
|
}
|
|
}
|
|
|
|
if Context.etcHosts != nil {
|
|
if err = Context.etcHosts.Close(); err != nil {
|
|
log.Error("closing hosts container: %s", err)
|
|
}
|
|
}
|
|
|
|
if Context.tls != nil {
|
|
Context.tls = nil
|
|
}
|
|
}
|
|
|
|
// This function is called before application exits
|
|
func cleanupAlways() {
|
|
if len(Context.pidFileName) != 0 {
|
|
_ = os.Remove(Context.pidFileName)
|
|
}
|
|
|
|
log.Info("stopped")
|
|
}
|
|
|
|
func exitWithError() {
|
|
os.Exit(64)
|
|
}
|
|
|
|
// loadCmdLineOpts reads command line arguments and initializes configuration
|
|
// from them. If there is an error or an effect, loadCmdLineOpts processes them
|
|
// and exits.
|
|
func loadCmdLineOpts() (opts options) {
|
|
opts, eff, err := parseCmdOpts(os.Args[0], os.Args[1:])
|
|
if err != nil {
|
|
log.Error(err.Error())
|
|
printHelp(os.Args[0])
|
|
|
|
exitWithError()
|
|
}
|
|
|
|
if eff != nil {
|
|
err = eff()
|
|
if err != nil {
|
|
log.Error(err.Error())
|
|
exitWithError()
|
|
}
|
|
|
|
os.Exit(0)
|
|
}
|
|
|
|
return opts
|
|
}
|
|
|
|
// printWebAddrs prints addresses built from proto, addr, and an appropriate
|
|
// port. At least one address is printed with the value of port. Output
|
|
// example:
|
|
//
|
|
// go to http://127.0.0.1:80
|
|
func printWebAddrs(proto, addr string, port int) {
|
|
log.Printf("go to %s://%s", proto, netutil.JoinHostPort(addr, port))
|
|
}
|
|
|
|
// printHTTPAddresses prints the IP addresses which user can use to access the
|
|
// admin interface. proto is either schemeHTTP or schemeHTTPS.
|
|
func printHTTPAddresses(proto string) {
|
|
tlsConf := tlsConfigSettings{}
|
|
if Context.tls != nil {
|
|
Context.tls.WriteDiskConfig(&tlsConf)
|
|
}
|
|
|
|
port := int(config.HTTPConfig.Address.Port())
|
|
if proto == aghhttp.SchemeHTTPS {
|
|
port = tlsConf.PortHTTPS
|
|
}
|
|
|
|
// TODO(e.burkov): Inspect and perhaps merge with the previous condition.
|
|
if proto == aghhttp.SchemeHTTPS && tlsConf.ServerName != "" {
|
|
printWebAddrs(proto, tlsConf.ServerName, tlsConf.PortHTTPS)
|
|
|
|
return
|
|
}
|
|
|
|
bindHost := config.HTTPConfig.Address.Addr()
|
|
if !bindHost.IsUnspecified() {
|
|
printWebAddrs(proto, bindHost.String(), port)
|
|
|
|
return
|
|
}
|
|
|
|
ifaces, err := aghnet.GetValidNetInterfacesForWeb()
|
|
if err != nil {
|
|
log.Error("web: getting iface ips: %s", err)
|
|
// That's weird, but we'll ignore it.
|
|
//
|
|
// TODO(e.burkov): Find out when it happens.
|
|
printWebAddrs(proto, bindHost.String(), port)
|
|
|
|
return
|
|
}
|
|
|
|
for _, iface := range ifaces {
|
|
for _, addr := range iface.Addresses {
|
|
printWebAddrs(proto, addr.String(), port)
|
|
}
|
|
}
|
|
}
|
|
|
|
// -------------------
|
|
// first run / install
|
|
// -------------------
|
|
func detectFirstRun() bool {
|
|
configfile := Context.configFilename
|
|
if !filepath.IsAbs(configfile) {
|
|
configfile = filepath.Join(Context.workDir, Context.configFilename)
|
|
}
|
|
_, err := os.Stat(configfile)
|
|
return errors.Is(err, os.ErrNotExist)
|
|
}
|
|
|
|
// 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.Debug("home: customdial: dialing addr %q for network %s", addr, network)
|
|
|
|
host, port, err := net.SplitHostPort(addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dialer := &net.Dialer{
|
|
Timeout: time.Minute * 5,
|
|
}
|
|
|
|
if net.ParseIP(host) != nil || config.DNS.Port == 0 {
|
|
return dialer.DialContext(ctx, network, addr)
|
|
}
|
|
|
|
addrs, err := Context.dnsServer.Resolve(host)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolving %q: %w", host, err)
|
|
}
|
|
|
|
log.Debug("dnsServer.Resolve: %q: %v", host, addrs)
|
|
|
|
if len(addrs) == 0 {
|
|
return nil, fmt.Errorf("couldn't lookup host: %q", host)
|
|
}
|
|
|
|
var dialErrs []error
|
|
for _, a := range addrs {
|
|
addr = net.JoinHostPort(a.String(), port)
|
|
conn, err = dialer.DialContext(ctx, network, addr)
|
|
if err != nil {
|
|
dialErrs = append(dialErrs, err)
|
|
|
|
continue
|
|
}
|
|
|
|
return conn, err
|
|
}
|
|
|
|
return nil, errors.List(fmt.Sprintf("couldn't dial to %s", addr), dialErrs...)
|
|
}
|
|
|
|
func getHTTPProxy(_ *http.Request) (*url.URL, error) {
|
|
if config.ProxyURL == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
return url.Parse(config.ProxyURL)
|
|
}
|
|
|
|
// jsonError is a generic JSON error response.
|
|
//
|
|
// TODO(a.garipov): Merge together with the implementations in [dhcpd] and other
|
|
// packages after refactoring the web handler registering.
|
|
type jsonError struct {
|
|
// Message is the error message, an opaque string.
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// cmdlineUpdate updates current application and exits.
|
|
func cmdlineUpdate(opts options) {
|
|
if !opts.performUpdate {
|
|
return
|
|
}
|
|
|
|
// Initialize the DNS server to use the internal resolver which the updater
|
|
// needs to be able to resolve the update source hostname.
|
|
//
|
|
// TODO(e.burkov): We could probably initialize the internal resolver
|
|
// separately.
|
|
err := initDNSServer(nil, nil, nil, nil, nil, nil, &tlsConfigSettings{})
|
|
fatalOnError(err)
|
|
|
|
log.Info("cmdline update: performing update")
|
|
|
|
updater := Context.updater
|
|
info, err := updater.VersionInfo(true)
|
|
if err != nil {
|
|
vcu := updater.VersionCheckURL()
|
|
log.Error("getting version info from %s: %s", vcu, err)
|
|
|
|
os.Exit(1)
|
|
}
|
|
|
|
if info.NewVersion == version.Version() {
|
|
log.Info("no updates available")
|
|
|
|
os.Exit(0)
|
|
}
|
|
|
|
err = updater.Update(Context.firstRun)
|
|
fatalOnError(err)
|
|
|
|
err = restartService()
|
|
if err != nil {
|
|
log.Debug("restarting service: %s", err)
|
|
log.Info("AdGuard Home was not installed as a service. " +
|
|
"Please restart running instances of AdGuardHome manually.")
|
|
}
|
|
|
|
os.Exit(0)
|
|
}
|