Pull request 2284: AG-32257-file-permission-mitigation

Squashed commit of the following:

commit 6e0e61ec2e95a563b04a622f46c6bbe2b2e12711
Merge: e3cccc01a 5b5b39713
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Oct 2 20:51:29 2024 +0300

    Merge branch 'master' into AG-32257-file-permission-mitigation

commit e3cccc01a9cbd382cec0fcd7f3685e43acb48424
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Oct 2 19:57:32 2024 +0300

    dnsforward: imp test

commit 16ecebbc2fd2f4afe2bf475774af1786fa7a02c0
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Oct 2 19:22:10 2024 +0300

    configmigrate: imp tests

commit da8777c3a7c81e17c0d08cfff4e3a9c8d2bbd649
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Oct 2 18:58:46 2024 +0300

    all: imp types, tests

commit 58822a0ef8aa2d944a667d1ba77fe23ff52af424
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Oct 2 18:28:37 2024 +0300

    all: imp chlog

commit 8ce81f918cc5cf43972e2045532a48c829257a2f
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Oct 2 18:09:57 2024 +0300

    all: improve permissions, add safe_fs_patterns
This commit is contained in:
Ainar Garipov
2024-10-02 21:00:15 +03:00
parent 5b5b397132
commit 355cec1d7b
31 changed files with 879 additions and 52 deletions

View File

@@ -7,6 +7,7 @@ import (
"bufio"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
"path"
@@ -19,6 +20,12 @@ import (
"github.com/AdguardTeam/golibs/log"
)
// Default file and directory permissions.
const (
DefaultPermDir fs.FileMode = 0o700
DefaultPermFile fs.FileMode = 0o600
)
// Unsupported is a helper that returns a wrapped [errors.ErrUnsupported].
func Unsupported(op string) (err error) {
return fmt.Errorf("%s: not supported on %s: %w", op, runtime.GOOS, errors.ErrUnsupported)

View File

@@ -2,4 +2,4 @@
package configmigrate
// LastSchemaVersion is the most recent schema version.
const LastSchemaVersion uint = 28
const LastSchemaVersion uint = 29

View File

@@ -19,6 +19,7 @@ func TestUpgradeSchema1to2(t *testing.T) {
m := New(&Config{
WorkingDir: "",
DataDir: "",
})
err := m.migrateTo2(diskConf)

View File

@@ -10,20 +10,24 @@ import (
// Config is a the configuration for initializing a [Migrator].
type Config struct {
// WorkingDir is an absolute path to the working directory of AdGuardHome.
// WorkingDir is the absolute path to the working directory of AdGuardHome.
WorkingDir string
// DataDir is the absolute path to the data directory of AdGuardHome.
DataDir string
}
// Migrator performs the YAML configuration file migrations.
type Migrator struct {
// workingDir is an absolute path to the working directory of AdGuardHome.
workingDir string
dataDir string
}
// New creates a new Migrator.
func New(cfg *Config) (m *Migrator) {
func New(c *Config) (m *Migrator) {
return &Migrator{
workingDir: cfg.WorkingDir,
workingDir: c.WorkingDir,
dataDir: c.DataDir,
}
}
@@ -120,6 +124,7 @@ func (m *Migrator) upgradeConfigSchema(current, target uint, diskConf yobj) (err
25: migrateTo26,
26: migrateTo27,
27: migrateTo28,
28: m.migrateTo29,
}
for i, migrate := range upgrades[current:target] {

View File

@@ -1,9 +1,12 @@
package configmigrate_test
import (
"bytes"
"io/fs"
"os"
"path"
"path/filepath"
"runtime"
"testing"
"github.com/AdguardTeam/AdGuardHome/internal/configmigrate"
@@ -202,6 +205,7 @@ func TestMigrateConfig_Migrate(t *testing.T) {
migrator := configmigrate.New(&configmigrate.Config{
WorkingDir: t.Name(),
DataDir: filepath.Join(t.Name(), "data"),
})
newBody, upgraded, err := migrator.Migrate(body, tc.targetVersion)
require.NoError(t, err)
@@ -211,3 +215,43 @@ func TestMigrateConfig_Migrate(t *testing.T) {
})
}
}
// TODO(a.garipov): Consider ways of merging into the previous one.
func TestMigrateConfig_Migrate_v29(t *testing.T) {
const (
pathUnix = `/path/to/file.txt`
userDirPatUnix = `TestMigrateConfig_Migrate/v29/data/userfilters/*`
pathWindows = `C:\path\to\file.txt`
userDirPatWindows = `TestMigrateConfig_Migrate\v29\data\userfilters\*`
)
pathToReplace := pathUnix
patternToReplace := userDirPatUnix
if runtime.GOOS == "windows" {
pathToReplace = pathWindows
patternToReplace = userDirPatWindows
}
body, err := fs.ReadFile(testdata, "TestMigrateConfig_Migrate/v29/input.yml")
require.NoError(t, err)
body = bytes.ReplaceAll(body, []byte("FILEPATH"), []byte(pathToReplace))
wantBody, err := fs.ReadFile(testdata, "TestMigrateConfig_Migrate/v29/output.yml")
require.NoError(t, err)
wantBody = bytes.ReplaceAll(wantBody, []byte("FILEPATH"), []byte(pathToReplace))
wantBody = bytes.ReplaceAll(wantBody, []byte("USERFILTERSPATH"), []byte(patternToReplace))
migrator := configmigrate.New(&configmigrate.Config{
WorkingDir: t.Name(),
DataDir: "TestMigrateConfig_Migrate/v29/data",
})
newBody, upgraded, err := migrator.Migrate(body, 29)
require.NoError(t, err)
require.True(t, upgraded)
require.YAMLEq(t, string(wantBody), string(newBody))
}

View File

@@ -0,0 +1,117 @@
http:
address: 127.0.0.1:3000
session_ttl: 3h
pprof:
enabled: true
port: 6060
users:
- name: testuser
password: testpassword
dns:
bind_hosts:
- 127.0.0.1
port: 53
parental_sensitivity: 0
upstream_dns:
- tls://1.1.1.1
- tls://1.0.0.1
- quic://8.8.8.8:784
bootstrap_dns:
- 8.8.8.8:53
edns_client_subnet:
enabled: true
use_custom: false
custom_ip: ""
filtering:
filtering_enabled: true
parental_enabled: false
safebrowsing_enabled: false
safe_search:
enabled: false
bing: true
duckduckgo: true
google: true
pixabay: true
yandex: true
youtube: true
protection_enabled: true
blocked_services:
schedule:
time_zone: Local
ids:
- 500px
blocked_response_ttl: 10
filters:
- url: https://adaway.org/hosts.txt
name: AdAway
enabled: false
- url: FILEPATH
name: Local Filter
enabled: false
clients:
persistent:
- name: localhost
ids:
- 127.0.0.1
- aa:aa:aa:aa:aa:aa
use_global_settings: true
use_global_blocked_services: true
filtering_enabled: false
parental_enabled: false
safebrowsing_enabled: false
safe_search:
enabled: true
bing: true
duckduckgo: true
google: true
pixabay: true
yandex: true
youtube: true
blocked_services:
schedule:
time_zone: Local
ids:
- 500px
runtime_sources:
whois: true
arp: true
rdns: true
dhcp: true
hosts: true
dhcp:
enabled: false
interface_name: vboxnet0
local_domain_name: local
dhcpv4:
gateway_ip: 192.168.0.1
subnet_mask: 255.255.255.0
range_start: 192.168.0.10
range_end: 192.168.0.250
lease_duration: 1234
icmp_timeout_msec: 10
schema_version: 28
user_rules: []
querylog:
enabled: true
file_enabled: true
interval: 720h
size_memory: 1000
ignored:
- '|.^'
statistics:
enabled: true
interval: 240h
ignored:
- '|.^'
os:
group: ''
rlimit_nofile: 123
user: ''
log:
file: ""
max_backups: 0
max_size: 100
max_age: 3
compress: true
local_time: false
verbose: true

View File

@@ -0,0 +1,120 @@
http:
address: 127.0.0.1:3000
session_ttl: 3h
pprof:
enabled: true
port: 6060
users:
- name: testuser
password: testpassword
dns:
bind_hosts:
- 127.0.0.1
port: 53
parental_sensitivity: 0
upstream_dns:
- tls://1.1.1.1
- tls://1.0.0.1
- quic://8.8.8.8:784
bootstrap_dns:
- 8.8.8.8:53
edns_client_subnet:
enabled: true
use_custom: false
custom_ip: ""
filtering:
filtering_enabled: true
parental_enabled: false
safebrowsing_enabled: false
safe_fs_patterns:
- USERFILTERSPATH
- FILEPATH
safe_search:
enabled: false
bing: true
duckduckgo: true
google: true
pixabay: true
yandex: true
youtube: true
protection_enabled: true
blocked_services:
schedule:
time_zone: Local
ids:
- 500px
blocked_response_ttl: 10
filters:
- url: https://adaway.org/hosts.txt
name: AdAway
enabled: false
- url: FILEPATH
name: Local Filter
enabled: false
clients:
persistent:
- name: localhost
ids:
- 127.0.0.1
- aa:aa:aa:aa:aa:aa
use_global_settings: true
use_global_blocked_services: true
filtering_enabled: false
parental_enabled: false
safebrowsing_enabled: false
safe_search:
enabled: true
bing: true
duckduckgo: true
google: true
pixabay: true
yandex: true
youtube: true
blocked_services:
schedule:
time_zone: Local
ids:
- 500px
runtime_sources:
whois: true
arp: true
rdns: true
dhcp: true
hosts: true
dhcp:
enabled: false
interface_name: vboxnet0
local_domain_name: local
dhcpv4:
gateway_ip: 192.168.0.1
subnet_mask: 255.255.255.0
range_start: 192.168.0.10
range_end: 192.168.0.250
lease_duration: 1234
icmp_timeout_msec: 10
schema_version: 29
user_rules: []
querylog:
enabled: true
file_enabled: true
interval: 720h
size_memory: 1000
ignored:
- '|.^'
statistics:
enabled: true
interval: 240h
ignored:
- '|.^'
os:
group: ''
rlimit_nofile: 123
user: ''
log:
file: ""
max_backups: 0
max_size: 100
max_age: 3
compress: true
local_time: false
verbose: true

View File

@@ -0,0 +1,63 @@
package configmigrate
import (
"fmt"
"path/filepath"
)
// migrateTo29 performs the following changes:
//
// # BEFORE:
// 'filters':
// - 'enabled': true
// 'url': /path/to/file.txt
// 'name': My FS Filter
// 'id': 1234
//
// # AFTER:
// 'filters':
// - 'enabled': true
// 'url': /path/to/file.txt
// 'name': My FS Filter
// 'id': 1234
// # …
// 'filtering':
// 'safe_fs_patterns':
// - '/opt/AdGuardHome/data/userfilters/*'
// - '/path/to/file.txt'
// # …
func (m Migrator) migrateTo29(diskConf yobj) (err error) {
diskConf["schema_version"] = 29
filterVals, ok, err := fieldVal[[]any](diskConf, "filters")
if !ok {
return err
}
paths := []string{
filepath.Join(m.dataDir, "userfilters", "*"),
}
for i, v := range filterVals {
var f yobj
f, ok = v.(yobj)
if !ok {
return fmt.Errorf("filters: at index %d: expected object, got %T", i, v)
}
var u string
u, ok, _ = fieldVal[string](f, "url")
if ok && filepath.IsAbs(u) {
paths = append(paths, u)
}
}
fltConf, ok, err := fieldVal[yobj](diskConf, "filtering")
if !ok {
return err
}
fltConf["safe_fs_patterns"] = paths
return nil
}

View File

@@ -12,6 +12,7 @@ import (
"strings"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
@@ -185,7 +186,7 @@ func writeDB(path string, leases []*dbLease) (err error) {
return err
}
err = maybe.WriteFile(path, buf, 0o644)
err = maybe.WriteFile(path, buf, aghos.DefaultPermFile)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err

View File

@@ -584,7 +584,11 @@ func TestSafeSearch(t *testing.T) {
req := createTestMessage(tc.host)
var reply *dns.Msg
reply, _, err = client.Exchange(req, addr)
require.Eventually(t, func() (ok bool) {
reply, _, err = client.Exchange(req, addr)
return err == nil
}, testTimeout*10, testTimeout)
require.NoErrorf(t, err, "couldn't talk to server %s: %s", addr, err)
if tc.wantCNAME != "" {

View File

@@ -11,6 +11,7 @@ import (
"strings"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/AdGuardHome/internal/aghrenameio"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
"github.com/AdguardTeam/golibs/container"
@@ -448,11 +449,7 @@ func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
var res *rulelist.ParseResult
// Change the default 0o600 permission to something more acceptable by end
// users.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/3198.
tmpFile, err := aghrenameio.NewPendingFile(flt.Path(d.conf.DataDir), 0o644)
tmpFile, err := aghrenameio.NewPendingFile(flt.Path(d.conf.DataDir), aghos.DefaultPermFile)
if err != nil {
return false, err
}
@@ -522,6 +519,11 @@ func (d *DNSFilter) reader(fltURL string) (r io.ReadCloser, err error) {
return r, nil
}
fltURL = filepath.Clean(fltURL)
if !pathMatchesAny(d.safeFSPatterns, fltURL) {
return nil, fmt.Errorf("path %q does not match safe patterns", fltURL)
}
r, err = os.Open(fltURL)
if err != nil {
return nil, fmt.Errorf("opening file: %w", err)

View File

@@ -19,6 +19,7 @@ import (
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
"github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors"
@@ -130,6 +131,10 @@ type Config struct {
// UserRules is the global list of custom rules.
UserRules []string `yaml:"-"`
// SafeFSPatterns are the patterns for matching which local filtering-rule
// files can be added.
SafeFSPatterns []string `yaml:"safe_fs_patterns"`
SafeBrowsingCacheSize uint `yaml:"safebrowsing_cache_size"` // (in bytes)
SafeSearchCacheSize uint `yaml:"safesearch_cache_size"` // (in bytes)
ParentalCacheSize uint `yaml:"parental_cache_size"` // (in bytes)
@@ -257,6 +262,8 @@ type DNSFilter struct {
refreshLock *sync.Mutex
hostCheckers []hostChecker
safeFSPatterns []string
}
// Filter represents a filter list
@@ -987,13 +994,22 @@ func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) {
d = &DNSFilter{
idGen: newIDGenerator(int32(time.Now().Unix())),
bufPool: syncutil.NewSlicePool[byte](rulelist.DefaultRuleBufSize),
safeSearch: c.SafeSearch,
refreshLock: &sync.Mutex{},
safeBrowsingChecker: c.SafeBrowsingChecker,
parentalControlChecker: c.ParentalControlChecker,
confMu: &sync.RWMutex{},
}
d.safeSearch = c.SafeSearch
for i, p := range c.SafeFSPatterns {
// Use Match to validate the patterns here.
_, err = filepath.Match(p, "test")
if err != nil {
return nil, fmt.Errorf("safe_fs_patterns: at index %d: %w", i, err)
}
d.safeFSPatterns = append(d.safeFSPatterns, p)
}
d.hostCheckers = []hostChecker{{
check: d.matchSysHosts,
@@ -1022,7 +1038,7 @@ func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) {
err = d.prepareRewrites()
if err != nil {
return nil, fmt.Errorf("rewrites: preparing: %s", err)
return nil, fmt.Errorf("rewrites: preparing: %w", err)
}
if d.conf.BlockedServices != nil {
@@ -1037,11 +1053,16 @@ func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) {
if err != nil {
d.Close()
return nil, fmt.Errorf("initializing filtering subsystem: %s", err)
return nil, fmt.Errorf("initializing filtering subsystem: %w", err)
}
}
_ = os.MkdirAll(filepath.Join(d.conf.DataDir, filterDir), 0o755)
err = os.MkdirAll(filepath.Join(d.conf.DataDir, filterDir), aghos.DefaultPermDir)
if err != nil {
d.Close()
return nil, fmt.Errorf("making filtering directory: %w", err)
}
d.loadFilters(d.conf.Filters)
d.loadFilters(d.conf.WhitelistFilters)

View File

@@ -20,14 +20,22 @@ import (
)
// validateFilterURL validates the filter list URL or file name.
func validateFilterURL(urlStr string) (err error) {
func (d *DNSFilter) validateFilterURL(urlStr string) (err error) {
defer func() { err = errors.Annotate(err, "checking filter: %w") }()
if filepath.IsAbs(urlStr) {
urlStr = filepath.Clean(urlStr)
_, err = os.Stat(urlStr)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
}
// Don't wrap the error since it's informative enough as is.
return err
if !pathMatchesAny(d.safeFSPatterns, urlStr) {
return fmt.Errorf("path %q does not match safe patterns", urlStr)
}
return nil
}
u, err := url.ParseRequestURI(urlStr)
@@ -65,7 +73,7 @@ func (d *DNSFilter) handleFilteringAddURL(w http.ResponseWriter, r *http.Request
return
}
err = validateFilterURL(fj.URL)
err = d.validateFilterURL(fj.URL)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
@@ -225,7 +233,7 @@ func (d *DNSFilter) handleFilteringSetURL(w http.ResponseWriter, r *http.Request
return
}
err = validateFilterURL(fj.Data.URL)
err = d.validateFilterURL(fj.Data.URL)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "invalid url: %s", err)

View File

@@ -0,0 +1,37 @@
package filtering
import (
"fmt"
"path/filepath"
)
// pathMatchesAny returns true if filePath matches one of globs. globs must be
// valid. filePath must be absolute and clean. If globs are empty,
// pathMatchesAny returns false.
//
// TODO(a.garipov): Move to golibs?
func pathMatchesAny(globs []string, filePath string) (ok bool) {
if len(globs) == 0 {
return false
}
clean, err := filepath.Abs(filePath)
if err != nil {
panic(fmt.Errorf("pathMatchesAny: %w", err))
} else if clean != filePath {
panic(fmt.Errorf("pathMatchesAny: filepath %q is not absolute", filePath))
}
for _, g := range globs {
ok, err = filepath.Match(g, filePath)
if err != nil {
panic(fmt.Errorf("pathMatchesAny: bad pattern: %w", err))
}
if ok {
return true
}
}
return false
}

View File

@@ -0,0 +1,78 @@
//go:build unix
package filtering
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPathInAnyDir(t *testing.T) {
t.Parallel()
const (
filePath = "/path/to/file.txt"
filePathGlob = "/path/to/*"
otherFilePath = "/otherpath/to/file.txt"
)
testCases := []struct {
want assert.BoolAssertionFunc
filePath string
name string
globs []string
}{{
want: assert.False,
filePath: filePath,
name: "nil_pats",
globs: nil,
}, {
want: assert.True,
filePath: filePath,
name: "match",
globs: []string{
filePath,
otherFilePath,
},
}, {
want: assert.False,
filePath: filePath,
name: "no_match",
globs: []string{
otherFilePath,
},
}, {
want: assert.True,
filePath: filePath,
name: "match_star",
globs: []string{
filePathGlob,
},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
tc.want(t, pathMatchesAny(tc.globs, tc.filePath))
})
}
require.True(t, t.Run("panic_on_unabs_file_path", func(t *testing.T) {
t.Parallel()
assert.Panics(t, func() {
_ = pathMatchesAny([]string{"/home/user"}, "../../etc/passwd")
})
}))
require.True(t, t.Run("panic_on_bad_pat", func(t *testing.T) {
t.Parallel()
assert.Panics(t, func() {
_ = pathMatchesAny([]string{`\`}, filePath)
})
}))
}

View File

@@ -0,0 +1,73 @@
//go:build windows
package filtering
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPathInAnyDir(t *testing.T) {
t.Parallel()
const (
filePath = `C:\path\to\file.txt`
filePathGlob = `C:\path\to\*`
otherFilePath = `C:\otherpath\to\file.txt`
)
testCases := []struct {
want assert.BoolAssertionFunc
filePath string
name string
globs []string
}{{
want: assert.False,
filePath: filePath,
name: "nil_pats",
globs: nil,
}, {
want: assert.True,
filePath: filePath,
name: "match",
globs: []string{
filePath,
otherFilePath,
},
}, {
want: assert.False,
filePath: filePath,
name: "no_match",
globs: []string{
otherFilePath,
},
}, {
want: assert.True,
filePath: filePath,
name: "match_star",
globs: []string{
filePathGlob,
},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
tc.want(t, pathMatchesAny(tc.globs, tc.filePath))
})
}
require.True(t, t.Run("panic_on_unabs_file_path", func(t *testing.T) {
t.Parallel()
assert.Panics(t, func() {
_ = pathMatchesAny([]string{`C:\home\user`}, `..\..\etc\passwd`)
})
}))
// TODO(a.garipov): See if there is anything for which filepath.Match
// returns ErrBadPattern on Windows.
}

View File

@@ -11,6 +11,7 @@ import (
"path/filepath"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/AdGuardHome/internal/aghrenameio"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/ioutil"
@@ -196,7 +197,7 @@ func (f *Filter) readFromHTTP(
return "", nil, fmt.Errorf("got status code %d, want %d", resp.StatusCode, http.StatusOK)
}
fltFile, err := aghrenameio.NewPendingFile(cachePath, 0o644)
fltFile, err := aghrenameio.NewPendingFile(cachePath, aghos.DefaultPermFile)
if err != nil {
return "", nil, fmt.Errorf("creating temp file: %w", err)
}
@@ -271,7 +272,7 @@ func parseIntoCache(
filePath string,
cachePath string,
) (parseRes *ParseResult, err error) {
tmpFile, err := aghrenameio.NewPendingFile(cachePath, 0o644)
tmpFile, err := aghrenameio.NewPendingFile(cachePath, aghos.DefaultPermFile)
if err != nil {
return nil, fmt.Errorf("creating temp file: %w", err)
}

View File

@@ -9,6 +9,7 @@ import (
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
@@ -89,7 +90,7 @@ func InitAuth(
trustedProxies: trustedProxies,
}
var err error
a.db, err = bbolt.Open(dbFilename, 0o644, nil)
a.db, err = bbolt.Open(dbFilename, aghos.DefaultPermFile, nil)
if err != nil {
log.Error("auth: open DB: %s: %s", dbFilename, err)
if err.Error() == "invalid argument" {

View File

@@ -9,6 +9,7 @@ import (
"sync"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/AdGuardHome/internal/aghtls"
"github.com/AdguardTeam/AdGuardHome/internal/configmigrate"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
@@ -26,9 +27,15 @@ import (
yaml "gopkg.in/yaml.v3"
)
// dataDir is the name of a directory under the working one to store some
// persistent data.
const dataDir = "data"
const (
// dataDir is the name of a directory under the working one to store some
// persistent data.
dataDir = "data"
// userFilterDataDir is the name of the directory used to store users'
// FS-based rule lists.
userFilterDataDir = "userfilters"
)
// logSettings are the logging settings part of the configuration file.
type logSettings struct {
@@ -520,6 +527,7 @@ func parseConfig() (err error) {
migrator := configmigrate.New(&configmigrate.Config{
WorkingDir: Context.workDir,
DataDir: Context.getDataDir(),
})
var upgraded bool
@@ -534,7 +542,7 @@ func parseConfig() (err error) {
confPath := configFilePath()
log.Debug("writing config file %q after config upgrade", confPath)
err = maybe.WriteFile(confPath, config.fileData, 0o644)
err = maybe.WriteFile(confPath, config.fileData, aghos.DefaultPermFile)
if err != nil {
return fmt.Errorf("writing new config: %w", err)
}
@@ -700,7 +708,7 @@ func (c *configuration) write() (err error) {
return fmt.Errorf("generating config file: %w", err)
}
err = maybe.WriteFile(confPath, buf.Bytes(), 0o644)
err = maybe.WriteFile(confPath, buf.Bytes(), aghos.DefaultPermFile)
if err != nil {
return fmt.Errorf("writing config file: %w", err)
}

View File

@@ -270,9 +270,9 @@ DNSStubListener=no
const resolvConfPath = "/etc/resolv.conf"
// Deactivate DNSStubListener
func disableDNSStubListener() error {
func disableDNSStubListener() (err error) {
dir := filepath.Dir(resolvedConfPath)
err := os.MkdirAll(dir, 0o755)
err = os.MkdirAll(dir, 0o755)
if err != nil {
return fmt.Errorf("os.MkdirAll: %s: %w", dir, err)
}
@@ -413,9 +413,12 @@ func (web *webAPI) handleInstallConfigure(w http.ResponseWriter, r *http.Request
copyInstallSettings(curConfig, config)
Context.firstRun = false
config.HTTPConfig.Address = netip.AddrPortFrom(req.Web.IP, req.Web.Port)
config.DNS.BindHosts = []netip.Addr{req.DNS.IP}
config.DNS.Port = req.DNS.Port
config.Filtering.SafeFSPatterns = []string{
filepath.Join(Context.workDir, userFilterDataDir, "*"),
}
config.HTTPConfig.Address = netip.AddrPortFrom(req.Web.IP, req.Web.Port)
u := &webUser{
Name: req.Username,

View File

@@ -47,14 +47,9 @@ func onConfigModified() {
// initDNS updates all the fields of the [Context] needed to initialize the DNS
// server and initializes it at last. It also must not be called unless
// [config] and [Context] are initialized. l must not be nil.
func initDNS(l *slog.Logger) (err error) {
func initDNS(l *slog.Logger, statsDir, querylogDir string) (err error) {
anonymizer := config.anonymizer()
statsDir, querylogDir, err := checkStatsAndQuerylogDirs(&Context, config)
if err != nil {
return err
}
statsConf := stats.Config{
Logger: l.With(slogutil.KeyPrefix, "stats"),
Filename: filepath.Join(statsDir, "stats.db"),

View File

@@ -31,6 +31,7 @@ import (
"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/permcheck"
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
"github.com/AdguardTeam/AdGuardHome/internal/stats"
"github.com/AdguardTeam/AdGuardHome/internal/updater"
@@ -630,9 +631,9 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
}
}
dir := Context.getDataDir()
err = os.MkdirAll(dir, 0o755)
fatalOnError(errors.Annotate(err, "creating DNS data dir at %s: %w", dir))
dataDir := Context.getDataDir()
err = os.MkdirAll(dataDir, aghos.DefaultPermDir)
fatalOnError(errors.Annotate(err, "creating DNS data dir at %s: %w", dataDir))
GLMode = opts.glinetMode
@@ -649,8 +650,11 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
Context.web, err = initWeb(opts, clientBuildFS, upd, slogLogger)
fatalOnError(err)
statsDir, querylogDir, err := checkStatsAndQuerylogDirs(&Context, config)
fatalOnError(err)
if !Context.firstRun {
err = initDNS(slogLogger)
err = initDNS(slogLogger, statsDir, querylogDir)
fatalOnError(err)
Context.tls.start()
@@ -671,6 +675,12 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
}
}
if permcheck.NeedsMigration(confPath) {
permcheck.Migrate(Context.workDir, dataDir, statsDir, querylogDir, confPath)
}
permcheck.Check(Context.workDir, dataDir, statsDir, querylogDir, confPath)
Context.web.start()
// Wait for other goroutines to complete their job.
@@ -714,7 +724,12 @@ func (c *configuration) anonymizer() (ipmut *aghnet.IPMut) {
// startMods initializes and starts the DNS server after installation. l must
// not be nil.
func startMods(l *slog.Logger) (err error) {
err = initDNS(l)
statsDir, querylogDir, err := checkStatsAndQuerylogDirs(&Context, config)
if err != nil {
return err
}
err = initDNS(l, statsDir, querylogDir)
if err != nil {
return err
}

View File

@@ -14,6 +14,7 @@ import (
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
@@ -182,7 +183,7 @@ func (m *Manager) write() (err error) {
return fmt.Errorf("encoding: %w", err)
}
err = maybe.WriteFile(m.fileName, b, 0o644)
err = maybe.WriteFile(m.fileName, b, aghos.DefaultPermFile)
if err != nil {
return fmt.Errorf("writing: %w", err)
}

View File

@@ -0,0 +1,93 @@
package permcheck
import (
"io/fs"
"os"
"path/filepath"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
)
// NeedsMigration returns true if AdGuard Home files need permission migration.
//
// TODO(a.garipov): Consider ways to detect this better.
func NeedsMigration(confFilePath string) (ok bool) {
s, err := os.Stat(confFilePath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
// Likely a first run. Don't check.
return false
}
log.Error("permcheck: checking if files need migration: %s", err)
// Unexpected error. Try to migrate just in case.
return true
}
return s.Mode().Perm() != aghos.DefaultPermFile
}
// Migrate attempts to change the permissions of AdGuard Home's files. It logs
// the results at an appropriate level.
func Migrate(workDir, dataDir, statsDir, querylogDir, confFilePath string) {
chmodDir(workDir)
chmodFile(confFilePath)
// TODO(a.garipov): Put all paths in one place and remove this duplication.
chmodDir(dataDir)
chmodDir(filepath.Join(dataDir, "filters"))
chmodFile(filepath.Join(dataDir, "sessions.db"))
chmodFile(filepath.Join(dataDir, "leases.json"))
if dataDir != querylogDir {
chmodDir(querylogDir)
}
chmodFile(filepath.Join(querylogDir, "querylog.json"))
chmodFile(filepath.Join(querylogDir, "querylog.json.1"))
if dataDir != statsDir {
chmodDir(statsDir)
}
chmodFile(filepath.Join(statsDir, "stats.db"))
}
// chmodDir changes the permissions of a single directory. The results are
// logged at the appropriate level.
func chmodDir(dirPath string) {
chmodPath(dirPath, typeDir, aghos.DefaultPermDir)
}
// chmodFile changes the permissions of a single file. The results are logged
// at the appropriate level.
func chmodFile(filePath string) {
chmodPath(filePath, typeFile, aghos.DefaultPermFile)
}
// chmodPath changes the permissions of a single filesystem entity. The results
// are logged at the appropriate level.
func chmodPath(entPath, fileType string, fm fs.FileMode) {
err := os.Chmod(entPath, fm)
if err == nil {
log.Info("permcheck: changed permissions for %s %q", fileType, entPath)
return
} else if errors.Is(err, os.ErrNotExist) {
log.Debug("permcheck: changing permissions for %s %q: %s", fileType, entPath, err)
return
}
log.Error(
"permcheck: SECURITY WARNING: cannot change permissions for %s %q to %#o: %s; "+
"this can leave your system vulnerable, see "+
"https://adguard-dns.io/kb/adguard-home/running-securely/#os-service-concerns",
fileType,
entPath,
fm,
err,
)
}

View File

@@ -0,0 +1,86 @@
// Package permcheck contains code for simplifying permissions checks on files
// and directories.
//
// TODO(a.garipov): Improve the approach on Windows.
package permcheck
import (
"io/fs"
"os"
"path/filepath"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
)
// File type constants for logging.
const (
typeDir = "directory"
typeFile = "file"
)
// Check checks the permissions on important files. It logs the results at
// appropriate levels.
func Check(workDir, dataDir, statsDir, querylogDir, confFilePath string) {
checkDir(workDir)
checkFile(confFilePath)
// TODO(a.garipov): Put all paths in one place and remove this duplication.
checkDir(dataDir)
checkDir(filepath.Join(dataDir, "filters"))
checkFile(filepath.Join(dataDir, "sessions.db"))
checkFile(filepath.Join(dataDir, "leases.json"))
if dataDir != querylogDir {
checkDir(querylogDir)
}
checkFile(filepath.Join(querylogDir, "querylog.json"))
checkFile(filepath.Join(querylogDir, "querylog.json.1"))
if dataDir != statsDir {
checkDir(statsDir)
}
checkFile(filepath.Join(statsDir, "stats.db"))
}
// checkDir checks the permissions of a single directory. The results are
// logged at the appropriate level.
func checkDir(dirPath string) {
checkPath(dirPath, typeDir, aghos.DefaultPermDir)
}
// checkFile checks the permissions of a single file. The results are logged at
// the appropriate level.
func checkFile(filePath string) {
checkPath(filePath, typeFile, aghos.DefaultPermFile)
}
// checkPath checks the permissions of a single filesystem entity. The results
// are logged at the appropriate level.
func checkPath(entPath, fileType string, want fs.FileMode) {
s, err := os.Stat(entPath)
if err != nil {
logFunc := log.Error
if errors.Is(err, os.ErrNotExist) {
logFunc = log.Debug
}
logFunc("permcheck: checking %s %q: %s", fileType, entPath, err)
return
}
// TODO(a.garipov): Add a more fine-grained check and result reporting.
perm := s.Mode().Perm()
if perm != want {
log.Info(
"permcheck: SECURITY WARNING: %s %q has unexpected permissions %#o; want %#o",
fileType,
entPath,
perm,
want,
)
}
}

View File

@@ -8,6 +8,7 @@ import (
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
)
@@ -56,7 +57,7 @@ type qLogFile struct {
// newQLogFile initializes a new instance of the qLogFile.
func newQLogFile(path string) (qf *qLogFile, err error) {
f, err := os.OpenFile(path, os.O_RDONLY, 0o644)
f, err := os.OpenFile(path, os.O_RDONLY, aghos.DefaultPermFile)
if err != nil {
return nil, err
}

View File

@@ -7,6 +7,7 @@ import (
"os"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
)
@@ -70,7 +71,7 @@ func (l *queryLog) flushToFile(b *bytes.Buffer) (err error) {
filename := l.logFile
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644)
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, aghos.DefaultPermFile)
if err != nil {
return fmt.Errorf("creating file %q: %w", filename, err)
}

View File

@@ -15,6 +15,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/timeutil"
@@ -383,7 +384,7 @@ func (s *StatsCtx) openDB() (err error) {
s.logger.Debug("opening database")
var db *bbolt.DB
db, err = bbolt.Open(s.filename, 0o644, nil)
db, err = bbolt.Open(s.filename, aghos.DefaultPermFile, nil)
if err != nil {
if err.Error() == "invalid argument" {
const lines = `AdGuard Home cannot be initialized due to an incompatible file system.

View File

@@ -15,6 +15,7 @@ import (
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/AdGuardHome/internal/version"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/ioutil"
@@ -263,7 +264,7 @@ func (u *Updater) check() (err error) {
// ignores the configuration file if firstRun is true.
func (u *Updater) backup(firstRun bool) (err error) {
log.Debug("updater: backing up current configuration")
_ = os.Mkdir(u.backupDir, 0o755)
_ = os.Mkdir(u.backupDir, aghos.DefaultPermDir)
if !firstRun {
err = copyFile(u.confName, filepath.Join(u.backupDir, "AdGuardHome.yaml"))
if err != nil {
@@ -337,10 +338,10 @@ func (u *Updater) downloadPackageFile() (err error) {
return fmt.Errorf("io.ReadAll() failed: %w", err)
}
_ = os.Mkdir(u.updateDir, 0o755)
_ = os.Mkdir(u.updateDir, aghos.DefaultPermDir)
log.Debug("updater: saving package to file")
err = os.WriteFile(u.packageName, body, 0o644)
err = os.WriteFile(u.packageName, body, aghos.DefaultPermFile)
if err != nil {
return fmt.Errorf("os.WriteFile() failed: %w", err)
}
@@ -527,7 +528,7 @@ func copyFile(src, dst string) error {
if e != nil {
return e
}
e = os.WriteFile(dst, d, 0o644)
e = os.WriteFile(dst, d, aghos.DefaultPermFile)
if e != nil {
return e
}