Skip to content

Commit

Permalink
EDNS: Client Subnet (#1007)
Browse files Browse the repository at this point in the history
* added util for handling EDNS0 options

* disable caching if the request contains a netmask size greater than 1

* added config section for ECS handling and validation for it

*added ecs_resolver for enhancing and cleaning subnet and client IP information
  • Loading branch information
kwitsch authored Nov 20, 2023
1 parent d52c598 commit d37d183
Show file tree
Hide file tree
Showing 19 changed files with 1,116 additions and 66 deletions.
4 changes: 1 addition & 3 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,7 @@
"GitHub.vscode-github-actions"
],
"settings": {
"go.lintFlags": [
"--config=${containerWorkspaceFolder}/.golangci.yml"
],
"go.lintFlags": ["--config=${containerWorkspaceFolder}/.golangci.yml"],
"go.alternateTools": {
"go-langserver": "gopls"
},
Expand Down
8 changes: 4 additions & 4 deletions .devcontainer/scripts/runItOnGo.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@

FOLDER_PATH=$1
if [ -z "${FOLDER_PATH}" ]; then
FOLDER_PATH=$PWD
FOLDER_PATH=$PWD
fi

BASE_PATH=$2
if [ -z "${BASE_PATH}" ]; then
BASE_PATH=$WORKSPACE_FOLDER
BASE_PATH=$WORKSPACE_FOLDER
fi

if [ "$FOLDER_PATH" = "$BASE_PATH" ]; then
echo "Skipping lcov creation for base path"
exit 1
echo "Skipping lcov creation for base path"
exit 1
fi

FOLDER_NAME=${FOLDER_PATH#"$BASE_PATH/"}
Expand Down
2 changes: 1 addition & 1 deletion config/caching.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type CachingConfig struct {

// IsEnabled implements `config.Configurable`.
func (c *CachingConfig) IsEnabled() bool {
return c.MaxCachingTime.IsAboveZero()
return c.MaxCachingTime.IsAtLeastZero()
}

// LogConfig implements `config.Configurable`.
Expand Down
17 changes: 14 additions & 3 deletions config/caching_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,22 @@ var _ = Describe("CachingConfig", func() {
})

Describe("IsEnabled", func() {
It("should be false by default", func() {
It("should be true by default", func() {
cfg := CachingConfig{}
Expect(defaults.Set(&cfg)).Should(Succeed())

Expect(cfg.IsEnabled()).Should(BeFalse())
Expect(cfg.IsEnabled()).Should(BeTrue())
})

When("the config is disabled", func() {
BeforeEach(func() {
cfg = CachingConfig{
MaxCachingTime: Duration(time.Hour * -1),
}
})
It("should be false", func() {
Expect(cfg.IsEnabled()).Should(BeFalse())
})
})

When("the config is enabled", func() {
Expand Down Expand Up @@ -72,7 +83,7 @@ var _ = Describe("CachingConfig", func() {

Expect(cfg.Prefetching).Should(BeTrue())
Expect(cfg.PrefetchThreshold).Should(Equal(0))
Expect(cfg.MaxCachingTime).ShouldNot(BeZero())
Expect(cfg.MaxCachingTime).Should(BeZero())
})
})
})
Expand Down
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ type Config struct {
FqdnOnly FqdnOnlyConfig `yaml:"fqdnOnly"`
Filtering FilteringConfig `yaml:"filtering"`
Ede EdeConfig `yaml:"ede"`
ECS ECSConfig `yaml:"ecs"`
SUDN SUDNConfig `yaml:"specialUseDomains"`

// Deprecated options
Expand Down
11 changes: 11 additions & 0 deletions config/duration.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,35 @@ import (
"github.com/hako/durafmt"
)

// Duration is a wrapper for time.Duration to support yaml unmarshalling
type Duration time.Duration

// ToDuration converts Duration to time.Duration
func (c Duration) ToDuration() time.Duration {
return time.Duration(c)
}

// IsAboveZero returns true if duration is above zero
func (c Duration) IsAboveZero() bool {
return c.ToDuration() > 0
}

// IsAtLeastZero returns true if duration is at least zero
func (c Duration) IsAtLeastZero() bool {
return c.ToDuration() >= 0
}

// Seconds returns duration in seconds
func (c Duration) Seconds() float64 {
return c.ToDuration().Seconds()
}

// SecondsU32 returns duration in seconds as uint32
func (c Duration) SecondsU32() uint32 {
return uint32(c.Seconds())
}

// String implements `fmt.Stringer`
func (c Duration) String() string {
return durafmt.Parse(c.ToDuration()).String()
}
Expand Down
81 changes: 81 additions & 0 deletions config/ecs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package config

import (
"fmt"
"strconv"

"github.com/sirupsen/logrus"
)

const (
ecsIpv4MaskMax = uint8(32)
ecsIpv6MaskMax = uint8(128)
)

// ECSv4Mask is the subnet mask to be added as EDNS0 option for IPv4
type ECSv4Mask uint8

// UnmarshalText implements the encoding.TextUnmarshaler interface
func (x *ECSv4Mask) UnmarshalText(text []byte) error {
res, err := unmarshalInternal(text, ecsIpv4MaskMax, "IPv4")
if err != nil {
return err
}

*x = ECSv4Mask(res)

return nil
}

// ECSv6Mask is the subnet mask to be added as EDNS0 option for IPv6
type ECSv6Mask uint8

// UnmarshalText implements the encoding.TextUnmarshaler interface
func (x *ECSv6Mask) UnmarshalText(text []byte) error {
res, err := unmarshalInternal(text, ecsIpv6MaskMax, "IPv6")
if err != nil {
return err
}

*x = ECSv6Mask(res)

return nil
}

// ECSConfig is the configuration of the ECS resolver
type ECSConfig struct {
UseAsClient bool `yaml:"useAsClient" default:"false"`
Forward bool `yaml:"forward" default:"false"`
IPv4Mask ECSv4Mask `yaml:"ipv4Mask" default:"0"`
IPv6Mask ECSv6Mask `yaml:"ipv6Mask" default:"0"`
}

// IsEnabled returns true if the ECS resolver is enabled
func (c *ECSConfig) IsEnabled() bool {
return c.UseAsClient || c.Forward || c.IPv4Mask > 0 || c.IPv6Mask > 0
}

// LogConfig logs the configuration
func (c *ECSConfig) LogConfig(logger *logrus.Entry) {
logger.Infof("Use as client = %t", c.UseAsClient)
logger.Infof("Forward = %t", c.Forward)
logger.Infof("IPv4 netmask = %d", c.IPv4Mask)
logger.Infof("IPv6 netmask = %d", c.IPv6Mask)
}

// unmarshalInternal unmarshals the subnet mask from the given text and checks if the value is valid
// it is used by the UnmarshalText methods of ECSv4Mask and ECSv6Mask
func unmarshalInternal(text []byte, maxvalue uint8, name string) (uint8, error) {
strVal := string(text)

uiVal, err := strconv.ParseUint(strVal, 10, 8)
if err != nil {
return 0, err
}

if uiVal > uint64(maxvalue) {
return 0, fmt.Errorf("mask value (%s) is too large for %s(max: %d)", strVal, name, maxvalue)
}

return uint8(uiVal), nil
}
167 changes: 167 additions & 0 deletions config/ecs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package config

import (
"github.com/0xERR0R/blocky/log"
"github.com/creasty/defaults"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"gopkg.in/yaml.v2"
)

var _ = Describe("ECSConfig", func() {
var (
c ECSConfig
err error
)

BeforeEach(func() {
err = defaults.Set(&c)
Expect(err).Should(Succeed())
})

Describe("IsEnabled", func() {
When("all fields are default", func() {
It("should be disabled", func() {
Expect(c.IsEnabled()).Should(BeFalse())
})
})

When("UseEcsAsClient is true", func() {
BeforeEach(func() {
c.UseAsClient = true
})

It("should be enabled", func() {
Expect(c.IsEnabled()).Should(BeTrue())
})
})

When("ForwardEcs is true", func() {
BeforeEach(func() {
c.Forward = true
})

It("should be enabled", func() {
Expect(c.IsEnabled()).Should(BeTrue())
})
})

When("IPv4Mask is set", func() {
BeforeEach(func() {
c.IPv4Mask = 24
})

It("should be enabled", func() {
Expect(c.IsEnabled()).Should(BeTrue())
})
})

When("IPv6Mask is set", func() {
BeforeEach(func() {
c.IPv6Mask = 64
})

It("should be enabled", func() {
Expect(c.IsEnabled()).Should(BeTrue())
})
})
})

Describe("LogConfig", func() {
BeforeEach(func() {
logger, hook = log.NewMockEntry()
})

It("should log configuration", func() {
c.LogConfig(logger)

Expect(hook.Calls).Should(HaveLen(4))
Expect(hook.Messages).Should(SatisfyAll(
ContainElement(ContainSubstring("Use as client")),
ContainElement(ContainSubstring("Forward")),
ContainElement(ContainSubstring("IPv4 netmask")),
ContainElement(ContainSubstring("IPv6 netmask")),
))
})
})

Describe("Parse", func() {
var data []byte

Context("IPv4Mask", func() {
var ipmask ECSv4Mask

When("Parse correct value", func() {
BeforeEach(func() {
data = []byte("24")
err = yaml.Unmarshal(data, &ipmask)
Expect(err).Should(Succeed())
})

It("should be value", func() {
Expect(ipmask).Should(Equal(ECSv4Mask(24)))
})
})

When("Parse NaN value", func() {
BeforeEach(func() {
data = []byte("FALSE")
err = yaml.Unmarshal(data, &ipmask)
})

It("should be error", func() {
Expect(err).Should(HaveOccurred())
})
})

When("Parse incorrect value", func() {
BeforeEach(func() {
data = []byte("35")
err = yaml.Unmarshal(data, &ipmask)
})

It("should be error", func() {
Expect(err).Should(HaveOccurred())
})
})
})

Context("IPv6Mask", func() {
var ipmask ECSv6Mask

When("Parse correct value", func() {
BeforeEach(func() {
data = []byte("64")
err = yaml.Unmarshal(data, &ipmask)
Expect(err).Should(Succeed())
})

It("should be value", func() {
Expect(ipmask).Should(Equal(ECSv6Mask(64)))
})
})

When("Parse NaN value", func() {
BeforeEach(func() {
data = []byte("FALSE")
err = yaml.Unmarshal(data, &ipmask)
})

It("should be error", func() {
Expect(err).Should(HaveOccurred())
})
})

When("Parse incorrect value", func() {
BeforeEach(func() {
data = []byte("130")
err = yaml.Unmarshal(data, &ipmask)
})

It("should be error", func() {
Expect(err).Should(HaveOccurred())
})
})
})
})
})
Loading

0 comments on commit d37d183

Please sign in to comment.