build(tests): e2e integration tests with docker and testcontainers (#753)

This commit is contained in:
Dimitri Herzog 2022-11-24 21:54:52 +01:00 committed by GitHub
parent d4813a6448
commit fb0810f18d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1835 additions and 14 deletions

View File

@ -10,4 +10,5 @@ node_modules
.gitignore
*.md
LICENSE
vendor
vendor
e2e/

27
.github/workflows/e2e-tests.yml vendored Normal file
View File

@ -0,0 +1,27 @@
name: Run e2e tests
on:
push:
pull_request:
jobs:
e2e-test:
name: Build Docker image
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version-file: go.mod
id: go
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Run e2e
run: make e2e-test

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
.idea/
.vscode/
*.iml
*.test
/*.pem
bin/
dist/

View File

@ -77,3 +77,4 @@ issues:
linters:
- gochecknoglobals
- dupl
- gosec

View File

@ -76,4 +76,4 @@ ENV BLOCKY_CONFIG_FILE=/app/config.yml
ENTRYPOINT ["/app/blocky"]
HEALTHCHECK --interval=1m --timeout=3s CMD ["/app/blocky", "healthcheck"]
HEALTHCHECK --start-period=1m --timeout=3s CMD ["/app/blocky", "healthcheck"]

View File

@ -1,4 +1,4 @@
.PHONY: all clean build swagger test lint run fmt docker-build help
.PHONY: all clean build swagger test e2e-test lint run fmt docker-build help
.DEFAULT_GOAL:=help
VERSION?=$(shell git describe --always --tags)
@ -54,11 +54,20 @@ ifdef BIN_AUTOCAB
setcap 'cap_net_bind_service=+ep' $(GO_BUILD_OUTPUT)
endif
test: ## run tests
go run github.com/onsi/ginkgo/v2/ginkgo -v --coverprofile=coverage.txt --covermode=atomic -cover ./...
test: ## run tests
go run github.com/onsi/ginkgo/v2/ginkgo --label-filter="!e2e" --coverprofile=coverage.txt --covermode=atomic -cover ./...
e2e-test: ## run e2e tests
docker buildx build \
--build-arg VERSION=blocky-e2e \
--network=host \
-o type=docker \
-t blocky-e2e \
.
go run github.com/onsi/ginkgo/v2/ginkgo --label-filter="e2e" ./...
race: ## run tests with race detector
go run github.com/onsi/ginkgo/v2/ginkgo --race ./...
go run github.com/onsi/ginkgo/v2/ginkgo --label-filter="!e2e" --race ./...
lint: ## run golangcli-lint checks
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.50.1
@ -81,4 +90,4 @@ docker-build: ## Build docker image
.
help: ## Shows help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
@grep -E '^[0-9a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

110
e2e/basic_test.go Normal file
View File

@ -0,0 +1,110 @@
package e2e
import (
"context"
"fmt"
"net"
"net/http"
. "github.com/0xERR0R/blocky/helpertest"
"github.com/0xERR0R/blocky/util"
"github.com/miekg/dns"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/testcontainers/testcontainers-go"
)
var _ = Describe("Basic functional tests", func() {
var blocky, moka testcontainers.Container
var err error
Describe("Container start", func() {
BeforeEach(func() {
moka, err = createDNSMokkaContainer("moka1", `A google/NOERROR("A 1.2.3.4 123")`)
Expect(err).Should(Succeed())
DeferCleanup(moka.Terminate)
})
When("Minimal configuration is provided", func() {
BeforeEach(func() {
blocky, err = createBlockyContainer(tmpDir,
"upstream:",
" default:",
" - moka1",
)
Expect(err).Should(Succeed())
DeferCleanup(blocky.Terminate)
})
It("Should start and answer DNS queries", func() {
msg := util.NewMsgWithQuestion("google.de.", dns.Type(dns.TypeA))
Expect(doDNSRequest(blocky, msg)).Should(BeDNSRecord("google.de.", dns.TypeA, 123, "1.2.3.4"))
})
It("should return 'healthy' container status (healthcheck)", func() {
Eventually(func(g Gomega) string {
state, err := blocky.State(context.Background())
g.Expect(err).NotTo(HaveOccurred())
return state.Health.Status
}, "2m", "1s").Should(Equal("healthy"))
})
})
Context("http port configuration", func() {
When("'httpPort' is not defined", func() {
BeforeEach(func() {
blocky, err = createBlockyContainer(tmpDir,
"upstream:",
" default:",
" - moka1",
)
Expect(err).Should(Succeed())
DeferCleanup(blocky.Terminate)
})
It("should not open http port", func() {
host, port, err := getContainerHostPort(blocky, "4000/tcp")
Expect(err).Should(Succeed())
_, err = http.Get(fmt.Sprintf("http://%s", net.JoinHostPort(host, port)))
Expect(err).Should(HaveOccurred())
})
})
When("'httpPort' is defined", func() {
BeforeEach(func() {
blocky, err = createBlockyContainer(tmpDir,
"upstream:",
" default:",
" - moka1",
"httpPort: 4000",
)
Expect(err).Should(Succeed())
DeferCleanup(blocky.Terminate)
})
It("should serve http content", func() {
host, port, err := getContainerHostPort(blocky, "4000/tcp")
Expect(err).Should(Succeed())
url := fmt.Sprintf("http://%s", net.JoinHostPort(host, port))
By("serve static html content", func() {
Eventually(http.Get).WithArguments(url).Should(HaveHTTPStatus(http.StatusOK))
})
By("serve pprof endpoint", func() {
Eventually(http.Get).WithArguments(url + "/debug/").Should(HaveHTTPStatus(http.StatusOK))
})
By("prometheus endpoint should be disabled", func() {
Eventually(http.Get).WithArguments(url + "/metrics").Should(HaveHTTPStatus(http.StatusNotFound))
})
By("serve DoH endpoint", func() {
Eventually(http.Get).WithArguments(url +
"/dns-query?dns=q80BAAABAAAAAAAAA3d3dwdleGFtcGxlA2NvbQAAAQAB").Should(HaveHTTPStatus(http.StatusOK))
})
})
})
})
})
})

117
e2e/blocking_test.go Normal file
View File

@ -0,0 +1,117 @@
package e2e
import (
. "github.com/0xERR0R/blocky/helpertest"
"github.com/0xERR0R/blocky/util"
"github.com/miekg/dns"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/testcontainers/testcontainers-go"
)
var _ = Describe("External lists and query blocking", func() {
var blocky, httpServer, moka testcontainers.Container
var err error
BeforeEach(func() {
moka, err = createDNSMokkaContainer("moka", `A google/NOERROR("A 1.2.3.4 123")`)
Expect(err).Should(Succeed())
DeferCleanup(moka.Terminate)
})
Describe("List download on startup", func() {
When("external blacklist ist not available", func() {
Context("startStrategy = blocking", func() {
BeforeEach(func() {
blocky, err = createBlockyContainer(tmpDir,
"logLevel: warn",
"upstream:",
" default:",
" - moka",
"blocking:",
" startStrategy: blocking",
" blackLists:",
" ads:",
" - http://wrong.domain.url/list.txt",
" clientGroupsBlock:",
" default:",
" - ads",
)
Expect(err).Should(Succeed())
DeferCleanup(blocky.Terminate)
})
It("should start with warning in log work without errors", func() {
msg := util.NewMsgWithQuestion("google.com.", dns.Type(dns.TypeA))
Expect(doDNSRequest(blocky, msg)).Should(BeDNSRecord("google.com.", dns.TypeA, 123, "1.2.3.4"))
Expect(getContainerLogs(blocky)).Should(ContainElement(ContainSubstring("error during file processing")))
})
})
Context("startStrategy = failOnError", func() {
BeforeEach(func() {
blocky, err = createBlockyContainer(tmpDir,
"logLevel: warn",
"upstream:",
" default:",
" - moka",
"blocking:",
" startStrategy: failOnError",
" blackLists:",
" ads:",
" - http://wrong.domain.url/list.txt",
" clientGroupsBlock:",
" default:",
" - ads",
)
Expect(err).Should(HaveOccurred())
DeferCleanup(blocky.Terminate)
})
It("should fail to start", func() {
Expect(blocky.IsRunning()).Should(BeFalse())
Expect(getContainerLogs(blocky)).
Should(ContainElement(ContainSubstring("Error: can't start server: 1 error occurred")))
})
})
})
})
Describe("Query blocking against external blacklists", func() {
When("external blacklists are defined and available", func() {
BeforeEach(func() {
httpServer, err = createHTTPServerContainer("httpserver", tmpDir, "list.txt", "blockeddomain.com")
Expect(err).Should(Succeed())
DeferCleanup(httpServer.Terminate)
blocky, err = createBlockyContainer(tmpDir,
"logLevel: warn",
"upstream:",
" default:",
" - moka",
"blocking:",
" blackLists:",
" ads:",
" - http://httpserver:8080/list.txt",
" clientGroupsBlock:",
" default:",
" - ads",
)
Expect(err).Should(Succeed())
DeferCleanup(blocky.Terminate)
})
It("should download external list on startup and block queries", func() {
msg := util.NewMsgWithQuestion("blockeddomain.com.", dns.Type(dns.TypeA))
Expect(doDNSRequest(blocky, msg)).Should(BeDNSRecord("blockeddomain.com.", dns.TypeA, 6*60*60, "0.0.0.0"))
Expect(getContainerLogs(blocky)).Should(BeEmpty())
})
})
})
})

252
e2e/containers.go Normal file
View File

@ -0,0 +1,252 @@
package e2e
import (
"bufio"
"context"
"fmt"
"io"
"net"
"strings"
"time"
"github.com/0xERR0R/blocky/helpertest"
"github.com/docker/go-connections/nat"
"github.com/miekg/dns"
"github.com/onsi/ginkgo/v2"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
//nolint:gochecknoglobals
var NetworkName = fmt.Sprintf("blocky-e2e-network_%d", time.Now().Unix())
const (
redisImage = "redis:7"
postgresImage = "postgres:15"
mariaDBImage = "mariadb:10"
mokaImage = "ghcr.io/0xerr0r/dns-mokka:0.2.0"
staticServerImage = "halverneus/static-file-server:latest"
blockyImage = "blocky-e2e"
)
func createDNSMokkaContainer(alias string, rules ...string) (testcontainers.Container, error) {
ctx := context.Background()
mokaRules := make(map[string]string)
for i, rule := range rules {
mokaRules[fmt.Sprintf("MOKKA_RULE_%d", i)] = rule
}
req := testcontainers.ContainerRequest{
Image: mokaImage,
Networks: []string{NetworkName},
ExposedPorts: []string{"53/tcp", "53/udp"},
NetworkAliases: map[string][]string{NetworkName: {alias}},
WaitingFor: wait.ForExposedPort(),
Env: mokaRules,
}
return testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
}
func createHTTPServerContainer(alias string, tmpDir *helpertest.TmpFolder,
filename string, lines ...string) (testcontainers.Container, error) {
f1 := tmpDir.CreateStringFile(filename,
lines...,
)
if f1.Error != nil {
return nil, f1.Error
}
const modeOwner = 700
ctx := context.Background()
req := testcontainers.ContainerRequest{
Image: staticServerImage,
Networks: []string{NetworkName},
NetworkAliases: map[string][]string{NetworkName: {alias}},
ExposedPorts: []string{"8080/tcp"},
Env: map[string]string{"FOLDER": "/"},
Files: []testcontainers.ContainerFile{
{
HostFilePath: f1.Path,
ContainerFilePath: fmt.Sprintf("/%s", filename),
FileMode: modeOwner,
},
},
}
return testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
}
func createRedisContainer() (testcontainers.Container, error) {
ctx := context.Background()
req := testcontainers.ContainerRequest{
Image: redisImage,
Networks: []string{NetworkName},
ExposedPorts: []string{"6379/tcp"},
NetworkAliases: map[string][]string{NetworkName: {"redis"}},
WaitingFor: wait.ForExposedPort(),
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
return container, err
}
func createPostgresContainer() (testcontainers.Container, error) {
ctx := context.Background()
req := testcontainers.ContainerRequest{
Image: postgresImage,
Networks: []string{NetworkName},
ExposedPorts: []string{"5432/tcp"},
NetworkAliases: map[string][]string{NetworkName: {"postgres"}},
Env: map[string]string{
"POSTGRES_USER": "user",
"POSTGRES_PASSWORD": "user",
},
WaitingFor: wait.ForExposedPort(),
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
return container, err
}
func createMariaDBContainer() (testcontainers.Container, error) {
ctx := context.Background()
req := testcontainers.ContainerRequest{
Image: mariaDBImage,
Networks: []string{NetworkName},
ExposedPorts: []string{"3306/tcp"},
NetworkAliases: map[string][]string{NetworkName: {"mariaDB"}},
Env: map[string]string{
"MARIADB_USER": "user",
"MARIADB_PASSWORD": "user",
"MARIADB_DATABASE": "user",
"MARIADB_ROOT_PASSWORD": "user",
},
WaitingFor: wait.ForAll(wait.ForLog("ready for connections"), wait.ForExposedPort()),
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
return container, err
}
func createBlockyContainer(tmpDir *helpertest.TmpFolder, lines ...string) (testcontainers.Container, error) {
f1 := tmpDir.CreateStringFile("config1.yaml",
lines...,
)
if f1.Error != nil {
return nil, f1.Error
}
const modeOwner = 700
ctx := context.Background()
req := testcontainers.ContainerRequest{
Image: blockyImage,
Networks: []string{NetworkName},
ExposedPorts: []string{"53/tcp", "53/udp", "4000/tcp"},
Files: []testcontainers.ContainerFile{
{
HostFilePath: f1.Path,
ContainerFilePath: "/app/config.yml",
FileMode: modeOwner,
},
},
WaitingFor: wait.NewExecStrategy([]string{"/app/blocky", "healthcheck"}),
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
// attach container log if error occurs
if r, err := container.Logs(context.Background()); err == nil {
if b, err := io.ReadAll(r); err == nil {
ginkgo.AddReportEntry("blocky container log", string(b))
}
}
}
return container, err
}
func doDNSRequest(blocky testcontainers.Container, message *dns.Msg) (*dns.Msg, error) {
const timeout = 5 * time.Second
c := &dns.Client{
Net: "tcp",
Timeout: timeout,
}
host, port, err := getContainerHostPort(blocky, "53/tcp")
if err != nil {
return nil, err
}
msg, _, err := c.Exchange(message, net.JoinHostPort(host, port))
return msg, err
}
func getContainerHostPort(c testcontainers.Container, p nat.Port) (host string, port string, err error) {
res, err := c.MappedPort(context.Background(), p)
if err != nil {
return "", "", err
}
host, err = c.Host(context.Background())
if err != nil {
return "", "", err
}
return host, res.Port(), err
}
func getContainerLogs(c testcontainers.Container) (lines []string, err error) {
if r, err := c.Logs(context.Background()); err == nil {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Text()
if len(strings.TrimSpace(line)) > 0 {
lines = append(lines, line)
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return lines, nil
}
return nil, err
}

47
e2e/e2e_suite_test.go Normal file
View File

@ -0,0 +1,47 @@
package e2e
import (
"context"
"testing"
"github.com/0xERR0R/blocky/helpertest"
"github.com/0xERR0R/blocky/log"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/testcontainers/testcontainers-go"
)
func TestLists(t *testing.T) {
log.Silence()
RegisterFailHandler(Fail)
RunSpecs(t, "e2e Suite", Label("e2e"))
}
var (
network testcontainers.Network
tmpDir *helpertest.TmpFolder
)
var _ = BeforeSuite(func() {
var err error
network, err = testcontainers.GenericNetwork(context.Background(), testcontainers.GenericNetworkRequest{
NetworkRequest: testcontainers.NetworkRequest{
Name: NetworkName,
CheckDuplicate: false,
Attachable: true,
},
})
Expect(err).Should(Succeed())
DeferCleanup(func() {
err := network.Remove(context.Background())
Expect(err).Should(Succeed())
})
tmpDir = helpertest.NewTmpFolder("config")
Expect(tmpDir.Error).Should(Succeed())
DeferCleanup(tmpDir.Clean)
})

134
e2e/metrics_test.go Normal file
View File

@ -0,0 +1,134 @@
package e2e
import (
"bufio"
"fmt"
"net"
"net/http"
"strings"
. "github.com/0xERR0R/blocky/helpertest"
"github.com/0xERR0R/blocky/util"
"github.com/miekg/dns"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/testcontainers/testcontainers-go"
)
var _ = Describe("Metrics functional tests", func() {
var blocky, moka, httpServer1, httpServer2 testcontainers.Container
var err error
var metricsURL string
Describe("Metrics", func() {
BeforeEach(func() {
moka, err = createDNSMokkaContainer("moka1", `A google/NOERROR("A 1.2.3.4 123")`)
Expect(err).Should(Succeed())
DeferCleanup(moka.Terminate)
httpServer1, err = createHTTPServerContainer("httpserver1", tmpDir, "list1.txt", "domain1.com")
Expect(err).Should(Succeed())
DeferCleanup(httpServer1.Terminate)
httpServer2, err = createHTTPServerContainer("httpserver2", tmpDir, "list2.txt", "domain1.com", "domain2", "domain3")
Expect(err).Should(Succeed())
DeferCleanup(httpServer2.Terminate)
blocky, err = createBlockyContainer(tmpDir,
"upstream:",
" default:",
" - moka1",
"blocking:",
" blackLists:",
" group1:",
" - http://httpserver1:8080/list1.txt",
" group2:",
" - http://httpserver2:8080/list2.txt",
"httpPort: 4000",
"prometheus:",
" enable: true",
)
Expect(err).Should(Succeed())
DeferCleanup(blocky.Terminate)
host, port, err := getContainerHostPort(blocky, "4000/tcp")
Expect(err).Should(Succeed())
metricsURL = fmt.Sprintf("http://%s/metrics", net.JoinHostPort(host, port))
})
When("Blocky is started", func() {
It("Should provide 'blocky_build_info' prometheus metrics", func() {
Expect(fetchBlockyMetrics(metricsURL)).Should(ContainElement(ContainSubstring("blocky_build_info")))
})
It("Should provide 'blocky_blocking_enabled' prometheus metrics", func() {
Expect(fetchBlockyMetrics(metricsURL)).Should(ContainElement("blocky_blocking_enabled 1"))
})
})
When("Some query results are cached", func() {
BeforeEach(func() {
Expect(fetchBlockyMetrics(metricsURL)).Should(ContainElement("blocky_cache_entry_count 0"))
Expect(fetchBlockyMetrics(metricsURL)).Should(ContainElement("blocky_cache_hit_count 0"))
Expect(fetchBlockyMetrics(metricsURL)).Should(ContainElement("blocky_cache_miss_count 0"))
})
It("Should increment cache counts", func() {
msg := util.NewMsgWithQuestion("google.de.", dns.Type(dns.TypeA))
By("first query, should increment the cache miss count and the total count", func() {
Expect(doDNSRequest(blocky, msg)).Should(BeDNSRecord("google.de.", dns.TypeA, 123, "1.2.3.4"))
Expect(fetchBlockyMetrics(metricsURL)).Should(ContainElement("blocky_cache_entry_count 1"))
Expect(fetchBlockyMetrics(metricsURL)).Should(ContainElement("blocky_cache_hit_count 0"))
Expect(fetchBlockyMetrics(metricsURL)).Should(ContainElement("blocky_cache_miss_count 1"))
})
By("Same query again, should increment the cache hit count", func() {
Expect(doDNSRequest(blocky, msg)).Should(BeDNSRecord("google.de.", dns.TypeA, 0, "1.2.3.4"))
Expect(fetchBlockyMetrics(metricsURL)).Should(ContainElement("blocky_cache_entry_count 1"))
Expect(fetchBlockyMetrics(metricsURL)).Should(ContainElement("blocky_cache_hit_count 1"))
Expect(fetchBlockyMetrics(metricsURL)).Should(ContainElement("blocky_cache_miss_count 1"))
})
})
})
When("Lists are loaded", func() {
It("Should expose list cache sizes per group as metrics", func() {
Expect(fetchBlockyMetrics(metricsURL)).Should(ContainElement("blocky_blacklist_cache{group=\"group1\"} 1"))
Expect(fetchBlockyMetrics(metricsURL)).Should(ContainElement("blocky_blacklist_cache{group=\"group2\"} 3"))
})
})
})
})
func fetchBlockyMetrics(url string) ([]string, error) {
var metrics []string
r, err := http.Get(url)
if err != nil {
return nil, err
}
defer r.Body.Close()
scanner := bufio.NewScanner(r.Body)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "blocky_") {
metrics = append(metrics, line)
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return metrics, nil
}

195
e2e/querylog_test.go Normal file
View File

@ -0,0 +1,195 @@
package e2e
import (
"fmt"
"net"
"github.com/0xERR0R/blocky/util"
"github.com/miekg/dns"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/testcontainers/testcontainers-go"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
var _ = Describe("Query logs functional tests", func() {
var blocky, moka, database testcontainers.Container
var db *gorm.DB
var err error
BeforeEach(func() {
moka, err = createDNSMokkaContainer("moka1", `A google/NOERROR("A 1.2.3.4 123")`, `A unknown/NXDOMAIN()`)
Expect(err).Should(Succeed())
DeferCleanup(moka.Terminate)
})
Describe("Query logging into the mariaDB database", func() {
BeforeEach(func() {
database, err = createMariaDBContainer()
Expect(err).Should(Succeed())
DeferCleanup(database.Terminate)
blocky, err = createBlockyContainer(tmpDir,
"logLevel: warn",
"upstream:",
" default:",
" - moka1",
"queryLog:",
" type: mysql",
" target: user:user@tcp(mariaDB:3306)/user?charset=utf8mb4&parseTime=True&loc=Local",
)
Expect(err).Should(Succeed())
DeferCleanup(blocky.Terminate)
dbHost, dbPort, err := getContainerHostPort(database, "3306/tcp")
Expect(err).Should(Succeed())
dsn := fmt.Sprintf("user:user@tcp(%s)/user?charset=utf8mb4&parseTime=True&loc=Local",
net.JoinHostPort(dbHost, dbPort))
// database might be slow on first start, retry here if necessary
Eventually(gorm.Open, "10s", "1s").WithArguments(mysql.Open(dsn), &gorm.Config{}).Should(Not(BeNil()))
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
Expect(err).Should(Succeed())
Eventually(countEntries).WithArguments(db).Should(BeNumerically("==", 0))
})
When("Some queries were performed", func() {
It("Should store query log in the mariaDB database", func() {
By("Performing 2 queries", func() {
Expect(doDNSRequest(blocky, util.NewMsgWithQuestion("google.de.", dns.Type(dns.TypeA)))).Should(Not(BeNil()))
Expect(doDNSRequest(blocky, util.NewMsgWithQuestion("unknown.domain.", dns.Type(dns.TypeA)))).Should(Not(BeNil()))
})
By("check entries count asynchronously, since blocky flushes log entries in bulk", func() {
Eventually(countEntries, "60s", "1s").WithArguments(db).Should(BeNumerically("==", 2))
})
By("check entry content", func() {
entries, err := queryEntries(db)
Expect(err).Should(Succeed())
Expect(entries).Should(HaveLen(2))
Expect(entries[0]).Should(SatisfyAll(
HaveField("ResponseType", "RESOLVED"),
HaveField("QuestionType", "A"),
HaveField("QuestionName", "google.de"),
HaveField("Answer", "A (1.2.3.4)"),
HaveField("ResponseCode", "NOERROR"),
))
Expect(entries[1]).Should(SatisfyAll(
HaveField("ResponseType", "RESOLVED"),
HaveField("QuestionType", "A"),
HaveField("QuestionName", "unknown.domain"),
HaveField("Answer", ""),
HaveField("ResponseCode", "NXDOMAIN"),
))
})
})
})
})
Describe("Query logging into the postgres database", func() {
BeforeEach(func() {
database, err = createPostgresContainer()
Expect(err).Should(Succeed())
DeferCleanup(database.Terminate)
blocky, err = createBlockyContainer(tmpDir,
"logLevel: warn",
"upstream:",
" default:",
" - moka1",
"queryLog:",
" type: postgresql",
" target: postgres://user:user@postgres:5432/user",
)
Expect(err).Should(Succeed())
DeferCleanup(blocky.Terminate)
dbHost, dbPort, err := getContainerHostPort(database, "5432/tcp")
Expect(err).Should(Succeed())
dsn := fmt.Sprintf("postgres://user:user@%s/user", net.JoinHostPort(dbHost, dbPort))
// database might be slow on first start, retry here if necessary
Eventually(gorm.Open, "10s", "1s").WithArguments(postgres.Open(dsn), &gorm.Config{}).Should(Not(BeNil()))
db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
Expect(err).Should(Succeed())
Eventually(countEntries).WithArguments(db).Should(BeNumerically("==", 0))
})
When("Some queries were performed", func() {
msg := util.NewMsgWithQuestion("google.de.", dns.Type(dns.TypeA))
It("Should store query log in the postgres database", func() {
By("Performing 2 queries", func() {
Expect(doDNSRequest(blocky, msg)).Should(Not(BeNil()))
Expect(doDNSRequest(blocky, msg)).Should(Not(BeNil()))
})
By("check entries count asynchronously, since blocky flushes log entries in bulk", func() {
Eventually(countEntries, "60s", "1s").WithArguments(db).Should(BeNumerically("==", 2))
})
By("check entry content", func() {
entries, err := queryEntries(db)
Expect(err).Should(Succeed())
Expect(entries).Should(HaveLen(2))
Expect(entries[0]).Should(SatisfyAll(
HaveField("ResponseType", "RESOLVED"),
HaveField("QuestionType", "A"),
HaveField("QuestionName", "google.de"),
HaveField("Answer", "A (1.2.3.4)"),
HaveField("ResponseCode", "NOERROR"),
))
Expect(entries[1]).Should(SatisfyAll(
HaveField("ResponseType", "CACHED"),
HaveField("QuestionType", "A"),
HaveField("QuestionName", "google.de"),
HaveField("Answer", "A (1.2.3.4)"),
HaveField("ResponseCode", "NOERROR"),
))
})
})
})
})
})
type logEntry struct {
ResponseType string
QuestionType string
QuestionName string
Answer string
ResponseCode string
}
func queryEntries(db *gorm.DB) ([]logEntry, error) {
var entries []logEntry
return entries, db.Find(&entries).Order("request_ts DESC").Error
}
func countEntries(db *gorm.DB) (int64, error) {
var cnt int64
return cnt, db.Table("log_entries").Count(&cnt).Error
}

155
e2e/redis_test.go Normal file
View File

@ -0,0 +1,155 @@
package e2e
import (
"context"
"net"
. "github.com/0xERR0R/blocky/helpertest"
"github.com/0xERR0R/blocky/util"
"github.com/go-redis/redis/v8"
"github.com/miekg/dns"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/testcontainers/testcontainers-go"
)
var _ = Describe("Redis configuration tests", func() {
var blocky1, blocky2, redisDB, moka testcontainers.Container
var redisClient *redis.Client
var err error
BeforeEach(func() {
redisDB, err = createRedisContainer()
Expect(err).Should(Succeed())
DeferCleanup(redisDB.Terminate)
dbHost, dbPort, err := getContainerHostPort(redisDB, "6379/tcp")
Expect(err).Should(Succeed())
redisClient = redis.NewClient(&redis.Options{
Addr: net.JoinHostPort(dbHost, dbPort),
})
Expect(dbSize(redisClient)).Should(BeNumerically("==", 0))
moka, err = createDNSMokkaContainer("moka1", `A google/NOERROR("A 1.2.3.4 123")`)
Expect(err).Should(Succeed())
DeferCleanup(func() {
_ = moka.Terminate(context.Background())
})
})
Describe("Cache sharing between blocky instances", func() {
When("Redis and 2 blocky instances are configured", func() {
BeforeEach(func() {
blocky1, err = createBlockyContainer(tmpDir,
"logLevel: warn",
"upstream:",
" default:",
" - moka1",
"redis:",
" address: redis:6379",
)
Expect(err).Should(Succeed())
DeferCleanup(blocky1.Terminate)
blocky2, err = createBlockyContainer(tmpDir,
"logLevel: warn",
"upstream:",
" default:",
" - moka1",
"redis:",
" address: redis:6379",
)
Expect(err).Should(Succeed())
DeferCleanup(blocky2.Terminate)
})
It("2nd instance of blocky should use cache from redis", func() {
msg := util.NewMsgWithQuestion("google.de.", dns.Type(dns.TypeA))
By("Query first blocky instance, should store cache in redis", func() {
Expect(doDNSRequest(blocky1, msg)).Should(BeDNSRecord("google.de.", dns.TypeA, 123, "1.2.3.4"))
})
By("Check redis, must contain one cache entry", func() {
Eventually(dbSize).WithArguments(redisClient).Should(BeNumerically("==", 1))
})
By("Shutdown the upstream DNS server", func() {
Expect(moka.Terminate(context.Background())).Should(Succeed())
})
By("Query second blocky instance, should use cache from redis", func() {
Expect(doDNSRequest(blocky2, msg)).Should(BeDNSRecord("google.de.", dns.TypeA, 0, "1.2.3.4"))
})
By("No warnings/errors in log", func() {
Expect(getContainerLogs(blocky1)).Should(BeEmpty())
Expect(getContainerLogs(blocky2)).Should(BeEmpty())
})
})
})
})
Describe("Cache loading on startup", func() {
When("Redis and 1 blocky instance are configured", func() {
BeforeEach(func() {
blocky1, err = createBlockyContainer(tmpDir,
"logLevel: warn",
"upstream:",
" default:",
" - moka1",
"redis:",
" address: redis:6379",
)
Expect(err).Should(Succeed())
DeferCleanup(blocky1.Terminate)
})
It("should load cache from redis after start", func() {
msg := util.NewMsgWithQuestion("google.de.", dns.Type(dns.TypeA))
By("Query first blocky instance, should store cache in redis\"", func() {
Expect(doDNSRequest(blocky1, msg)).Should(BeDNSRecord("google.de.", dns.TypeA, 123, "1.2.3.4"))
})
By("Check redis, must contain one cache entry", func() {
Eventually(dbSize).WithArguments(redisClient).Should(BeNumerically("==", 1))
})
By("start other instance of blocky now -> it should load the cache from redis", func() {
blocky2, err = createBlockyContainer(tmpDir,
"logLevel: warn",
"upstream:",
" default:",
" - moka1",
"redis:",
" address: redis:6379",
)
Expect(err).Should(Succeed())
DeferCleanup(blocky2.Terminate)
})
By("Shutdown the upstream DNS server", func() {
Expect(moka.Terminate(context.Background())).Should(Succeed())
})
By("Query second blocky instance", func() {
Expect(doDNSRequest(blocky2, msg)).Should(BeDNSRecord("google.de.", dns.TypeA, 0, "1.2.3.4"))
})
By("No warnings/errors in log", func() {
Expect(getContainerLogs(blocky1)).Should(BeEmpty())
Expect(getContainerLogs(blocky2)).Should(BeEmpty())
})
})
})
})
})
func dbSize(redisClient *redis.Client) (int64, error) {
return redisClient.DBSize(context.Background()).Result()
}

89
e2e/upstream_test.go Normal file
View File

@ -0,0 +1,89 @@
package e2e
import (
. "github.com/0xERR0R/blocky/helpertest"
"github.com/0xERR0R/blocky/util"
"github.com/miekg/dns"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/testcontainers/testcontainers-go"
)
var _ = Describe("Upstream resolver configuration tests", func() {
var blocky testcontainers.Container
var err error
Describe("'startVerifyUpstream' parameter handling", func() {
When("'startVerifyUpstream' is false and upstream server is not reachable", func() {
BeforeEach(func() {
blocky, err = createBlockyContainer(tmpDir,
"upstream:",
" default:",
" - 192.192.192.192",
"startVerifyUpstream: false",
)
Expect(err).Should(Succeed())
DeferCleanup(blocky.Terminate)
})
It("should start even if upstream server is not reachable", func() {
Expect(blocky.IsRunning()).Should(BeTrue())
})
})
When("'startVerifyUpstream' is true and upstream server is not reachable", func() {
BeforeEach(func() {
blocky, err = createBlockyContainer(tmpDir,
"upstream:",
" default:",
" - 192.192.192.192",
"startVerifyUpstream: true",
)
Expect(err).Should(HaveOccurred())
DeferCleanup(blocky.Terminate)
})
It("should not start", func() {
Expect(blocky.IsRunning()).Should(BeFalse())
Expect(getContainerLogs(blocky)).
Should(ContainElement(ContainSubstring("unable to reach any DNS resolvers configured for resolver group default")))
})
})
})
Describe("'upstreamTimeout' parameter handling", func() {
var moka testcontainers.Container
BeforeEach(func() {
moka, err = createDNSMokkaContainer("moka1",
`A example.com/NOERROR("A 1.2.3.4 123")`,
`A delay.com/delay(NOERROR("A 1.1.1.1 100"), "300ms")`)
Expect(err).Should(Succeed())
DeferCleanup(moka.Terminate)
blocky, err = createBlockyContainer(tmpDir,
"upstream:",
" default:",
" - moka1",
"upstreamTimeout: 200ms",
)
Expect(err).Should(Succeed())
DeferCleanup(blocky.Terminate)
})
It("should consider the timeout parameter", func() {
By("query without timeout", func() {
msg := util.NewMsgWithQuestion("example.com.", dns.Type(dns.TypeA))
Expect(doDNSRequest(blocky, msg)).Should(BeDNSRecord("example.com.", dns.TypeA, 123, "1.2.3.4"))
})
By("query with timeout", func() {
msg := util.NewMsgWithQuestion("delay.com/.", dns.Type(dns.TypeA))
resp, err := doDNSRequest(blocky, msg)
Expect(err).Should(Succeed())
Expect(resp.Rcode).Should(Equal(dns.RcodeServerFailure))
})
})
})
})

32
go.mod
View File

@ -35,13 +35,40 @@ require (
gorm.io/gorm v1.24.1-0.20221019064659-5dd2bb482755
)
require github.com/dosgo/zigtool v0.0.0-20210923085854-9c6fc1d62198
require (
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
github.com/docker/go-connections v0.4.0
github.com/dosgo/zigtool v0.0.0-20210923085854-9c6fc1d62198
github.com/testcontainers/testcontainers-go v0.15.0
)
require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/Microsoft/hcsshim v0.9.4 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/containerd/cgroups v1.0.4 // indirect
github.com/containerd/containerd v1.6.8 // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/docker v20.10.17+incompatible // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/lib/pq v1.10.6 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/moby/sys/mount v0.3.3 // indirect
github.com/moby/sys/mountinfo v0.6.2 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect
github.com/opencontainers/runc v1.1.3 // indirect
go.opencensus.io v0.23.0 // indirect
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde // indirect
google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad // indirect
google.golang.org/grpc v1.47.0 // indirect
)
require (
@ -55,7 +82,6 @@ require (
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
@ -86,7 +112,7 @@ require (
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-sqlite3 v1.14.15 // indirect
github.com/mattn/goveralls v0.0.11 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/pkg/errors v0.9.1 // indirect

657
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -92,10 +92,16 @@ func (matcher *dnsRecordMatcher) matchSingle(rr dns.RR) (success bool, err error
// Match checks the DNS record
func (matcher *dnsRecordMatcher) Match(actual interface{}) (success bool, err error) {
switch i := actual.(type) {
case *dns.Msg:
return matcher.Match(i.Answer)
case dns.RR:
return matcher.matchSingle(i)
case []dns.RR:
return matcher.matchSingle(i[0])
if len(i) == 1 {
return matcher.matchSingle(i[0])
}
return false, fmt.Errorf("DNSRecord matcher expects []dns.RR with len == 1")
default:
return false, fmt.Errorf("DNSRecord matcher expects an dns.RR or []dns.RR")
}