Skip to content

Commit

Permalink
all: check private domains
Browse files Browse the repository at this point in the history
  • Loading branch information
EugeneOne1 committed Feb 14, 2022
1 parent b43aa86 commit 6b71848
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 109 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ and this project adheres to

### Changed

- Domain specific private reverse DNS upstream servers are now validated to
allow only `*.in-addr.arpa` and `*.ip6.arpa` domains pointing to
locally-served networks ([#3381]). **Note:** If you have invalid entries
configured, consider to remove those manually.
- Response filtering is now performed using the record types of the answer
section of messages as opposed to the type of the question ([#4238]).
- Instead of adding the build time information, the build scripts now use the
Expand Down Expand Up @@ -80,6 +84,7 @@ In this release, the schema version has changed from 12 to 13.
[#2993]: https://github.com/AdguardTeam/AdGuardHome/issues/2993
[#3057]: https://github.com/AdguardTeam/AdGuardHome/issues/3057
[#3367]: https://github.com/AdguardTeam/AdGuardHome/issues/3367
[#3381]: https://github.com/AdguardTeam/AdGuardHome/issues/3381
[#3503]: https://github.com/AdguardTeam/AdGuardHome/issues/3503
[#4216]: https://github.com/AdguardTeam/AdGuardHome/issues/4216
[#4221]: https://github.com/AdguardTeam/AdGuardHome/issues/4221
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.17

require (
github.com/AdguardTeam/dnsproxy v0.41.1
github.com/AdguardTeam/golibs v0.10.4
github.com/AdguardTeam/golibs v0.10.5
github.com/AdguardTeam/urlfilter v0.15.2
github.com/NYTimes/gziphandler v1.1.1
github.com/ameshkov/dnscrypt/v2 v2.2.3
Expand Down
3 changes: 2 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ github.com/AdguardTeam/dnsproxy v0.41.1/go.mod h1:PZ9l22h3Er+5mxFQB7oHZMTvx+aa9R
github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
github.com/AdguardTeam/golibs v0.4.2/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
github.com/AdguardTeam/golibs v0.10.3/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
github.com/AdguardTeam/golibs v0.10.4 h1:TMBkablZC0IZOpRgg9fzAKlxxNhSN2YJq7qbgtuZ7PQ=
github.com/AdguardTeam/golibs v0.10.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
github.com/AdguardTeam/golibs v0.10.5 h1:4/nl1yIBJOv5luVu9SURW8LfgOjI3zQ2moIUy/1k0y4=
github.com/AdguardTeam/golibs v0.10.5/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
github.com/AdguardTeam/gomitmproxy v0.2.0/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU=
github.com/AdguardTeam/urlfilter v0.15.2 h1:LZGgrm4l4Ys9eAqB+UUmZfiC6vHlDlYFhx0WXqo6LtQ=
github.com/AdguardTeam/urlfilter v0.15.2/go.mod h1:46YZDOV1+qtdRDuhZKVPSSp7JWWes0KayqHrKAFBdEI=
Expand Down
10 changes: 6 additions & 4 deletions internal/aghnet/subnetdetector.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@ import (

// SubnetDetector describes IP address properties.
type SubnetDetector struct {
// spNets is the slice of special-purpose address registries as defined
// by RFC-6890 (https://tools.ietf.org/html/rfc6890).
// spNets is the slice of special-purpose address registries as defined by
// the RFC 6890.
spNets []*net.IPNet

// locServedNets is the slice of locally-served networks as defined by
// RFC-6303 (https://tools.ietf.org/html/rfc6303).
// locServedNets is the slice of locally-served networks as defined by the
// RFC 6303.
locServedNets []*net.IPNet
}

// NewSubnetDetector returns a new IP detector.
//
// TODO(a.garipov): Decide whether an error is actually needed.
func NewSubnetDetector() (snd *SubnetDetector, err error) {
spNets := []string{
// "This" network.
Expand Down
216 changes: 121 additions & 95 deletions internal/dnsforward/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"time"

"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/errors"
Expand Down Expand Up @@ -41,7 +42,7 @@ type dnsConfig struct {
LocalPTRUpstreams *[]string `json:"local_ptr_upstreams"`
}

func (s *Server) getDNSConfig() dnsConfig {
func (s *Server) getDNSConfig() (c *dnsConfig) {
s.serverLock.RLock()
defer s.serverLock.RUnlock()

Expand Down Expand Up @@ -70,7 +71,7 @@ func (s *Server) getDNSConfig() dnsConfig {
upstreamMode = "parallel"
}

return dnsConfig{
return &dnsConfig{
Upstreams: &upstreams,
UpstreamsFile: &upstreamFile,
Bootstraps: &bootstraps,
Expand Down Expand Up @@ -106,7 +107,7 @@ func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
// since there is no need to omit it while decoding from JSON.
DefautLocalPTRUpstreams []string `json:"default_local_ptr_upstreams,omitempty"`
}{
dnsConfig: s.getDNSConfig(),
dnsConfig: *s.getDNSConfig(),
DefautLocalPTRUpstreams: defLocalPTRUps,
}

Expand Down Expand Up @@ -138,39 +139,63 @@ func (req *dnsConfig) checkBlockingMode() bool {
}

func (req *dnsConfig) checkUpstreamsMode() bool {
if req.UpstreamMode == nil {
return true
valid := []string{"", "fastest_addr", "parallel"}

return req.UpstreamMode == nil || stringutil.InSlice(valid, *req.UpstreamMode)
}

func (req *dnsConfig) checkBootstrap() (err error) {
if req.Bootstraps == nil {
return nil
}

for _, valid := range []string{
"",
"fastest_addr",
"parallel",
} {
if *req.UpstreamMode == valid {
return true
var b string
defer func() { err = errors.Annotate(err, "checking bootstrap %s: invalid address: %w", b) }()

for _, b = range *req.Bootstraps {
if b == "" {
return errors.Error("empty")
}

if _, err = upstream.NewResolver(b, nil); err != nil {
return err
}
}

return false
return nil
}

func (req *dnsConfig) checkBootstrap() (string, error) {
if req.Bootstraps == nil {
return "", nil
// validate returns an error if any field of req is invalid.
func (req *dnsConfig) validate(snd *aghnet.SubnetDetector) (err error) {
if req.Upstreams != nil {
err = ValidateUpstreams(*req.Upstreams, nil)
if err != nil {
return err
}
}

for _, boot := range *req.Bootstraps {
if boot == "" {
return boot, fmt.Errorf("invalid bootstrap server address: empty")
if req.LocalPTRUpstreams != nil {
err = ValidateUpstreams(*req.LocalPTRUpstreams, snd)
if err != nil {
return err
}
}

if _, err := upstream.NewResolver(boot, nil); err != nil {
return boot, fmt.Errorf("invalid bootstrap server address: %w", err)
}
err = req.checkBootstrap()
if err != nil {
return err
}

return "", nil
switch {
case !req.checkBlockingMode():
return errors.Error("blocking_mode: incorrect value")
case !req.checkUpstreamsMode():
return errors.Error("upstream_mode: incorrect value")
case !req.checkCacheTTL():
return errors.Error("cache_ttl_min must be less or equal than cache_ttl_max")
default:
return nil
}
}

func (req *dnsConfig) checkCacheTTL() bool {
Expand All @@ -190,69 +215,33 @@ func (req *dnsConfig) checkCacheTTL() bool {
}

func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) {
req := dnsConfig{}
err := json.NewDecoder(r.Body).Decode(&req)
req := &dnsConfig{}
err := json.NewDecoder(r.Body).Decode(req)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "json Encode: %s", err)

return
}

if req.Upstreams != nil {
if err = ValidateUpstreams(*req.Upstreams); err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "wrong upstreams specification: %s", err)

return
}
}

var errBoot string
if errBoot, err = req.checkBootstrap(); err != nil {
aghhttp.Error(
r,
w,
http.StatusBadRequest,
"%s can not be used as bootstrap dns cause: %s",
errBoot,
err,
)
aghhttp.Error(r, w, http.StatusBadRequest, "decoding request: %s", err)

return
}

switch {
case !req.checkBlockingMode():
aghhttp.Error(r, w, http.StatusBadRequest, "blocking_mode: incorrect value")

return
case !req.checkUpstreamsMode():
aghhttp.Error(r, w, http.StatusBadRequest, "upstream_mode: incorrect value")

return
case !req.checkCacheTTL():
aghhttp.Error(
r,
w,
http.StatusBadRequest,
"cache_ttl_min must be less or equal than cache_ttl_max",
)
err = req.validate(s.subnetDetector)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)

return
default:
// Go on.
}

restart := s.setConfig(req)
s.conf.ConfigModified()

if restart {
if err = s.Reconfigure(nil); err != nil {
err = s.Reconfigure(nil)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "%s", err)
}
}
}

func (s *Server) setConfigRestartable(dc dnsConfig) (restart bool) {
func (s *Server) setConfigRestartable(dc *dnsConfig) (restart bool) {
if dc.Upstreams != nil {
s.conf.UpstreamDNS = *dc.Upstreams
restart = true
Expand All @@ -273,9 +262,9 @@ func (s *Server) setConfigRestartable(dc dnsConfig) (restart bool) {
restart = true
}

if dc.RateLimit != nil {
restart = restart || s.conf.Ratelimit != *dc.RateLimit
if dc.RateLimit != nil && s.conf.Ratelimit != *dc.RateLimit {
s.conf.Ratelimit = *dc.RateLimit
restart = true
}

if dc.EDNSCSEnabled != nil {
Expand Down Expand Up @@ -306,7 +295,7 @@ func (s *Server) setConfigRestartable(dc dnsConfig) (restart bool) {
return restart
}

func (s *Server) setConfig(dc dnsConfig) (restart bool) {
func (s *Server) setConfig(dc *dnsConfig) (restart bool) {
s.serverLock.Lock()
defer s.serverLock.Unlock()

Expand Down Expand Up @@ -353,55 +342,92 @@ type upstreamJSON struct {
PrivateUpstreams []string `json:"private_upstream"`
}

// IsCommentOrEmpty returns true of the string starts with a "#" character or is
// an empty string. This function is useful for filtering out non-upstream
// lines from upstream configs.
// IsCommentOrEmpty returns true if s starts with a "#" character or is empty.
// This function is useful for filtering out non-upstream lines from upstream
// configs.
func IsCommentOrEmpty(s string) (ok bool) {
return len(s) == 0 || s[0] == '#'
}

// PrivatenessChecker is used to check if the IP address belongs to a local
// network.
type PrivatenessChecker interface {
// IsLocallyServedNetwork returns true if ip is contained in any of address
// registries defined by RFC 6303.
IsLocallyServedNetwork(ip net.IP) (ok bool)
}

// type check
var _ PrivatenessChecker = (*aghnet.SubnetDetector)(nil)

// checkPrivatePTR returns an error if any of keys of the domainsMap isn't a
// valid ARPA domain pointing to a locally-served network. pc must not be nil.
func checkPrivatePTR(domainsMap map[string][]upstream.Upstream, pc PrivatenessChecker) (err error) {
var errs []error

for domain := range domainsMap {
var subnet *net.IPNet
subnet, err = netutil.SubnetFromReversedAddr(domain)
if err != nil {
errs = append(errs, err)

continue
}

if !pc.IsLocallyServedNetwork(subnet.IP) {
errs = append(
errs,
fmt.Errorf("arpa domain %q should point to a locally-served network", domain),
)
}
}

if len(errs) > 0 {
return errors.List("checking domain-specific upstreams", errs...)
}

return nil
}

// ValidateUpstreams validates each upstream and returns an error if any
// upstream is invalid or if there are no default upstreams specified.
// upstream is invalid or if there are no default upstreams specified. If pc is
// not nil, it's used to check each domain of domain-specific upstreams.
//
// TODO(e.burkov): Move into aghnet or even into dnsproxy.
func ValidateUpstreams(upstreams []string) (err error) {
// No need to validate comments
// TODO(e.burkov): Move into aghnet or even into dnsproxy. Perhaps
// proxy.ParseUpstreamsConfig should validate upstreams slice already so that
// this function may be considered useless.
func ValidateUpstreams(upstreams []string, pc PrivatenessChecker) (err error) {
// No need to validate comments and empty lines.
upstreams = stringutil.FilterOut(upstreams, IsCommentOrEmpty)

// Consider this case valid because defaultDNS will be used
if len(upstreams) == 0 {
// Consider this case valid since it means the default server should be
// used.
return nil
}

_, err = proxy.ParseUpstreamsConfig(
var upsConf *proxy.UpstreamConfig
upsConf, err = proxy.ParseUpstreamsConfig(
upstreams,
&upstream.Options{
Bootstrap: []string{},
Timeout: DefaultTimeout,
},
&upstream.Options{Bootstrap: []string{}, Timeout: DefaultTimeout},
)
if err != nil {
return err
} else if len(upsConf.Upstreams) == 0 {
return errors.Error("no default upstreams specified")
}

var defaultUpstreamFound bool
for _, u := range upstreams {
var useDefault bool
useDefault, err = validateUpstream(u)
_, err = validateUpstream(u)
if err != nil {
return err
}

if !defaultUpstreamFound {
defaultUpstreamFound = useDefault
}
}

if !defaultUpstreamFound {
return fmt.Errorf("no default upstreams specified")
if pc == nil {
return nil
}

return nil
return checkPrivatePTR(upsConf.DomainReservedUpstreams, pc)
}

var protocols = []string{"tls://", "https://", "tcp://", "sdns://", "quic://"}
Expand Down
Loading

0 comments on commit 6b71848

Please sign in to comment.