mirror of https://github.com/0xERR0R/blocky.git
parent
5427c1697c
commit
28789ee7fe
|
@ -58,7 +58,11 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func initConfig() {
|
func initConfig() {
|
||||||
config.LoadConfig(configPath, false)
|
err := config.LoadConfig(configPath, false)
|
||||||
|
if err != nil {
|
||||||
|
util.FatalOnError("unable to load configuration: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
log.ConfigureLogger(config.GetConfig().LogLevel, config.GetConfig().LogFormat, config.GetConfig().LogTimestamp)
|
log.ConfigureLogger(config.GetConfig().LogLevel, config.GetConfig().LogFormat, config.GetConfig().LogTimestamp)
|
||||||
|
|
||||||
if len(config.GetConfig().HTTPPorts) != 0 {
|
if len(config.GetConfig().HTTPPorts) != 0 {
|
||||||
|
|
|
@ -33,7 +33,11 @@ func newServeCommand() *cobra.Command {
|
||||||
func startServer(_ *cobra.Command, _ []string) {
|
func startServer(_ *cobra.Command, _ []string) {
|
||||||
printBanner()
|
printBanner()
|
||||||
|
|
||||||
config.LoadConfig(configPath, true)
|
err := config.LoadConfig(configPath, true)
|
||||||
|
if err != nil {
|
||||||
|
util.FatalOnError("unable to load configuration: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
log.ConfigureLogger(config.GetConfig().LogLevel, config.GetConfig().LogFormat, config.GetConfig().LogTimestamp)
|
log.ConfigureLogger(config.GetConfig().LogLevel, config.GetConfig().LogFormat, config.GetConfig().LogTimestamp)
|
||||||
|
|
||||||
configureHTTPClient(config.GetConfig())
|
configureHTTPClient(config.GetConfig())
|
||||||
|
|
|
@ -8,11 +8,15 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
|
||||||
"github.com/hako/durafmt"
|
"github.com/hako/durafmt"
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
|
||||||
"github.com/0xERR0R/blocky/log"
|
"github.com/0xERR0R/blocky/log"
|
||||||
"github.com/creasty/defaults"
|
"github.com/creasty/defaults"
|
||||||
|
@ -36,6 +40,12 @@ type NetProtocol uint16
|
||||||
// )
|
// )
|
||||||
type QueryLogType int16
|
type QueryLogType int16
|
||||||
|
|
||||||
|
type QType dns.Type
|
||||||
|
|
||||||
|
func (c QType) String() string {
|
||||||
|
return dns.TypeToString[uint16(c)]
|
||||||
|
}
|
||||||
|
|
||||||
type Duration time.Duration
|
type Duration time.Duration
|
||||||
|
|
||||||
func (c *Duration) String() string {
|
func (c *Duration) String() string {
|
||||||
|
@ -170,6 +180,30 @@ func (c *Duration) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *QType) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
var input string
|
||||||
|
if err := unmarshal(&input); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
t, found := dns.StringToType[input]
|
||||||
|
if !found {
|
||||||
|
types := make([]string, 0, len(dns.StringToType))
|
||||||
|
for k := range dns.StringToType {
|
||||||
|
types = append(types, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(types)
|
||||||
|
|
||||||
|
return fmt.Errorf("unknown DNS query type: '%s'. Please use following types '%s'",
|
||||||
|
input, strings.Join(types, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
*c = QType(t)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var validDomain = regexp.MustCompile(
|
var validDomain = regexp.MustCompile(
|
||||||
`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`)
|
`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`)
|
||||||
|
|
||||||
|
@ -270,11 +304,13 @@ type Config struct {
|
||||||
HTTPPorts ListenConfig `yaml:"httpPort"`
|
HTTPPorts ListenConfig `yaml:"httpPort"`
|
||||||
HTTPSPorts ListenConfig `yaml:"httpsPort"`
|
HTTPSPorts ListenConfig `yaml:"httpsPort"`
|
||||||
TLSPorts ListenConfig `yaml:"tlsPort"`
|
TLSPorts ListenConfig `yaml:"tlsPort"`
|
||||||
|
// Deprecated
|
||||||
DisableIPv6 bool `yaml:"disableIPv6" default:"false"`
|
DisableIPv6 bool `yaml:"disableIPv6" default:"false"`
|
||||||
CertFile string `yaml:"certFile"`
|
CertFile string `yaml:"certFile"`
|
||||||
KeyFile string `yaml:"keyFile"`
|
KeyFile string `yaml:"keyFile"`
|
||||||
BootstrapDNS Upstream `yaml:"bootstrapDns"`
|
BootstrapDNS Upstream `yaml:"bootstrapDns"`
|
||||||
HostsFile HostsFileConfig `yaml:"hostsFile"`
|
HostsFile HostsFileConfig `yaml:"hostsFile"`
|
||||||
|
Filtering FilteringConfig `yaml:"filtering"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrometheusConfig contains the config values for prometheus
|
// PrometheusConfig contains the config values for prometheus
|
||||||
|
@ -375,14 +411,18 @@ type HostsFileConfig struct {
|
||||||
RefreshPeriod Duration `yaml:"refreshPeriod" default:"1h"`
|
RefreshPeriod Duration `yaml:"refreshPeriod" default:"1h"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FilteringConfig struct {
|
||||||
|
QueryTypes []QType `yaml:"queryTypes"`
|
||||||
|
}
|
||||||
|
|
||||||
// nolint:gochecknoglobals
|
// nolint:gochecknoglobals
|
||||||
var config = &Config{}
|
var config = &Config{}
|
||||||
|
|
||||||
// LoadConfig creates new config from YAML file
|
// LoadConfig creates new config from YAML file
|
||||||
func LoadConfig(path string, mandatory bool) {
|
func LoadConfig(path string, mandatory bool) error {
|
||||||
cfg := Config{}
|
cfg := Config{}
|
||||||
if err := defaults.Set(&cfg); err != nil {
|
if err := defaults.Set(&cfg); err != nil {
|
||||||
log.Log().Fatal("Can't apply default values: ", err)
|
return fmt.Errorf("can't apply default values: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := ioutil.ReadFile(path)
|
data, err := ioutil.ReadFile(path)
|
||||||
|
@ -392,34 +432,47 @@ func LoadConfig(path string, mandatory bool) {
|
||||||
// config file does not exist
|
// config file does not exist
|
||||||
// return config with default values
|
// return config with default values
|
||||||
config = &cfg
|
config = &cfg
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Log().Fatal("Can't read config file: ", err)
|
return fmt.Errorf("can't read config file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
unmarshalConfig(data, cfg)
|
return unmarshalConfig(data, cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func unmarshalConfig(data []byte, cfg Config) {
|
func unmarshalConfig(data []byte, cfg Config) error {
|
||||||
err := yaml.UnmarshalStrict(data, &cfg)
|
err := yaml.UnmarshalStrict(data, &cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Log().Fatal("wrong file structure: ", err)
|
return fmt.Errorf("wrong file structure: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
validateConfig(&cfg)
|
err = validateConfig(&cfg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to validate config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
config = &cfg
|
config = &cfg
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateConfig(cfg *Config) {
|
func validateConfig(cfg *Config) (err error) {
|
||||||
if len(cfg.TLSPorts) != 0 && (cfg.CertFile == "" || cfg.KeyFile == "") {
|
if len(cfg.TLSPorts) != 0 && (cfg.CertFile == "" || cfg.KeyFile == "") {
|
||||||
log.Log().Fatal("certFile and keyFile parameters are mandatory for TLS")
|
err = multierror.Append(err, errors.New("'certFile' and 'keyFile' parameters are mandatory for TLS"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cfg.HTTPSPorts) != 0 && (cfg.CertFile == "" || cfg.KeyFile == "") {
|
if len(cfg.HTTPSPorts) != 0 && (cfg.CertFile == "" || cfg.KeyFile == "") {
|
||||||
log.Log().Fatal("certFile and keyFile parameters are mandatory for HTTPS")
|
err = multierror.Append(err, errors.New("'certFile' and 'keyFile' parameters are mandatory for HTTPS"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.DisableIPv6 {
|
||||||
|
log.Log().Warnf("'disableIPv6' is deprecated. Please use 'filtering.queryTypes' with 'AAAA' instead.")
|
||||||
|
|
||||||
|
cfg.Filtering.QueryTypes = append(cfg.Filtering.QueryTypes, QType(dns.TypeAAAA))
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConfig returns the current config
|
// GetConfig returns the current config
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0xERR0R/blocky/helpertest"
|
"github.com/miekg/dns"
|
||||||
|
|
||||||
. "github.com/0xERR0R/blocky/log"
|
. "github.com/0xERR0R/blocky/log"
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
@ -21,7 +21,8 @@ var _ = Describe("Config", func() {
|
||||||
err := os.Chdir("../testdata")
|
err := os.Chdir("../testdata")
|
||||||
Expect(err).Should(Succeed())
|
Expect(err).Should(Succeed())
|
||||||
|
|
||||||
LoadConfig("config.yml", true)
|
err = LoadConfig("config.yml", true)
|
||||||
|
Expect(err).Should(Succeed())
|
||||||
|
|
||||||
Expect(config.DNSPorts).Should(Equal(ListenConfig{"55553", ":55554", "[::1]:55555"}))
|
Expect(config.DNSPorts).Should(Equal(ListenConfig{"55553", ":55554", "[::1]:55555"}))
|
||||||
Expect(config.Upstream.ExternalResolvers["default"]).Should(HaveLen(3))
|
Expect(config.Upstream.ExternalResolvers["default"]).Should(HaveLen(3))
|
||||||
|
@ -44,6 +45,7 @@ var _ = Describe("Config", func() {
|
||||||
Expect(config.Blocking.ClientGroupsBlock).Should(HaveLen(2))
|
Expect(config.Blocking.ClientGroupsBlock).Should(HaveLen(2))
|
||||||
Expect(config.Blocking.BlockTTL).Should(Equal(Duration(time.Minute)))
|
Expect(config.Blocking.BlockTTL).Should(Equal(Duration(time.Minute)))
|
||||||
Expect(config.Blocking.RefreshPeriod).Should(Equal(Duration(2 * time.Hour)))
|
Expect(config.Blocking.RefreshPeriod).Should(Equal(Duration(2 * time.Hour)))
|
||||||
|
Expect(config.Filtering.QueryTypes).Should(HaveLen(2))
|
||||||
|
|
||||||
Expect(config.Caching.MaxCachingTime).Should(Equal(Duration(0)))
|
Expect(config.Caching.MaxCachingTime).Should(Equal(Duration(0)))
|
||||||
Expect(config.Caching.MinCachingTime).Should(Equal(Duration(0)))
|
Expect(config.Caching.MinCachingTime).Should(Equal(Duration(0)))
|
||||||
|
@ -53,7 +55,7 @@ var _ = Describe("Config", func() {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
When("config file is malformed", func() {
|
When("config file is malformed", func() {
|
||||||
It("should log with fatal and exit", func() {
|
It("should return error", func() {
|
||||||
|
|
||||||
dir, err := ioutil.TempDir("", "blocky")
|
dir, err := ioutil.TempDir("", "blocky")
|
||||||
defer os.Remove(dir)
|
defer os.Remove(dir)
|
||||||
|
@ -63,48 +65,48 @@ var _ = Describe("Config", func() {
|
||||||
err = ioutil.WriteFile("config.yml", []byte("malformed_config"), 0600)
|
err = ioutil.WriteFile("config.yml", []byte("malformed_config"), 0600)
|
||||||
Expect(err).Should(Succeed())
|
Expect(err).Should(Succeed())
|
||||||
|
|
||||||
helpertest.ShouldLogFatal(func() {
|
err = LoadConfig("config.yml", true)
|
||||||
LoadConfig("config.yml", true)
|
Expect(err).Should(HaveOccurred())
|
||||||
})
|
Expect(err.Error()).Should(ContainSubstring("wrong file structure"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
When("duration is in wrong format", func() {
|
When("duration is in wrong format", func() {
|
||||||
It("should log with fatal and exit", func() {
|
It("should return error", func() {
|
||||||
cfg := Config{}
|
cfg := Config{}
|
||||||
data :=
|
data :=
|
||||||
`blocking:
|
`blocking:
|
||||||
refreshPeriod: wrongduration`
|
refreshPeriod: wrongduration`
|
||||||
helpertest.ShouldLogFatal(func() {
|
err := unmarshalConfig([]byte(data), cfg)
|
||||||
unmarshalConfig([]byte(data), cfg)
|
Expect(err).Should(HaveOccurred())
|
||||||
})
|
Expect(err.Error()).Should(ContainSubstring("invalid duration \"wrongduration\""))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
When("CustomDNS hast wrong IP defined", func() {
|
When("CustomDNS hast wrong IP defined", func() {
|
||||||
It("should log with fatal and exit", func() {
|
It("should return error", func() {
|
||||||
cfg := Config{}
|
cfg := Config{}
|
||||||
data :=
|
data :=
|
||||||
`customDNS:
|
`customDNS:
|
||||||
mapping:
|
mapping:
|
||||||
someDomain: 192.168.178.WRONG`
|
someDomain: 192.168.178.WRONG`
|
||||||
helpertest.ShouldLogFatal(func() {
|
err := unmarshalConfig([]byte(data), cfg)
|
||||||
unmarshalConfig([]byte(data), cfg)
|
Expect(err).Should(HaveOccurred())
|
||||||
})
|
Expect(err.Error()).Should(ContainSubstring("invalid IP address '192.168.178.WRONG'"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
When("Conditional mapping hast wrong defined upstreams", func() {
|
When("Conditional mapping hast wrong defined upstreams", func() {
|
||||||
It("should log with fatal and exit", func() {
|
It("should return error", func() {
|
||||||
cfg := Config{}
|
cfg := Config{}
|
||||||
data :=
|
data :=
|
||||||
`conditional:
|
`conditional:
|
||||||
mapping:
|
mapping:
|
||||||
multiple.resolvers: 192.168.178.1,wrongprotocol:4.4.4.4:53`
|
multiple.resolvers: 192.168.178.1,wrongprotocol:4.4.4.4:53`
|
||||||
helpertest.ShouldLogFatal(func() {
|
err := unmarshalConfig([]byte(data), cfg)
|
||||||
unmarshalConfig([]byte(data), cfg)
|
Expect(err).Should(HaveOccurred())
|
||||||
})
|
Expect(err.Error()).Should(ContainSubstring("wrong host name 'wrongprotocol:4.4.4.4:53'"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
When("Wrong upstreams are defined", func() {
|
When("Wrong upstreams are defined", func() {
|
||||||
It("should log with fatal and exit", func() {
|
It("should return error", func() {
|
||||||
cfg := Config{}
|
cfg := Config{}
|
||||||
data :=
|
data :=
|
||||||
`upstream:
|
`upstream:
|
||||||
|
@ -112,21 +114,32 @@ var _ = Describe("Config", func() {
|
||||||
- 8.8.8.8
|
- 8.8.8.8
|
||||||
- wrongprotocol:8.8.4.4
|
- wrongprotocol:8.8.4.4
|
||||||
- 1.1.1.1`
|
- 1.1.1.1`
|
||||||
helpertest.ShouldLogFatal(func() {
|
err := unmarshalConfig([]byte(data), cfg)
|
||||||
unmarshalConfig([]byte(data), cfg)
|
Expect(err).Should(HaveOccurred())
|
||||||
})
|
Expect(err.Error()).Should(ContainSubstring("can't convert upstream 'wrongprotocol:8.8.4.4'"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
When("config is not YAML", func() {
|
When("config is not YAML", func() {
|
||||||
It("should log with fatal and exit", func() {
|
It("should return error", func() {
|
||||||
cfg := Config{}
|
cfg := Config{}
|
||||||
data :=
|
data :=
|
||||||
`///`
|
`///`
|
||||||
helpertest.ShouldLogFatal(func() {
|
err := unmarshalConfig([]byte(data), cfg)
|
||||||
unmarshalConfig([]byte(data), cfg)
|
Expect(err).Should(HaveOccurred())
|
||||||
|
Expect(err.Error()).Should(ContainSubstring("cannot unmarshal !!str `///`"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
When("Validation fails", func() {
|
||||||
|
It("should return error", func() {
|
||||||
|
cfg := Config{}
|
||||||
|
data :=
|
||||||
|
`httpsPort: 443`
|
||||||
|
err := unmarshalConfig([]byte(data), cfg)
|
||||||
|
Expect(err).Should(HaveOccurred())
|
||||||
|
Expect(err.Error()).Should(ContainSubstring("'certFile' and 'keyFile' parameters are mandatory for HTTPS"))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
When("TlsPort is defined", func() {
|
When("TlsPort is defined", func() {
|
||||||
|
@ -136,9 +149,9 @@ var _ = Describe("Config", func() {
|
||||||
c := &Config{
|
c := &Config{
|
||||||
TLSPorts: ListenConfig{"953"},
|
TLSPorts: ListenConfig{"953"},
|
||||||
}
|
}
|
||||||
helpertest.ShouldLogFatal(func() {
|
err := validateConfig(c)
|
||||||
validateConfig(c)
|
Expect(err).Should(HaveOccurred())
|
||||||
})
|
Expect(err.Error()).Should(ContainSubstring("'certFile' and 'keyFile' parameters are mandatory for TLS"))
|
||||||
})
|
})
|
||||||
|
|
||||||
By("certFile/keyFile set", func() {
|
By("certFile/keyFile set", func() {
|
||||||
|
@ -147,11 +160,24 @@ var _ = Describe("Config", func() {
|
||||||
KeyFile: "key",
|
KeyFile: "key",
|
||||||
CertFile: "cert",
|
CertFile: "cert",
|
||||||
}
|
}
|
||||||
validateConfig(c)
|
err := validateConfig(c)
|
||||||
|
Expect(err).Should(Succeed())
|
||||||
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
When("Deprecated parameter 'disableIPv6' is set", func() {
|
||||||
|
It("should add 'AAAA' to filter.queryTypes", func() {
|
||||||
|
c := &Config{
|
||||||
|
DisableIPv6: true,
|
||||||
|
}
|
||||||
|
err := validateConfig(c)
|
||||||
|
Expect(err).Should(Succeed())
|
||||||
|
Expect(c.Filtering.QueryTypes).Should(ContainElements(QType(dns.TypeAAAA)))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
When("HttpsPort is defined", func() {
|
When("HttpsPort is defined", func() {
|
||||||
It("certFile/keyFile must be set", func() {
|
It("certFile/keyFile must be set", func() {
|
||||||
|
|
||||||
|
@ -159,9 +185,9 @@ var _ = Describe("Config", func() {
|
||||||
c := &Config{
|
c := &Config{
|
||||||
HTTPSPorts: ListenConfig{"443"},
|
HTTPSPorts: ListenConfig{"443"},
|
||||||
}
|
}
|
||||||
helpertest.ShouldLogFatal(func() {
|
err := validateConfig(c)
|
||||||
validateConfig(c)
|
Expect(err).Should(HaveOccurred())
|
||||||
})
|
Expect(err.Error()).Should(ContainSubstring("'certFile' and 'keyFile' parameters are mandatory for HTTPS"))
|
||||||
})
|
})
|
||||||
|
|
||||||
By("certFile/keyFile set", func() {
|
By("certFile/keyFile set", func() {
|
||||||
|
@ -170,32 +196,30 @@ var _ = Describe("Config", func() {
|
||||||
KeyFile: "key",
|
KeyFile: "key",
|
||||||
CertFile: "cert",
|
CertFile: "cert",
|
||||||
}
|
}
|
||||||
validateConfig(c)
|
err := validateConfig(c)
|
||||||
|
Expect(err).Should(Succeed())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
When("config directory does not exist", func() {
|
When("config directory does not exist", func() {
|
||||||
It("should log with fatal and exit if config is mandatory", func() {
|
It("should return error", func() {
|
||||||
err := os.Chdir("../..")
|
err := os.Chdir("../..")
|
||||||
Expect(err).Should(Succeed())
|
Expect(err).Should(Succeed())
|
||||||
|
|
||||||
defer func() { Log().ExitFunc = nil }()
|
err = LoadConfig("config.yml", true)
|
||||||
|
Expect(err).Should(HaveOccurred())
|
||||||
|
Expect(err.Error()).Should(ContainSubstring("no such file or directory"))
|
||||||
|
|
||||||
var fatal bool
|
|
||||||
|
|
||||||
Log().ExitFunc = func(int) { fatal = true }
|
|
||||||
LoadConfig("config.yml", true)
|
|
||||||
|
|
||||||
Expect(fatal).Should(BeTrue())
|
|
||||||
})
|
})
|
||||||
|
|
||||||
It("should use default config if config is not mandatory", func() {
|
It("should use default config if config is not mandatory", func() {
|
||||||
err := os.Chdir("../..")
|
err := os.Chdir("../..")
|
||||||
Expect(err).Should(Succeed())
|
Expect(err).Should(Succeed())
|
||||||
|
|
||||||
LoadConfig("config.yml", false)
|
err = LoadConfig("config.yml", false)
|
||||||
|
|
||||||
|
Expect(err).Should(Succeed())
|
||||||
Expect(config.LogLevel).Should(Equal(LevelInfo))
|
Expect(config.LogLevel).Should(Equal(LevelInfo))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -318,6 +342,35 @@ var _ = Describe("Config", func() {
|
||||||
Expect(err).Should(MatchError("some err"))
|
Expect(err).Should(MatchError("some err"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
Context("QueryTyoe", func() {
|
||||||
|
It("Should parse existing DNS type as string", func() {
|
||||||
|
t := QType(0)
|
||||||
|
err := t.UnmarshalYAML(func(i interface{}) error {
|
||||||
|
*i.(*string) = "AAAA"
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).Should(Succeed())
|
||||||
|
Expect(t).Should(Equal(QType(dns.TypeAAAA)))
|
||||||
|
Expect(t.String()).Should(Equal("AAAA"))
|
||||||
|
})
|
||||||
|
It("should fail if DNS type does not exist", func() {
|
||||||
|
t := QType(0)
|
||||||
|
err := t.UnmarshalYAML(func(i interface{}) error {
|
||||||
|
*i.(*string) = "WRONGTYPE"
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).Should(HaveOccurred())
|
||||||
|
Expect(err.Error()).Should(ContainSubstring("unknown DNS query type: 'WRONGTYPE'"))
|
||||||
|
})
|
||||||
|
It("should fail if wrong YAML format", func() {
|
||||||
|
d := QType(0)
|
||||||
|
err := d.UnmarshalYAML(func(i interface{}) error {
|
||||||
|
return errors.New("some err")
|
||||||
|
})
|
||||||
|
Expect(err).Should(HaveOccurred())
|
||||||
|
Expect(err).Should(MatchError("some err"))
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
DescribeTable("parse upstream string",
|
DescribeTable("parse upstream string",
|
||||||
|
|
|
@ -190,8 +190,12 @@ httpPort: 4000
|
||||||
#keyFile: server.key
|
#keyFile: server.key
|
||||||
# optional: use this DNS server to resolve blacklist urls and upstream DNS servers. Useful if no DNS resolver is configured and blocky needs to resolve a host name. Format net:IP:port, net must be udp or tcp
|
# optional: use this DNS server to resolve blacklist urls and upstream DNS servers. Useful if no DNS resolver is configured and blocky needs to resolve a host name. Format net:IP:port, net must be udp or tcp
|
||||||
bootstrapDns: tcp:1.1.1.1
|
bootstrapDns: tcp:1.1.1.1
|
||||||
# optional: Drop all AAAA query if set to true. Default: false
|
|
||||||
disableIPv6: false
|
filtering:
|
||||||
|
# optional: drop all queries with following query types. Default: empty
|
||||||
|
queryTypes:
|
||||||
|
- AAAA
|
||||||
|
|
||||||
# optional: if path defined, use this file for query resolution (A, AAAA and rDNS). Default: empty
|
# optional: if path defined, use this file for query resolution (A, AAAA and rDNS). Default: empty
|
||||||
hostsFile:
|
hostsFile:
|
||||||
# optional: Path to hosts file (e.g. /etc/hosts on Linux)
|
# optional: Path to hosts file (e.g. /etc/hosts on Linux)
|
||||||
|
|
|
@ -20,7 +20,6 @@ configuration properties as [JSON](config.yml).
|
||||||
| certFile | path | yes, if httpsPort > 0 | | Path to cert and key file for SSL encryption (DoH and DoT) |
|
| certFile | path | yes, if httpsPort > 0 | | Path to cert and key file for SSL encryption (DoH and DoT) |
|
||||||
| keyFile | path | yes, if httpsPort > 0 | | Path to cert and key file for SSL encryption (DoH and DoT)
|
| keyFile | path | yes, if httpsPort > 0 | | Path to cert and key file for SSL encryption (DoH and DoT)
|
||||||
| bootstrapDns | IP:port | no | | Use this DNS server to resolve blacklist urls and upstream DNS servers. Useful if no DNS resolver is configured and blocky needs to resolve a host name. NOTE: Works only on Linux/*Nix OS due to golang limitations under windows. |
|
| bootstrapDns | IP:port | no | | Use this DNS server to resolve blacklist urls and upstream DNS servers. Useful if no DNS resolver is configured and blocky needs to resolve a host name. NOTE: Works only on Linux/*Nix OS due to golang limitations under windows. |
|
||||||
| disableIPv6 | bool | no | false | Drop all AAAA query if set to true |
|
|
||||||
| logLevel | enum (debug, info, warn, error) | no | info | Log level |
|
| logLevel | enum (debug, info, warn, error) | no | info | Log level |
|
||||||
| logFormat | enum (text, json) | no | text | Log format (text or json). |
|
| logFormat | enum (text, json) | no | text | Log format (text or json). |
|
||||||
| logTimestamp | bool | no | true | Log time stamps (true or false). |
|
| logTimestamp | bool | no | true | Log time stamps (true or false). |
|
||||||
|
@ -109,6 +108,21 @@ value by setting the `upstreamTimeout` configuration parameter (in **duration fo
|
||||||
upstreamTimeout: 5s
|
upstreamTimeout: 5s
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Filtering
|
||||||
|
|
||||||
|
Under certain circumstances, it may be useful to filter some types of DNS queries. You can define one or more DNS query
|
||||||
|
types, all queries with these types will be dropped (empty answer will be returned).
|
||||||
|
|
||||||
|
!!! example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
filtering:
|
||||||
|
queryTypes:
|
||||||
|
- AAAA
|
||||||
|
```
|
||||||
|
|
||||||
|
This configuration will drop all 'AAAA' (IPv6) queries.
|
||||||
|
|
||||||
## Custom DNS
|
## Custom DNS
|
||||||
|
|
||||||
You can define your own domain name to IP mappings. For example, you can use a user-friendly name for a network printer
|
You can define your own domain name to IP mappings. For example, you can use a user-friendly name for a network printer
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
// CONDITIONAL // the query was resolved by the conditional upstream resolver
|
// CONDITIONAL // the query was resolved by the conditional upstream resolver
|
||||||
// CUSTOMDNS // the query was resolved by a custom rule
|
// CUSTOMDNS // the query was resolved by a custom rule
|
||||||
// HOSTSFILE // the query was resolved by looking up the hosts file
|
// HOSTSFILE // the query was resolved by looking up the hosts file
|
||||||
|
// FILTERED // the query was filtered by query type
|
||||||
// )
|
// )
|
||||||
type ResponseType int
|
type ResponseType int
|
||||||
|
|
||||||
|
|
|
@ -95,9 +95,12 @@ const (
|
||||||
// ResponseTypeHOSTSFILE is a ResponseType of type HOSTSFILE.
|
// ResponseTypeHOSTSFILE is a ResponseType of type HOSTSFILE.
|
||||||
// the query was resolved by looking up the hosts file
|
// the query was resolved by looking up the hosts file
|
||||||
ResponseTypeHOSTSFILE
|
ResponseTypeHOSTSFILE
|
||||||
|
// ResponseTypeFILTERED is a ResponseType of type FILTERED.
|
||||||
|
// the query was filtered by query type
|
||||||
|
ResponseTypeFILTERED
|
||||||
)
|
)
|
||||||
|
|
||||||
const _ResponseTypeName = "RESOLVEDCACHEDBLOCKEDCONDITIONALCUSTOMDNSHOSTSFILE"
|
const _ResponseTypeName = "RESOLVEDCACHEDBLOCKEDCONDITIONALCUSTOMDNSHOSTSFILEFILTERED"
|
||||||
|
|
||||||
var _ResponseTypeNames = []string{
|
var _ResponseTypeNames = []string{
|
||||||
_ResponseTypeName[0:8],
|
_ResponseTypeName[0:8],
|
||||||
|
@ -106,6 +109,7 @@ var _ResponseTypeNames = []string{
|
||||||
_ResponseTypeName[21:32],
|
_ResponseTypeName[21:32],
|
||||||
_ResponseTypeName[32:41],
|
_ResponseTypeName[32:41],
|
||||||
_ResponseTypeName[41:50],
|
_ResponseTypeName[41:50],
|
||||||
|
_ResponseTypeName[50:58],
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResponseTypeNames returns a list of possible string values of ResponseType.
|
// ResponseTypeNames returns a list of possible string values of ResponseType.
|
||||||
|
@ -122,6 +126,7 @@ var _ResponseTypeMap = map[ResponseType]string{
|
||||||
ResponseTypeCONDITIONAL: _ResponseTypeName[21:32],
|
ResponseTypeCONDITIONAL: _ResponseTypeName[21:32],
|
||||||
ResponseTypeCUSTOMDNS: _ResponseTypeName[32:41],
|
ResponseTypeCUSTOMDNS: _ResponseTypeName[32:41],
|
||||||
ResponseTypeHOSTSFILE: _ResponseTypeName[41:50],
|
ResponseTypeHOSTSFILE: _ResponseTypeName[41:50],
|
||||||
|
ResponseTypeFILTERED: _ResponseTypeName[50:58],
|
||||||
}
|
}
|
||||||
|
|
||||||
// String implements the Stringer interface.
|
// String implements the Stringer interface.
|
||||||
|
@ -139,6 +144,7 @@ var _ResponseTypeValue = map[string]ResponseType{
|
||||||
_ResponseTypeName[21:32]: ResponseTypeCONDITIONAL,
|
_ResponseTypeName[21:32]: ResponseTypeCONDITIONAL,
|
||||||
_ResponseTypeName[32:41]: ResponseTypeCUSTOMDNS,
|
_ResponseTypeName[32:41]: ResponseTypeCUSTOMDNS,
|
||||||
_ResponseTypeName[41:50]: ResponseTypeHOSTSFILE,
|
_ResponseTypeName[41:50]: ResponseTypeHOSTSFILE,
|
||||||
|
_ResponseTypeName[50:58]: ResponseTypeFILTERED,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseResponseType attempts to convert a string to a ResponseType.
|
// ParseResponseType attempts to convert a string to a ResponseType.
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
package resolver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/0xERR0R/blocky/config"
|
||||||
|
"github.com/0xERR0R/blocky/model"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FilteringResolver filters DNS queries (for example can drop all AAAA query)
|
||||||
|
// returns empty ANSWER with NOERROR
|
||||||
|
type FilteringResolver struct {
|
||||||
|
NextResolver
|
||||||
|
queryTypes map[config.QType]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FilteringResolver) Resolve(request *model.Request) (*model.Response, error) {
|
||||||
|
qType := request.Req.Question[0].Qtype
|
||||||
|
if _, found := r.queryTypes[config.QType(qType)]; found {
|
||||||
|
response := new(dns.Msg)
|
||||||
|
response.SetRcode(request.Req, dns.RcodeSuccess)
|
||||||
|
|
||||||
|
return &model.Response{Res: response, RType: model.ResponseTypeFILTERED}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.next.Resolve(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FilteringResolver) Configuration() (result []string) {
|
||||||
|
qTypes := make([]string, len(r.queryTypes))
|
||||||
|
ix := 0
|
||||||
|
|
||||||
|
for qType := range r.queryTypes {
|
||||||
|
qTypes[ix] = qType.String()
|
||||||
|
ix++
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(qTypes)
|
||||||
|
|
||||||
|
result = append(result, fmt.Sprintf("filtering query Types: '%v'", strings.Join(qTypes, ", ")))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFilteringResolver(cfg config.FilteringConfig) ChainedResolver {
|
||||||
|
queryTypes := make(map[config.QType]bool, len(cfg.QueryTypes))
|
||||||
|
for _, queryType := range cfg.QueryTypes {
|
||||||
|
queryTypes[queryType] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return &FilteringResolver{
|
||||||
|
queryTypes: queryTypes,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
package resolver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/0xERR0R/blocky/config"
|
||||||
|
. "github.com/0xERR0R/blocky/model"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("FilteringResolver", func() {
|
||||||
|
var (
|
||||||
|
sut *FilteringResolver
|
||||||
|
sutConfig config.FilteringConfig
|
||||||
|
m *MockResolver
|
||||||
|
mockAnswer *dns.Msg
|
||||||
|
)
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
mockAnswer = new(dns.Msg)
|
||||||
|
})
|
||||||
|
|
||||||
|
JustBeforeEach(func() {
|
||||||
|
sut = NewFilteringResolver(sutConfig).(*FilteringResolver)
|
||||||
|
m = &MockResolver{}
|
||||||
|
m.On("Resolve", mock.Anything).Return(&Response{Res: mockAnswer}, nil)
|
||||||
|
sut.Next(m)
|
||||||
|
})
|
||||||
|
|
||||||
|
When("Filtering query types are defined", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
sutConfig = config.FilteringConfig{
|
||||||
|
QueryTypes: []config.QType{config.QType(dns.TypeAAAA), config.QType(dns.TypeMX)},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
It("Should delegate to next resolver if request query has other type", func() {
|
||||||
|
resp, err := sut.Resolve(newRequest("example.com", dns.TypeA))
|
||||||
|
Expect(err).Should(Succeed())
|
||||||
|
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeSuccess))
|
||||||
|
Expect(resp.RType).Should(Equal(ResponseTypeRESOLVED))
|
||||||
|
Expect(resp.Res.Answer).Should(BeEmpty())
|
||||||
|
// delegated to next resolver
|
||||||
|
Expect(m.Calls).Should(HaveLen(1))
|
||||||
|
})
|
||||||
|
It("Should return empty answer for defined query type", func() {
|
||||||
|
resp, err := sut.Resolve(newRequest("example.com", dns.TypeAAAA))
|
||||||
|
Expect(err).Should(Succeed())
|
||||||
|
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeSuccess))
|
||||||
|
Expect(resp.RType).Should(Equal(ResponseTypeFILTERED))
|
||||||
|
Expect(resp.Res.Answer).Should(BeEmpty())
|
||||||
|
|
||||||
|
// no call of next resolver
|
||||||
|
Expect(m.Calls).Should(BeZero())
|
||||||
|
})
|
||||||
|
It("Configure should output all query types", func() {
|
||||||
|
c := sut.Configuration()
|
||||||
|
Expect(c).Should(HaveLen(1))
|
||||||
|
Expect(c[0]).Should(Equal("filtering query Types: 'AAAA, MX'"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
When("No filtering query types are defined", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
sutConfig = config.FilteringConfig{}
|
||||||
|
})
|
||||||
|
It("Should return empty answer without error", func() {
|
||||||
|
resp, err := sut.Resolve(newRequest("example.com", dns.TypeAAAA))
|
||||||
|
Expect(err).Should(Succeed())
|
||||||
|
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeSuccess))
|
||||||
|
Expect(resp.RType).Should(Equal(ResponseTypeRESOLVED))
|
||||||
|
Expect(resp.Res.Answer).Should(HaveLen(0))
|
||||||
|
})
|
||||||
|
It("Configure should output 'empty list'", func() {
|
||||||
|
c := sut.Configuration()
|
||||||
|
Expect(c).Should(HaveLen(1))
|
||||||
|
Expect(c[0]).Should(Equal("filtering query Types: ''"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,37 +0,0 @@
|
||||||
package resolver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/0xERR0R/blocky/model"
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
// IPv6DisablingResolver can drop all AAAA query (empty ANSWER with NOERROR)
|
|
||||||
type IPv6DisablingResolver struct {
|
|
||||||
NextResolver
|
|
||||||
disableAAAA bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *IPv6DisablingResolver) Resolve(request *model.Request) (*model.Response, error) {
|
|
||||||
if r.disableAAAA && request.Req.Question[0].Qtype == dns.TypeAAAA {
|
|
||||||
response := new(dns.Msg)
|
|
||||||
response.SetRcode(request.Req, dns.RcodeSuccess)
|
|
||||||
|
|
||||||
return &model.Response{Res: response, RType: model.ResponseTypeRESOLVED}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.next.Resolve(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *IPv6DisablingResolver) Configuration() (result []string) {
|
|
||||||
if r.disableAAAA {
|
|
||||||
result = append(result, "drop AAAA")
|
|
||||||
} else {
|
|
||||||
result = append(result, "accept AAAA")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewIPv6Checker(disable bool) ChainedResolver {
|
|
||||||
return &IPv6DisablingResolver{disableAAAA: disable}
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
package resolver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/0xERR0R/blocky/util"
|
|
||||||
|
|
||||||
. "github.com/0xERR0R/blocky/model"
|
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
. "github.com/onsi/ginkgo/v2"
|
|
||||||
. "github.com/onsi/gomega"
|
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ = Describe("IPv6DisablingResolver", func() {
|
|
||||||
var (
|
|
||||||
sut *IPv6DisablingResolver
|
|
||||||
m *MockResolver
|
|
||||||
mockAnswer *dns.Msg
|
|
||||||
disableIPv6 *bool
|
|
||||||
query = newRequest("example.com", dns.TypeAAAA)
|
|
||||||
)
|
|
||||||
|
|
||||||
JustBeforeEach(func() {
|
|
||||||
mockAnswer, _ = util.NewMsgWithAnswer("example.com.", 1230, dns.TypeAAAA, "2001:0db8:85a3:08d3:1319:8a2e:0370:7344")
|
|
||||||
sut = NewIPv6Checker(*disableIPv6).(*IPv6DisablingResolver)
|
|
||||||
m = &MockResolver{}
|
|
||||||
m.On("Resolve", mock.Anything).Return(&Response{Res: mockAnswer, Reason: "reason"}, nil)
|
|
||||||
sut.Next(m)
|
|
||||||
})
|
|
||||||
|
|
||||||
When("Configure IPv6 enabled", func() {
|
|
||||||
BeforeEach(func() {
|
|
||||||
b := false
|
|
||||||
disableIPv6 = &b
|
|
||||||
})
|
|
||||||
It("Should return one AAAA answer", func() {
|
|
||||||
resp, err := sut.Resolve(query)
|
|
||||||
Expect(err).Should(Succeed())
|
|
||||||
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeSuccess))
|
|
||||||
Expect(resp.Res.Answer).Should(HaveLen(1))
|
|
||||||
})
|
|
||||||
It("Configure should output 'accept'", func() {
|
|
||||||
c := sut.Configuration()
|
|
||||||
Expect(c).Should(HaveLen(1))
|
|
||||||
Expect(c[0]).Should(ContainSubstring("accept"))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
When("Configure IPv6 disabled", func() {
|
|
||||||
BeforeEach(func() {
|
|
||||||
b := true
|
|
||||||
disableIPv6 = &b
|
|
||||||
})
|
|
||||||
It("Should return empty answer without error", func() {
|
|
||||||
resp, err := sut.Resolve(query)
|
|
||||||
Expect(err).Should(Succeed())
|
|
||||||
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeSuccess))
|
|
||||||
Expect(resp.Res.Answer).Should(HaveLen(0))
|
|
||||||
})
|
|
||||||
It("Configure should output 'drop'", func() {
|
|
||||||
c := sut.Configuration()
|
|
||||||
Expect(c).Should(HaveLen(1))
|
|
||||||
Expect(c[0]).Should(ContainSubstring("drop"))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -194,7 +194,7 @@ func createQueryResolver(cfg *config.Config, redisClient *redis.Client) (resolve
|
||||||
br, brErr := resolver.NewBlockingResolver(cfg.Blocking, redisClient)
|
br, brErr := resolver.NewBlockingResolver(cfg.Blocking, redisClient)
|
||||||
|
|
||||||
return resolver.Chain(
|
return resolver.Chain(
|
||||||
resolver.NewIPv6Checker(cfg.DisableIPv6),
|
resolver.NewFilteringResolver(cfg.Filtering),
|
||||||
resolver.NewClientNamesResolver(cfg.ClientLookup),
|
resolver.NewClientNamesResolver(cfg.ClientLookup),
|
||||||
resolver.NewQueryLoggingResolver(cfg.QueryLog),
|
resolver.NewQueryLoggingResolver(cfg.QueryLog),
|
||||||
resolver.NewMetricsResolver(cfg.Prometheus),
|
resolver.NewMetricsResolver(cfg.Prometheus),
|
||||||
|
|
|
@ -11,6 +11,10 @@ conditional:
|
||||||
mapping:
|
mapping:
|
||||||
fritz.box: tcp+udp:192.168.178.1
|
fritz.box: tcp+udp:192.168.178.1
|
||||||
multiple.resolvers: tcp+udp:192.168.178.1,tcp+udp:192.168.178.2
|
multiple.resolvers: tcp+udp:192.168.178.1,tcp+udp:192.168.178.2
|
||||||
|
filtering:
|
||||||
|
queryTypes:
|
||||||
|
- AAAA
|
||||||
|
- A
|
||||||
blocking:
|
blocking:
|
||||||
blackLists:
|
blackLists:
|
||||||
ads:
|
ads:
|
||||||
|
|
Loading…
Reference in New Issue