Merge branch 'development'

This commit is contained in:
Dimitri Herzog 2022-11-12 21:39:38 +01:00
commit 6c616898b8
90 changed files with 4138 additions and 1006 deletions

View File

@ -1,6 +1,13 @@
bin/
dist/
bin
dist
site
docs
node_modules
.git
.idea
.github
testdata/
node_modules/
.vscode
.gitignore
*.md
LICENSE
vendor

View File

@ -7,3 +7,8 @@ updates:
open-pull-requests-limit: 10
assignees:
- 0xERR0R
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: daily

View File

@ -1,45 +1,35 @@
name: CI Build
on: [push, pull_request]
jobs:
build:
name: Build
make:
name: Test
runs-on: ubuntu-latest
strategy:
matrix:
make: [build, test, race, docker-build, goreleaser]
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Set up Go 1.18
uses: actions/setup-go@v1
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18
go-version-file: go.mod
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
- name: Get dependencies
run: |
go get -v -t -d ./...
if [ -f Gopkg.toml ]; then
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
dep ensure
fi
run: go mod download
- name: Build
run: make build
- name: Test
run: make test
- name: Race detection
run: make race
- name: make ${{ matrix.make }}
run: make ${{ matrix.make }}
if: matrix.make != 'goreleaser'
- name: Upload results to codecov
run: bash <(curl -s https://codecov.io/bash) -t 48d6a1a8-a66e-4f27-9cc1-a7b91c4209b2
- name: Docker images
run: make docker-build
uses: codecov/codecov-action@v3
if: matrix.make == 'test'
- name: Check GoReleaser configuration
uses: goreleaser/goreleaser-action@v2
uses: goreleaser/goreleaser-action@v3
if: matrix.make == 'goreleaser'
with:
args: check

25
.github/workflows/close_stale.yml vendored Normal file
View File

@ -0,0 +1,25 @@
name: 'Close stale issues and PRs'
on:
schedule:
- cron: '0 4 * * *'
jobs:
stale:
runs-on: ubuntu-latest
if: github.repository_owner == '0xERR0R'
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v6
with:
stale-issue-message: 'This issue is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
stale-pr-message: 'This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days.'
close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.'
close-pr-message: 'This PR was closed because it has been stalled for 10 days with no activity.'
days-before-issue-stale: 90
days-before-pr-stale: 45
days-before-issue-close: 5
days-before-pr-close: 10
exempt-all-milestones: true
operations-per-run: 60

View File

@ -35,11 +35,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@ -50,7 +50,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@ -64,4 +64,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v2

View File

@ -1,55 +1,207 @@
name: Development docker build
on:
push:
branches:
- development
- fb-*
permissions:
security-events: write
actions: read
contents: read
packages: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
docker:
if: github.repository_owner == '0xERR0R'
check:
name: Check if workflow should run
runs-on: ubuntu-latest
outputs:
enabled: ${{ steps.check.outputs.enabled }}
steps:
- name: Enabled Check
id: check
shell: bash
run: |
ENABLED=${{ secrets.DEVELOPMENT_DOCKER }}
if [[ "${{ github.repository_owner }}" == "0xERR0R" ]]; then
ENABLED="true"
fi
if [[ "${ENABLED,,}" != "true" ]]; then
echo "enabled=0" >> $GITHUB_OUTPUT
echo "Workflow is disabled"
echo "### Workflow is disabled" >> $GITHUB_STEP_SUMMARY
echo "To enable this workflow by creating a secret 'DEVELOPMENT_DOCKER' with the value 'true'" >> $GITHUB_STEP_SUMMARY
else
echo "enabled=1" >> $GITHUB_OUTPUT
echo "Workflow is enabled"
fi
docker:
name: Build Docker image
runs-on: ubuntu-latest
needs: check
if: ${{ needs.check.outputs.enabled == 1 }}
outputs:
repository: ${{ steps.get_vars.outputs.repository }}
branch: ${{ steps.get_vars.outputs.branch }}
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
with:
platforms: arm,arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v2
- name: Get registry token
id: get_token
shell: bash
run: |
if [ "${{ secrets.CR_PAT }}" ]; then
echo "token=${{ secrets.CR_PAT }}" >> $GITHUB_OUTPUT
else
echo "token=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_OUTPUT
fi
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.CR_PAT }}
password: ${{ steps.get_token.outputs.token }}
- name: Login to DockerHub
uses: docker/login-action@v1
if: github.repository_owner == '0xERR0R'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract branch name
- name: Populate build variables
id: get_vars
shell: bash
run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
id: extract_branch
run: |
REPOSITORY=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')
echo "repository=${REPOSITORY}" >> $GITHUB_OUTPUT
echo "REPOSITORY: ${REPOSITORY}"
BRANCH=${GITHUB_REF#refs/heads/}
echo "branch=${BRANCH}" >> $GITHUB_OUTPUT
echo "Branch: ${BRANCH}"
VERSION=$(git describe --always --tags)
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "VERSION: ${VERSION}"
BUILD_TIME=$(date '+%Y%m%d-%H%M%S')
echo "build_time=${BUILD_TIME}" >> $GITHUB_OUTPUT
echo "BUILD_TIME: ${BUILD_TIME}"
TAGS="ghcr.io/${REPOSITORY}:${BRANCH}"
if [[ "${{ github.repository_owner }}" == "0xERR0R" ]]; then
TAGS="${TAGS} , spx01/blocky:${BRANCH}"
fi
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
echo "TAGS: ${TAGS}"
- name: Build and push
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
push: true
tags: |
ghcr.io/0xerr0r/blocky:${{ steps.extract_branch.outputs.branch }}
spx01/blocky:${{ steps.extract_branch.outputs.branch }}
- name: Scan image
uses: anchore/scan-action@v3
id: scan
tags: ${{ steps.get_vars.outputs.tags }}
build-args: |
VERSION=${{ steps.get_vars.outputs.version }}
BUILD_TIME=${{ steps.get_vars.outputs.build_time }}
cache-from: type=gha
cache-to: type=gha,mode=max
repo-scan:
name: Repo vulnerability scan
runs-on: ubuntu-latest
needs: check
if: needs.check.outputs.enabled == 1
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Run Trivy vulnerability scanner in repo mode
uses: aquasecurity/trivy-action@master
with:
image: "spx01/blocky:${{ steps.extract_branch.outputs.branch }}"
fail-build: false
acs-report-enable: true
- name: upload Anchore scan SARIF report
uses: github/codeql-action/upload-sarif@v1
scan-type: 'fs'
ignore-unfixed: true
format: 'sarif'
output: 'trivy-repo-results.sarif'
severity: 'CRITICAL'
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: ${{ steps.scan.outputs.sarif }}
sarif_file: 'trivy-repo-results.sarif'
image-scan:
name: Image vulnerability scan
runs-on: ubuntu-latest
needs: docker
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Run Trivy vulnerability scanner on Docker image
uses: aquasecurity/trivy-action@master
with:
image-ref: 'ghcr.io/${{ needs.docker.outputs.repository }}:${{ needs.docker.outputs.branch }}'
format: 'sarif'
output: 'trivy-image-results.sarif'
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-image-results.sarif'
image-test:
name: Test docker images
runs-on: ubuntu-latest
needs: docker
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
platforms: arm,arm64
- name: Test images
shell: bash
run: |
echo '::group::Version for linux/amd64'
docker run --rm ghcr.io/${{ needs.docker.outputs.repository }}:${{ needs.docker.outputs.branch }} version
echo '::endgroup::'
echo '::group::Version for linux/arm/v6'
docker run --platform linux/arm/v6 --rm ghcr.io/${{ needs.docker.outputs.repository }}:${{ needs.docker.outputs.branch }} version
echo '::endgroup::'
echo '::group::Version for linux/arm/v7'
docker run --platform linux/arm/v7 --rm ghcr.io/${{ needs.docker.outputs.repository }}:${{ needs.docker.outputs.branch }} version
echo '::endgroup::'
echo '::group::Version for linux/arm64'
docker run --platform linux/arm64 --rm ghcr.io/${{ needs.docker.outputs.repository }}:${{ needs.docker.outputs.branch }} version
echo '::endgroup::'

View File

@ -8,8 +8,8 @@ jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: 3.x
- run: pip install mkdocs-material

View File

@ -9,12 +9,12 @@ jobs:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: '1.18'
- uses: actions/checkout@v2
go-version-file: go.mod
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.46.2
args: --timeout 5m0s
run: make lint

View File

@ -7,21 +7,20 @@ on:
jobs:
build:
runs-on: ubuntu-latest
if: github.repository_owner == '0xERR0R'
steps:
- name: Set up Go 1.18
uses: actions/setup-go@v1
with:
go-version: 1.18
id: go
- name: Checkout
uses: actions/checkout@v2
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: Build
run: make build
@ -30,40 +29,59 @@ jobs:
- name: Docker meta
id: docker_meta
uses: crazy-max/ghaction-docker-meta@v1
uses: crazy-max/ghaction-docker-meta@v4
with:
images: spx01/blocky,ghcr.io/0xerr0r/blocky
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
with:
platforms: arm,arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v2
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.CR_PAT }}
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Populate build variables
id: get_vars
shell: bash
run: |
VERSION=$(git describe --always --tags)
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "VERSION: ${VERSION}"
BUILD_TIME=$(date '+%Y%m%d-%H%M%S')
echo "build_time=${BUILD_TIME}" >> $GITHUB_OUTPUT
echo "BUILD_TIME: ${BUILD_TIME}"
- name: Build and push
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}
build-args: |
VERSION=${{ steps.get_vars.outputs.version }}
BUILD_TIME=${{ steps.get_vars.outputs.build_time }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
uses: goreleaser/goreleaser-action@v3
with:
version: latest
args: release --rm-dist

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
.idea/
.vscode/
*.iml
/*.pem
bin/
@ -13,3 +14,5 @@ todo.txt
!docs/config.yml
node_modules
package-lock.json
.vscode/
vendor/

View File

@ -3,7 +3,6 @@ linters:
- asciicheck
- bidichk
- bodyclose
- deadcode
- depguard
- dogsled
- dupl
@ -30,7 +29,6 @@ linters:
- gosimple
- govet
- grouper
- ifshort
- importas
- ineffassign
- lll
@ -41,25 +39,24 @@ linters:
- nilerr
- nilnil
- nlreturn
- nolintlint
- nosprintfhostport
- prealloc
- predeclared
- revive
- sqlclosecheck
- staticcheck
- structcheck
- stylecheck
- tenv
- typecheck
- unconvert
- unparam
- unused
- varcheck
- wastedassign
- whitespace
- wsl
disable:
- noctx
- contextcheck
- scopelint
disable-all: false

View File

@ -45,6 +45,7 @@ snapshot:
checksum:
name_template: "{{ .ProjectName }}_checksums.txt"
changelog:
use: github
sort: asc
filters:
exclude:

View File

@ -1,43 +1,79 @@
# build stage
FROM golang:1.18-alpine AS build-env
RUN apk add --no-cache \
git \
make \
gcc \
libc-dev \
zip \
ca-certificates
# ----------- stage: ca-certs
# get newest certificates in seperate stage for caching
FROM --platform=$BUILDPLATFORM alpine:3.16 AS ca-certs
RUN apk add --no-cache ca-certificates
ENV GO111MODULE=on \
CGO_ENABLED=0
WORKDIR /src
# update certificates and use the apk ones if update fails
RUN --mount=type=cache,target=/etc/ssl/certs \
update-ca-certificates 2>/dev/null || true
# ----------- stage: zig-env
# zig compiler is used for CGO cross compilation
# even though CGO is disabled it is used in the os and net package
FROM --platform=$BUILDPLATFORM ghcr.io/euantorano/zig:master AS zig-env
# ----------- stage: build
FROM --platform=$BUILDPLATFORM golang:1-alpine AS build
# required arguments
ARG VERSION
ARG BUILD_TIME
# auto provided by Docker
# https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
# set working directory
WORKDIR /go/src
# download packages
COPY go.mod go.sum ./
RUN go mod download
RUN --mount=type=cache,target=/go/pkg \
go mod download
# add source
ADD . .
COPY . .
ARG opts
RUN env ${opts} make build
# setup go & zig as CGO compiler
COPY --from=zig-env /usr/local/bin/zig /usr/local/bin/zig
ENV PATH="/usr/local/bin/zig:${PATH}" \
CC="zigcc" \
CXX="zigcpp" \
CGO_ENABLED=0 \
GOOS="linux" \
GOARCH=$TARGETARCH \
GO_SKIP_GENERATE=1\
GO_BUILD_FLAGS="-tags static -v " \
BIN_USER=100\
BIN_AUTOCAB=1 \
BIN_OUT_DIR="/bin"
# final stage
FROM alpine:3.16
# add make & libcap
RUN apk add --no-cache make libcap
# build binary
RUN --mount=type=bind,target=. \
--mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg \
make build GOARM=${TARGETVARIANT##*v}
# ----------- stage: final
FROM scratch
LABEL org.opencontainers.image.source="https://github.com/0xERR0R/blocky" \
org.opencontainers.image.url="https://github.com/0xERR0R/blocky" \
org.opencontainers.image.title="DNS proxy as ad-blocker for local network"
COPY --from=build-env /src/bin/blocky /app/blocky
RUN apk add --no-cache ca-certificates bind-tools tini tzdata libcap && \
adduser -S -D -H -h /app -s /sbin/nologin blocky && \
setcap 'cap_net_bind_service=+ep' /app/blocky
HEALTHCHECK --interval=1m --timeout=3s CMD dig @127.0.0.1 -p 53 healthcheck.blocky +tcp +short || exit 1
USER blocky
USER 100
WORKDIR /app
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["sh", "-c", "/app/blocky --config ${CONFIG_FILE:-/app/config.yml}"]
COPY --from=ca-certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /bin/blocky /app/blocky
ENV BLOCKY_CONFIG_FILE=/app/config.yml
ENTRYPOINT ["/app/blocky"]
HEALTHCHECK --interval=1m --timeout=3s CMD ["/app/blocky", "healthcheck"]

View File

@ -1,25 +1,36 @@
#!/usr/bin/env bash
.PHONY: all clean build swagger test lint run fmt docker-build help
.DEFAULT_GOAL:=help
.PHONY: all clean build swagger test lint run help
.DEFAULT_GOAL := help
VERSION?=$(shell git describe --always --tags)
BUILD_TIME?=$(shell date '+%Y%m%d-%H%M%S')
DOCKER_IMAGE_NAME=spx01/blocky
VERSION := $(shell git describe --always --tags)
BUILD_TIME=$(shell date '+%Y%m%d-%H%M%S')
DOCKER_IMAGE_NAME="spx01/blocky"
BINARY_NAME=blocky
BIN_OUT_DIR=bin
BINARY_NAME:=blocky
BIN_OUT_DIR?=bin
GOARCH?=$(shell go env GOARCH)
GOARM?=$(shell go env GOARM)
GO_BUILD_FLAGS?=-v
GO_BUILD_LD_FLAGS:=\
-w \
-s \
-X github.com/0xERR0R/blocky/util.Version=${VERSION} \
-X github.com/0xERR0R/blocky/util.BuildTime=${BUILD_TIME} \
-X github.com/0xERR0R/blocky/util.Architecture=${GOARCH}${GOARM}
GO_BUILD_OUTPUT:=$(BIN_OUT_DIR)/$(BINARY_NAME)$(BINARY_SUFFIX)
export PATH=$(shell go env GOPATH)/bin:$(shell echo $$PATH)
all: build test lint ## Build binary (with tests)
clean: ## cleans output directory
$(shell rm -rf $(BIN_OUT_DIR)/*)
rm -rf $(BIN_OUT_DIR)/*
swagger: ## creates swagger documentation as html file
go install github.com/swaggo/swag/cmd/swag@v1.6.9
npm install bootprint bootprint-openapi html-inline
$(shell go env GOPATH)/bin/swag init -g api/api.go
go run github.com/swaggo/swag/cmd/swag init -g api/api.go
$(shell) node_modules/bootprint/bin/bootprint.js openapi docs/swagger.json /tmp/swagger/
$(shell) node_modules/html-inline/bin/cmd.js /tmp/swagger/index.html > docs/swagger.html
@ -27,19 +38,30 @@ serve_docs: ## serves online docs
mkdocs serve
build: ## Build binary
go install github.com/abice/go-enum@v0.4.0
ifdef GO_SKIP_GENERATE
$(info skipping go generate)
else
go generate ./...
go build -v -ldflags="-w -s -X github.com/0xERR0R/blocky/util.Version=${VERSION} -X github.com/0xERR0R/blocky/util.BuildTime=${BUILD_TIME}" -o $(BIN_OUT_DIR)/$(BINARY_NAME)$(BINARY_SUFFIX)
endif
go build $(GO_BUILD_FLAGS) -ldflags="$(GO_BUILD_LD_FLAGS)" -o $(GO_BUILD_OUTPUT)
ifdef BIN_USER
$(info setting owner of $(GO_BUILD_OUTPUT) to $(BIN_USER))
chown $(BIN_USER) $(GO_BUILD_OUTPUT)
endif
ifdef BIN_AUTOCAB
$(info setting cap_net_bind_service to $(GO_BUILD_OUTPUT))
setcap 'cap_net_bind_service=+ep' $(GO_BUILD_OUTPUT)
endif
test: ## run tests
go test -v -coverprofile=coverage.txt -covermode=atomic -cover ./...
go run github.com/onsi/ginkgo/v2/ginkgo -v --coverprofile=coverage.txt --covermode=atomic -cover ./...
race: ## run tests with race detector
go test -race -short ./...
go run github.com/onsi/ginkgo/v2/ginkgo --race ./...
lint: build ## run golangcli-lint checks
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.46.2
$(shell go env GOPATH)/bin/golangci-lint run
lint: ## run golangcli-lint checks
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.50.1
golangci-lint run --timeout 5m
run: build ## Build and run binary
./$(BIN_OUT_DIR)/$(BINARY_NAME)
@ -47,8 +69,15 @@ run: build ## Build and run binary
fmt: ## gofmt and goimports all go files
find . -name '*.go' | while read -r file; do gofmt -w -s "$$file"; goimports -w "$$file"; done
docker-build: ## Build docker image
docker build --network=host --tag ${DOCKER_IMAGE_NAME} .
docker-build: ## Build docker image
go generate ./...
docker buildx build \
--build-arg VERSION=${VERSION} \
--build-arg BUILD_TIME=${BUILD_TIME} \
--network=host \
-o type=docker \
-t ${DOCKER_IMAGE_NAME} \
.
help: ## Shows help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

View File

@ -12,6 +12,11 @@ import (
"github.com/go-chi/chi/v5"
)
const (
contentTypeHeader = "content-type"
jsonContentType = "application/json"
)
// BlockingControl interface to control the blocking status
type BlockingControl interface {
EnableBlocking()
@ -57,7 +62,8 @@ func registerListRefreshEndpoints(router chi.Router, refresher ListRefresher) {
// @Tags lists
// @Success 200 "Lists were reloaded"
// @Router /lists/refresh [post]
func (l *ListRefreshEndpoint) apiListRefresh(_ http.ResponseWriter, _ *http.Request) {
func (l *ListRefreshEndpoint) apiListRefresh(rw http.ResponseWriter, _ *http.Request) {
rw.Header().Set(contentTypeHeader, jsonContentType)
l.refresher.RefreshLists()
}
@ -75,10 +81,12 @@ func registerBlockingEndpoints(router chi.Router, control BlockingControl) {
// @Tags blocking
// @Success 200 "Blocking is enabled"
// @Router /blocking/enable [get]
func (s *BlockingEndpoint) apiBlockingEnable(_ http.ResponseWriter, _ *http.Request) {
func (s *BlockingEndpoint) apiBlockingEnable(rw http.ResponseWriter, _ *http.Request) {
log.Log().Info("enabling blocking...")
s.control.EnableBlocking()
rw.Header().Set(contentTypeHeader, jsonContentType)
}
// apiBlockingDisable is the http endpoint to disable the blocking status
@ -98,6 +106,8 @@ func (s *BlockingEndpoint) apiBlockingDisable(rw http.ResponseWriter, req *http.
err error
)
rw.Header().Set(contentTypeHeader, jsonContentType)
// parse duration from query parameter
durationParam := req.URL.Query().Get("duration")
if len(durationParam) > 0 {
@ -132,6 +142,8 @@ func (s *BlockingEndpoint) apiBlockingDisable(rw http.ResponseWriter, req *http.
func (s *BlockingEndpoint) apiBlockingStatus(rw http.ResponseWriter, _ *http.Request) {
status := s.control.BlockingStatus()
rw.Header().Set(contentTypeHeader, jsonContentType)
response, err := json.Marshal(status)
util.LogOnError("unable to marshal response ", err)

View File

@ -51,9 +51,9 @@ var _ = Describe("API tests", func() {
r := &ListRefreshMock{}
sut := &ListRefreshEndpoint{refresher: r}
It("should trigger the list refresh", func() {
httpCode, _ := DoGetRequest("/api/lists/refresh", sut.apiListRefresh)
Expect(httpCode).Should(Equal(http.StatusOK))
Expect(r.refreshTriggered).Should(BeTrue())
resp, _ := DoGetRequest("/api/lists/refresh", sut.apiListRefresh)
Expect(resp).Should(HaveHTTPStatus(http.StatusOK))
Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/json"))
})
})
@ -74,18 +74,18 @@ var _ = Describe("API tests", func() {
It("should disable blocking resolver", func() {
By("Calling Rest API to deactivate", func() {
httpCode, _ := DoGetRequest("/api/blocking/disable", sut.apiBlockingDisable)
Expect(httpCode).Should(Equal(http.StatusOK))
Expect(bc.enabled).Should(BeFalse())
resp, _ := DoGetRequest("/api/blocking/disable", sut.apiBlockingDisable)
Expect(resp).Should(HaveHTTPStatus(http.StatusOK))
Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/json"))
})
})
})
When("Disable blocking is called with a wrong parameter", func() {
It("Should return http bad request as return code", func() {
httpCode, _ := DoGetRequest("/api/blocking/disable?duration=xyz", sut.apiBlockingDisable)
Expect(httpCode).Should(Equal(http.StatusBadRequest))
resp, _ := DoGetRequest("/api/blocking/disable?duration=xyz", sut.apiBlockingDisable)
Expect(resp).Should(HaveHTTPStatus(http.StatusBadRequest))
Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/json"))
})
})
@ -96,8 +96,9 @@ var _ = Describe("API tests", func() {
})
By("Calling Rest API to deactivate blocking for 0.5 sec", func() {
httpCode, _ := DoGetRequest("/api/blocking/disable?duration=500ms", sut.apiBlockingDisable)
Expect(httpCode).Should(Equal(http.StatusOK))
resp, _ := DoGetRequest("/api/blocking/disable?duration=500ms", sut.apiBlockingDisable)
Expect(resp).Should(HaveHTTPStatus(http.StatusOK))
Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/json"))
})
By("ensure that the blocking is disabled", func() {
@ -110,13 +111,15 @@ var _ = Describe("API tests", func() {
When("Blocking status is called", func() {
It("should return correct status", func() {
By("enable blocking via API", func() {
httpCode, _ := DoGetRequest("/api/blocking/enable", sut.apiBlockingEnable)
Expect(httpCode).Should(Equal(http.StatusOK))
resp, _ := DoGetRequest("/api/blocking/enable", sut.apiBlockingEnable)
Expect(resp).Should(HaveHTTPStatus(http.StatusOK))
Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/json"))
})
By("Query blocking status via API should return 'enabled'", func() {
httpCode, body := DoGetRequest("/api/blocking/status", sut.apiBlockingStatus)
Expect(httpCode).Should(Equal(http.StatusOK))
resp, body := DoGetRequest("/api/blocking/status", sut.apiBlockingStatus)
Expect(resp).Should(HaveHTTPStatus(http.StatusOK))
Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/json"))
var result BlockingStatus
err := json.NewDecoder(body).Decode(&result)
Expect(err).Should(Succeed())
@ -125,13 +128,15 @@ var _ = Describe("API tests", func() {
})
By("disable blocking via API", func() {
httpCode, _ := DoGetRequest("/api/blocking/disable?duration=500ms", sut.apiBlockingDisable)
Expect(httpCode).Should(Equal(http.StatusOK))
resp, _ := DoGetRequest("/api/blocking/disable?duration=500ms", sut.apiBlockingDisable)
Expect(resp).Should(HaveHTTPStatus(http.StatusOK))
Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/json"))
})
By("Query blocking status via API again should return 'disabled'", func() {
httpCode, body := DoGetRequest("/api/blocking/status", sut.apiBlockingStatus)
Expect(httpCode).Should(Equal(http.StatusOK))
resp, body := DoGetRequest("/api/blocking/status", sut.apiBlockingStatus)
Expect(resp).Should(HaveHTTPStatus(http.StatusOK))
Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/json"))
var result BlockingStatus
err := json.NewDecoder(body).Decode(&result)

View File

@ -120,8 +120,8 @@ var _ = Describe("Expiration cache", func() {
val, ttl := cache.Get("key1")
Expect(val).Should(Equal("val2"))
Expect(ttl.Milliseconds()).Should(And(
BeNumerically(">", 900)),
BeNumerically("<=", 1000))
BeNumerically(">", 900),
BeNumerically("<=", 1000)))
})
It("should delete the key if function returns nil", func() {

44
cmd/healthcheck.go Normal file
View File

@ -0,0 +1,44 @@
package cmd
import (
"fmt"
"net"
"github.com/miekg/dns"
"github.com/spf13/cobra"
)
const (
defaultDNSPort = 53
)
func NewHealthcheckCommand() *cobra.Command {
c := &cobra.Command{
Use: "healthcheck",
Short: "performs healthcheck",
RunE: healthcheck,
}
c.Flags().Uint16P("port", "p", defaultDNSPort, "healthcheck port 5333")
return c
}
func healthcheck(cmd *cobra.Command, args []string) error {
port, _ := cmd.Flags().GetUint16("port")
c := new(dns.Client)
c.Net = "tcp"
m := new(dns.Msg)
m.SetQuestion("healthcheck.blocky.", dns.TypeA)
_, _, err := c.Exchange(m, net.JoinHostPort("127.0.0.1", fmt.Sprintf("%d", port)))
if err == nil {
fmt.Println("OK")
} else {
fmt.Println("NOT OK")
}
return err
}

63
cmd/healthcheck_test.go Normal file
View File

@ -0,0 +1,63 @@
package cmd
import (
"fmt"
"github.com/miekg/dns"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Healthcheck command", func() {
Describe("Call healthcheck command", func() {
It("should fail", func() {
c := NewHealthcheckCommand()
c.SetArgs([]string{"-p", "5344"})
err := c.Execute()
Expect(err).Should(HaveOccurred())
})
It("shoul succeed", func() {
srv := createMockServer()
go func() {
defer GinkgoRecover()
err := srv.ListenAndServe()
Expect(err).Should(Succeed())
}()
DeferCleanup(srv.Shutdown)
Eventually(func() error {
c := NewHealthcheckCommand()
c.SetArgs([]string{"-p", "5333"})
return c.Execute()
}, "1s").Should(Succeed())
})
})
})
func createMockServer() *dns.Server {
res := &dns.Server{
Addr: "127.0.0.1:5333",
Net: "tcp",
Handler: dns.NewServeMux(),
NotifyStartedFunc: func() {
fmt.Println("Mock helthcheck server is up")
},
}
th := res.Handler.(*dns.ServeMux)
th.HandleFunc("healthcheck.blocky", func(w dns.ResponseWriter, request *dns.Msg) {
resp := new(dns.Msg)
resp.SetReply(request)
resp.Rcode = dns.RcodeSuccess
err := w.WriteMsg(resp)
Expect(err).Should(Succeed())
})
return res
}

View File

@ -2,7 +2,7 @@ package cmd
import (
"fmt"
"io/ioutil"
"io"
"net/http"
"github.com/0xERR0R/blocky/api"
@ -39,7 +39,7 @@ func refreshList(_ *cobra.Command, _ []string) error {
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := ioutil.ReadAll(resp.Body)
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("response NOK, %s %s", resp.Status, string(body))
}

View File

@ -4,7 +4,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"io"
"net/http"
"github.com/0xERR0R/blocky/api"
@ -53,7 +53,7 @@ func query(cmd *cobra.Command, args []string) error {
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := ioutil.ReadAll(resp.Body)
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("response NOK, %s %s", resp.Status, string(body))
}

View File

@ -2,7 +2,9 @@ package cmd
import (
"fmt"
"net"
"os"
"strconv"
"strings"
"github.com/0xERR0R/blocky/config"
@ -20,9 +22,11 @@ var (
)
const (
defaultPort = 4000
defaultHost = "localhost"
defaultConfigPath = "./config.yml"
defaultPort = 4000
defaultHost = "localhost"
defaultConfigPath = "./config.yml"
configFileEnvVar = "BLOCKY_CONFIG_FILE"
configFileEnvVarOld = "CONFIG_FILE"
)
// NewRootCommand creates a new root cli command instance
@ -49,13 +53,14 @@ Complete documentation is available at https://github.com/0xERR0R/blocky`,
NewVersionCommand(),
newServeCommand(),
newBlockingCommand(),
NewListsCommand())
NewListsCommand(),
NewHealthcheckCommand())
return c
}
func apiURL(path string) string {
return fmt.Sprintf("http://%s:%d%s", apiHost, apiPort, path)
return fmt.Sprintf("http://%s%s", net.JoinHostPort(apiHost, strconv.Itoa(int(apiPort))), path)
}
//nolint:gochecknoinits
@ -64,6 +69,18 @@ func init() {
}
func initConfig() {
if configPath == defaultConfigPath {
val, present := os.LookupEnv(configFileEnvVar)
if present {
configPath = val
} else {
val, present = os.LookupEnv(configFileEnvVarOld)
if present {
configPath = val
}
}
}
cfg, err := config.LoadConfig(configPath, false)
if err != nil {
util.FatalOnError("unable to load configuration: ", err)

View File

@ -1,23 +1,73 @@
package cmd
import (
"io/ioutil"
"io"
"os"
"github.com/0xERR0R/blocky/log"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "github.com/0xERR0R/blocky/helpertest"
)
var _ = Describe("Version command", func() {
var _ = Describe("root command", func() {
When("Version command is called", func() {
log.Log().ExitFunc = nil
It("should execute without error", func() {
c := NewRootCommand()
c.SetOutput(ioutil.Discard)
c.SetOutput(io.Discard)
c.SetArgs([]string{"help"})
err := c.Execute()
Expect(err).Should(Succeed())
})
})
When("Config provided", func() {
var (
tmpDir *TmpFolder
tmpFile *TmpFile
)
BeforeEach(func() {
configPath = defaultConfigPath
tmpDir = NewTmpFolder("RootCommand")
Expect(tmpDir.Error).Should(Succeed())
DeferCleanup(tmpDir.Clean)
tmpFile = tmpDir.CreateStringFile("config",
"upstream:",
" default:",
" - 1.1.1.1",
"blocking:",
" blackLists:",
" ads:",
" - https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt",
" clientGroupsBlock:",
" default:",
" - ads",
"port: 5333",
)
Expect(tmpFile.Error).Should(Succeed())
})
It("should accept old env var", func() {
os.Setenv(configFileEnvVarOld, tmpFile.Path)
DeferCleanup(func() { os.Unsetenv(configFileEnvVarOld) })
initConfig()
Expect(configPath).Should(Equal(tmpFile.Path))
})
It("should accept new env var", func() {
os.Setenv(configFileEnvVar, tmpFile.Path)
DeferCleanup(func() { os.Unsetenv(configFileEnvVar) })
initConfig()
Expect(configPath).Should(Equal(tmpFile.Path))
})
})
})

View File

@ -1,11 +1,10 @@
package cmd
import (
"time"
"github.com/0xERR0R/blocky/config"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Serve command", func() {
@ -21,13 +20,18 @@ var _ = Describe("Serve command", func() {
isConfigMandatory = false
grClosure := make(chan interface{})
go func() {
_ = startServer(newServeCommand(), []string{})
defer GinkgoRecover()
err := startServer(newServeCommand(), []string{})
Expect(err).Should(HaveOccurred())
close(grClosure)
}()
time.Sleep(100 * time.Millisecond)
done <- true
Eventually(grClosure).Should(BeClosed())
})
})
})

View File

@ -21,4 +21,5 @@ func printVersion(_ *cobra.Command, _ []string) {
fmt.Println("blocky")
fmt.Printf("Version: %s\n", util.Version)
fmt.Printf("Build time: %s\n", util.BuildTime)
fmt.Printf("Architecture: %s\n", util.Architecture)
}

View File

@ -1,3 +1,16 @@
coverage:
status:
project:
default:
target: auto
threshold: 80%
patch:
default:
# basic
target: auto
threshold: 0%
base: auto
only_pulls: true
ignore:
- "resolver/mocks.go"
- "**/*_enum.go"

View File

@ -1,10 +1,9 @@
//go:generate go-enum -f=$GOFILE --marshal --names
//go:generate go run github.com/abice/go-enum -f=$GOFILE --marshal --names
package config
import (
"errors"
"fmt"
"io/ioutil"
"net"
"os"
"path/filepath"
@ -12,6 +11,7 @@ import (
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/miekg/dns"
@ -36,6 +36,39 @@ const (
// )
type NetProtocol uint16
// IPVersion represents IP protocol version(s). ENUM(
// dual // IPv4 and IPv6
// v4 // IPv4 only
// v6 // IPv6 only
// )
type IPVersion uint8
func (ipv IPVersion) Net() string {
switch ipv {
case IPVersionDual:
return "ip"
case IPVersionV4:
return "ip4"
case IPVersionV6:
return "ip6"
}
panic(fmt.Errorf("bad value: %s", ipv))
}
func (ipv IPVersion) QTypes() []dns.Type {
switch ipv {
case IPVersionDual:
return []dns.Type{dns.Type(dns.TypeA), dns.Type(dns.TypeAAAA)}
case IPVersionV4:
return []dns.Type{dns.Type(dns.TypeA)}
case IPVersionV6:
return []dns.Type{dns.Type(dns.TypeAAAA)}
}
panic(fmt.Errorf("bad value: %s", ipv))
}
// QueryLogType type of the query log ENUM(
// console // use logger as fallback
// none // no logging
@ -46,6 +79,13 @@ type NetProtocol uint16
// )
type QueryLogType int16
// StartStrategyType upstart strategy ENUM(
// blocking // synchronously download blocking lists on startup
// failOnError // synchronously download blocking lists on startup and shutdown on error
// fast // asyncronously download blocking lists on startup
// )
type StartStrategyType uint16
type QType dns.Type
func (c QType) String() string {
@ -93,10 +133,11 @@ var netDefaultPort = map[NetProtocol]uint16{
// Upstream is the definition of external DNS server
type Upstream struct {
Net NetProtocol
Host string
Port uint16
Path string
Net NetProtocol
Host string
Port uint16
Path string
CommonName string // Common Name to use for certificate verification; optional. "" uses .Host
}
// IsDefault returns true if u is the default value
@ -315,12 +356,14 @@ func (s *QTypeSet) UnmarshalYAML(unmarshal func(interface{}) error) error {
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])$`)
// ParseUpstream creates new Upstream from passed string in format [net]:host[:port][/path]
// ParseUpstream creates new Upstream from passed string in format [net]:host[:port][/path][#commonname]
func ParseUpstream(upstream string) (Upstream, error) {
var path string
var port uint16
commonName, upstream := extractCommonName(upstream)
n, upstream := extractNet(upstream)
path, upstream = extractPath(upstream)
@ -357,13 +400,20 @@ func ParseUpstream(upstream string) (Upstream, error) {
}
return Upstream{
Net: n,
Host: host,
Port: port,
Path: path,
Net: n,
Host: host,
Port: port,
Path: path,
CommonName: commonName,
}, nil
}
func extractCommonName(in string) (string, string) {
upstream, cn, _ := strings.Cut(in, "#")
return cn, upstream
}
func extractPath(in string) (path string, upstream string) {
slashIdx := strings.Index(in, "/")
@ -399,33 +449,37 @@ func extractNet(upstream string) (NetProtocol, string) {
// Config main configuration
// nolint:maligned
type Config struct {
Upstream UpstreamConfig `yaml:"upstream"`
UpstreamTimeout Duration `yaml:"upstreamTimeout" default:"2s"`
CustomDNS CustomDNSConfig `yaml:"customDNS"`
Conditional ConditionalUpstreamConfig `yaml:"conditional"`
Blocking BlockingConfig `yaml:"blocking"`
ClientLookup ClientLookupConfig `yaml:"clientLookup"`
Caching CachingConfig `yaml:"caching"`
QueryLog QueryLogConfig `yaml:"queryLog"`
Prometheus PrometheusConfig `yaml:"prometheus"`
Redis RedisConfig `yaml:"redis"`
LogLevel log.Level `yaml:"logLevel" default:"info"`
LogFormat log.FormatType `yaml:"logFormat" default:"text"`
LogPrivacy bool `yaml:"logPrivacy" default:"false"`
LogTimestamp bool `yaml:"logTimestamp" default:"true"`
DNSPorts ListenConfig `yaml:"port" default:"[\"53\"]"`
HTTPPorts ListenConfig `yaml:"httpPort"`
HTTPSPorts ListenConfig `yaml:"httpsPort"`
TLSPorts ListenConfig `yaml:"tlsPort"`
DoHUserAgent string `yaml:"dohUserAgent"`
MinTLSServeVer string `yaml:"minTlsServeVersion" default:"1.2"`
Upstream UpstreamConfig `yaml:"upstream"`
UpstreamTimeout Duration `yaml:"upstreamTimeout" default:"2s"`
ConnectIPVersion IPVersion `yaml:"connectIPVersion"`
CustomDNS CustomDNSConfig `yaml:"customDNS"`
Conditional ConditionalUpstreamConfig `yaml:"conditional"`
Blocking BlockingConfig `yaml:"blocking"`
ClientLookup ClientLookupConfig `yaml:"clientLookup"`
Caching CachingConfig `yaml:"caching"`
QueryLog QueryLogConfig `yaml:"queryLog"`
Prometheus PrometheusConfig `yaml:"prometheus"`
Redis RedisConfig `yaml:"redis"`
LogLevel log.Level `yaml:"logLevel" default:"info"`
LogFormat log.FormatType `yaml:"logFormat" default:"text"`
LogPrivacy bool `yaml:"logPrivacy" default:"false"`
LogTimestamp bool `yaml:"logTimestamp" default:"true"`
DNSPorts ListenConfig `yaml:"port" default:"[\"53\"]"`
HTTPPorts ListenConfig `yaml:"httpPort"`
HTTPSPorts ListenConfig `yaml:"httpsPort"`
TLSPorts ListenConfig `yaml:"tlsPort"`
DoHUserAgent string `yaml:"dohUserAgent"`
MinTLSServeVer string `yaml:"minTlsServeVersion" default:"1.2"`
StartVerifyUpstream bool `yaml:"startVerifyUpstream" default:"false"`
// Deprecated
DisableIPv6 bool `yaml:"disableIPv6" default:"false"`
CertFile string `yaml:"certFile"`
KeyFile string `yaml:"keyFile"`
BootstrapDNS BootstrapConfig `yaml:"bootstrapDns"`
HostsFile HostsFileConfig `yaml:"hostsFile"`
FqdnOnly bool `yaml:"fqdnOnly" default:"false"`
Filtering FilteringConfig `yaml:"filtering"`
Ede EdeConfig `yaml:"ede"`
}
type BootstrapConfig bootstrapConfig // to avoid infinite recursion. See BootstrapConfig.UnmarshalYAML.
@ -447,7 +501,8 @@ type UpstreamConfig struct {
// RewriteConfig custom DNS configuration
type RewriteConfig struct {
Rewrite map[string]string `yaml:"rewrite"`
Rewrite map[string]string `yaml:"rewrite"`
FallbackUpstream bool `yaml:"fallbackUpstream" default:"false"`
}
// CustomDNSConfig custom DNS configuration
@ -476,17 +531,19 @@ type ConditionalUpstreamMapping struct {
// BlockingConfig configuration for query blocking
type BlockingConfig struct {
BlackLists map[string][]string `yaml:"blackLists"`
WhiteLists map[string][]string `yaml:"whiteLists"`
ClientGroupsBlock map[string][]string `yaml:"clientGroupsBlock"`
BlockType string `yaml:"blockType" default:"ZEROIP"`
BlockTTL Duration `yaml:"blockTTL" default:"6h"`
DownloadTimeout Duration `yaml:"downloadTimeout" default:"60s"`
DownloadAttempts uint `yaml:"downloadAttempts" default:"3"`
DownloadCooldown Duration `yaml:"downloadCooldown" default:"1s"`
RefreshPeriod Duration `yaml:"refreshPeriod" default:"4h"`
FailStartOnListError bool `yaml:"failStartOnListError" default:"false"`
ProcessingConcurrency uint `yaml:"processingConcurrency" default:"4"`
BlackLists map[string][]string `yaml:"blackLists"`
WhiteLists map[string][]string `yaml:"whiteLists"`
ClientGroupsBlock map[string][]string `yaml:"clientGroupsBlock"`
BlockType string `yaml:"blockType" default:"ZEROIP"`
BlockTTL Duration `yaml:"blockTTL" default:"6h"`
DownloadTimeout Duration `yaml:"downloadTimeout" default:"60s"`
DownloadAttempts uint `yaml:"downloadAttempts" default:"3"`
DownloadCooldown Duration `yaml:"downloadCooldown" default:"1s"`
RefreshPeriod Duration `yaml:"refreshPeriod" default:"4h"`
// Deprecated
FailStartOnListError bool `yaml:"failStartOnListError" default:"false"`
ProcessingConcurrency uint `yaml:"processingConcurrency" default:"4"`
StartStrategy StartStrategyType `yaml:"startStrategy" default:"blocking"`
}
// ClientLookupConfig configuration for the client lookup
@ -528,20 +585,31 @@ type RedisConfig struct {
}
type HostsFileConfig struct {
Filepath string `yaml:"filePath"`
HostsTTL Duration `yaml:"hostsTTL" default:"1h"`
RefreshPeriod Duration `yaml:"refreshPeriod" default:"1h"`
Filepath string `yaml:"filePath"`
HostsTTL Duration `yaml:"hostsTTL" default:"1h"`
RefreshPeriod Duration `yaml:"refreshPeriod" default:"1h"`
FilterLoopback bool `yaml:"filterLoopback"`
}
type FilteringConfig struct {
QueryTypes QTypeSet `yaml:"queryTypes"`
}
type EdeConfig struct {
Enable bool `yaml:"enable" default:"false"`
}
// nolint:gochecknoglobals
var config = &Config{}
var (
config = &Config{}
cfgLock sync.RWMutex
)
// LoadConfig creates new config from YAML file or a directory containing YAML files
func LoadConfig(path string, mandatory bool) (*Config, error) {
cfgLock.Lock()
defer cfgLock.Unlock()
cfg := Config{}
if err := defaults.Set(&cfg); err != nil {
return nil, fmt.Errorf("can't apply default values: %w", err)
@ -562,32 +630,14 @@ func LoadConfig(path string, mandatory bool) (*Config, error) {
var data []byte
if fs.IsDir() { //nolint:nestif
err = filepath.WalkDir(path, func(filePath string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if path == filePath {
return nil
}
fileData, err := os.ReadFile(filePath)
if err != nil {
return err
}
data = append(data, []byte("\n")...)
data = append(data, fileData...)
return nil
})
if fs.IsDir() {
data, err = readFromDir(path, data)
if err != nil {
return nil, fmt.Errorf("can't read config files: %w", err)
}
} else {
data, err = ioutil.ReadFile(path)
data, err = os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("can't read config file: %w", err)
}
@ -603,6 +653,57 @@ func LoadConfig(path string, mandatory bool) (*Config, error) {
return &cfg, nil
}
func readFromDir(path string, data []byte) ([]byte, error) {
err := filepath.WalkDir(path, func(filePath string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if path == filePath {
return nil
}
// Ignore non YAML files
if !strings.HasSuffix(filePath, ".yml") && !strings.HasSuffix(filePath, ".yaml") {
return nil
}
isRegular, err := isRegularFile(filePath)
if err != nil {
return err
}
// Ignore non regular files (directories, sockets, etc.)
if !isRegular {
return nil
}
fileData, err := os.ReadFile(filePath)
if err != nil {
return err
}
data = append(data, []byte("\n")...)
data = append(data, fileData...)
return nil
})
return data, err
}
// isRegularFile follows symlinks, so the result is `true` for a symlink to a regular file.
func isRegularFile(path string) (bool, error) {
stat, err := os.Stat(path)
if err != nil {
return false, err
}
isRegular := stat.Mode()&os.ModeType == 0
return isRegular, nil
}
func unmarshalConfig(data []byte, cfg *Config) error {
err := yaml.UnmarshalStrict(data, cfg)
if err != nil {
@ -620,10 +721,24 @@ func validateConfig(cfg *Config) {
cfg.Filtering.QueryTypes.Insert(dns.Type(dns.TypeAAAA))
}
if cfg.Blocking.FailStartOnListError {
log.Log().Warnf("'blocking.failStartOnListError' is deprecated. Please use 'blocking.startStrategy'" +
" with 'failOnError' instead.")
if cfg.Blocking.StartStrategy == StartStrategyTypeBlocking {
cfg.Blocking.StartStrategy = StartStrategyTypeFailOnError
} else if cfg.Blocking.StartStrategy == StartStrategyTypeFast {
log.Log().Warnf("'blocking.startStrategy' with 'fast' will ignore 'blocking.failStartOnListError'.")
}
}
}
// GetConfig returns the current config
func GetConfig() *Config {
cfgLock.RLock()
defer cfgLock.RUnlock()
return config
}

View File

@ -11,6 +11,79 @@ import (
"strings"
)
const (
// IPVersionDual is a IPVersion of type Dual.
// IPv4 and IPv6
IPVersionDual IPVersion = iota
// IPVersionV4 is a IPVersion of type V4.
// IPv4 only
IPVersionV4
// IPVersionV6 is a IPVersion of type V6.
// IPv6 only
IPVersionV6
)
var ErrInvalidIPVersion = fmt.Errorf("not a valid IPVersion, try [%s]", strings.Join(_IPVersionNames, ", "))
const _IPVersionName = "dualv4v6"
var _IPVersionNames = []string{
_IPVersionName[0:4],
_IPVersionName[4:6],
_IPVersionName[6:8],
}
// IPVersionNames returns a list of possible string values of IPVersion.
func IPVersionNames() []string {
tmp := make([]string, len(_IPVersionNames))
copy(tmp, _IPVersionNames)
return tmp
}
var _IPVersionMap = map[IPVersion]string{
IPVersionDual: _IPVersionName[0:4],
IPVersionV4: _IPVersionName[4:6],
IPVersionV6: _IPVersionName[6:8],
}
// String implements the Stringer interface.
func (x IPVersion) String() string {
if str, ok := _IPVersionMap[x]; ok {
return str
}
return fmt.Sprintf("IPVersion(%d)", x)
}
var _IPVersionValue = map[string]IPVersion{
_IPVersionName[0:4]: IPVersionDual,
_IPVersionName[4:6]: IPVersionV4,
_IPVersionName[6:8]: IPVersionV6,
}
// ParseIPVersion attempts to convert a string to a IPVersion.
func ParseIPVersion(name string) (IPVersion, error) {
if x, ok := _IPVersionValue[name]; ok {
return x, nil
}
return IPVersion(0), fmt.Errorf("%s is %w", name, ErrInvalidIPVersion)
}
// MarshalText implements the text marshaller method.
func (x IPVersion) MarshalText() ([]byte, error) {
return []byte(x.String()), nil
}
// UnmarshalText implements the text unmarshaller method.
func (x *IPVersion) UnmarshalText(text []byte) error {
name := string(text)
tmp, err := ParseIPVersion(name)
if err != nil {
return err
}
*x = tmp
return nil
}
const (
// NetProtocolTcpUdp is a NetProtocol of type Tcp+Udp.
// TCP and UDP protocols
@ -23,6 +96,8 @@ const (
NetProtocolHttps
)
var ErrInvalidNetProtocol = fmt.Errorf("not a valid NetProtocol, try [%s]", strings.Join(_NetProtocolNames, ", "))
const _NetProtocolName = "tcp+udptcp-tlshttps"
var _NetProtocolNames = []string{
@ -63,7 +138,7 @@ func ParseNetProtocol(name string) (NetProtocol, error) {
if x, ok := _NetProtocolValue[name]; ok {
return x, nil
}
return NetProtocol(0), fmt.Errorf("%s is not a valid NetProtocol, try [%s]", name, strings.Join(_NetProtocolNames, ", "))
return NetProtocol(0), fmt.Errorf("%s is %w", name, ErrInvalidNetProtocol)
}
// MarshalText implements the text marshaller method.
@ -103,6 +178,8 @@ const (
QueryLogTypeCsvClient
)
var ErrInvalidQueryLogType = fmt.Errorf("not a valid QueryLogType, try [%s]", strings.Join(_QueryLogTypeNames, ", "))
const _QueryLogTypeName = "consolenonemysqlpostgresqlcsvcsv-client"
var _QueryLogTypeNames = []string{
@ -152,7 +229,7 @@ func ParseQueryLogType(name string) (QueryLogType, error) {
if x, ok := _QueryLogTypeValue[name]; ok {
return x, nil
}
return QueryLogType(0), fmt.Errorf("%s is not a valid QueryLogType, try [%s]", name, strings.Join(_QueryLogTypeNames, ", "))
return QueryLogType(0), fmt.Errorf("%s is %w", name, ErrInvalidQueryLogType)
}
// MarshalText implements the text marshaller method.
@ -170,3 +247,76 @@ func (x *QueryLogType) UnmarshalText(text []byte) error {
*x = tmp
return nil
}
const (
// StartStrategyTypeBlocking is a StartStrategyType of type Blocking.
// synchronously download blocking lists on startup
StartStrategyTypeBlocking StartStrategyType = iota
// StartStrategyTypeFailOnError is a StartStrategyType of type FailOnError.
// synchronously download blocking lists on startup and shutdown on error
StartStrategyTypeFailOnError
// StartStrategyTypeFast is a StartStrategyType of type Fast.
// asyncronously download blocking lists on startup
StartStrategyTypeFast
)
var ErrInvalidStartStrategyType = fmt.Errorf("not a valid StartStrategyType, try [%s]", strings.Join(_StartStrategyTypeNames, ", "))
const _StartStrategyTypeName = "blockingfailOnErrorfast"
var _StartStrategyTypeNames = []string{
_StartStrategyTypeName[0:8],
_StartStrategyTypeName[8:19],
_StartStrategyTypeName[19:23],
}
// StartStrategyTypeNames returns a list of possible string values of StartStrategyType.
func StartStrategyTypeNames() []string {
tmp := make([]string, len(_StartStrategyTypeNames))
copy(tmp, _StartStrategyTypeNames)
return tmp
}
var _StartStrategyTypeMap = map[StartStrategyType]string{
StartStrategyTypeBlocking: _StartStrategyTypeName[0:8],
StartStrategyTypeFailOnError: _StartStrategyTypeName[8:19],
StartStrategyTypeFast: _StartStrategyTypeName[19:23],
}
// String implements the Stringer interface.
func (x StartStrategyType) String() string {
if str, ok := _StartStrategyTypeMap[x]; ok {
return str
}
return fmt.Sprintf("StartStrategyType(%d)", x)
}
var _StartStrategyTypeValue = map[string]StartStrategyType{
_StartStrategyTypeName[0:8]: StartStrategyTypeBlocking,
_StartStrategyTypeName[8:19]: StartStrategyTypeFailOnError,
_StartStrategyTypeName[19:23]: StartStrategyTypeFast,
}
// ParseStartStrategyType attempts to convert a string to a StartStrategyType.
func ParseStartStrategyType(name string) (StartStrategyType, error) {
if x, ok := _StartStrategyTypeValue[name]; ok {
return x, nil
}
return StartStrategyType(0), fmt.Errorf("%s is %w", name, ErrInvalidStartStrategyType)
}
// MarshalText implements the text marshaller method.
func (x StartStrategyType) MarshalText() ([]byte, error) {
return []byte(x.String()), nil
}
// UnmarshalText implements the text unmarshaller method.
func (x *StartStrategyType) UnmarshalText(text []byte) error {
name := string(text)
tmp, err := ParseStartStrategyType(name)
if err != nil {
return err
}
*x = tmp
return nil
}

View File

@ -2,26 +2,36 @@ package config
import (
"errors"
"io/ioutil"
"net"
"os"
"time"
"github.com/miekg/dns"
"github.com/0xERR0R/blocky/helpertest"
. "github.com/0xERR0R/blocky/log"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Config", func() {
var (
tmpDir *helpertest.TmpFolder
err error
)
BeforeEach(func() {
tmpDir = helpertest.NewTmpFolder("config")
Expect(tmpDir.Error).Should(Succeed())
DeferCleanup(tmpDir.Clean)
})
Describe("Creation of Config", func() {
When("Test config file will be parsed", func() {
It("should return a valid config struct", func() {
err := os.Chdir("../testdata")
Expect(err).Should(Succeed())
confFile := writeConfigYml(tmpDir)
Expect(confFile.Error).Should(Succeed())
_, err = LoadConfig("config.yml", true)
_, err = LoadConfig(confFile.Path, true)
Expect(err).Should(Succeed())
defaultTestFileConfig()
@ -29,36 +39,54 @@ var _ = Describe("Config", func() {
})
When("Test file does not exist", func() {
It("should fail", func() {
_, err := LoadConfig("../testdata/config-does-not-exist.yaml", true)
_, err := LoadConfig(tmpDir.JoinPath("config-does-not-exist.yaml"), true)
Expect(err).Should(Not(Succeed()))
})
})
When("Multiple config files are used", func() {
It("should return a valid config struct", func() {
_, err := LoadConfig("../testdata/config/", true)
err = writeConfigDir(tmpDir)
Expect(err).Should(Succeed())
_, err := LoadConfig(tmpDir.Path, true)
Expect(err).Should(Succeed())
defaultTestFileConfig()
})
It("should ignore non YAML files", func() {
err = writeConfigDir(tmpDir)
Expect(err).Should(Succeed())
tmpDir.CreateStringFile("ignore-me.txt", "THIS SHOULD BE IGNORED!")
_, err := LoadConfig(tmpDir.Path, true)
Expect(err).Should(Succeed())
})
It("should ignore non regular files", func() {
err = writeConfigDir(tmpDir)
Expect(err).Should(Succeed())
tmpDir.CreateSubFolder("subfolder")
tmpDir.CreateSubFolder("subfolder.yml")
_, err := LoadConfig(tmpDir.Path, true)
Expect(err).Should(Succeed())
})
})
When("Config folder does not exist", func() {
It("should fail", func() {
_, err := LoadConfig("../testdata/does-not-exist-config/", true)
_, err := LoadConfig(tmpDir.JoinPath("does-not-exist-config/"), true)
Expect(err).Should(Not(Succeed()))
})
})
When("config file is malformed", func() {
It("should return error", func() {
cfgFile := tmpDir.CreateStringFile("config.yml", "malformed_config")
Expect(cfgFile.Error).Should(Succeed())
dir, err := ioutil.TempDir("", "blocky")
defer os.Remove(dir)
Expect(err).Should(Succeed())
err = os.Chdir(dir)
Expect(err).Should(Succeed())
err = ioutil.WriteFile("config.yml", []byte("malformed_config"), 0600)
Expect(err).Should(Succeed())
_, err = LoadConfig("config.yml", true)
_, err = LoadConfig(cfgFile.Path, true)
Expect(err).Should(HaveOccurred())
Expect(err.Error()).Should(ContainSubstring("wrong file structure"))
})
@ -172,22 +200,39 @@ bootstrapDns:
})
})
When("Deprecated parameter 'failStartOnListError' is set", func() {
var (
c Config
)
BeforeEach(func() {
c = Config{
Blocking: BlockingConfig{
FailStartOnListError: true,
StartStrategy: StartStrategyTypeBlocking,
},
}
})
It("should change StartStrategy blocking to failOnError", func() {
validateConfig(&c)
Expect(c.Blocking.StartStrategy).Should(Equal(StartStrategyTypeFailOnError))
})
It("shouldn't change StartStrategy if set to fast", func() {
c.Blocking.StartStrategy = StartStrategyTypeFast
validateConfig(&c)
Expect(c.Blocking.StartStrategy).Should(Equal(StartStrategyTypeFast))
})
})
When("config directory does not exist", func() {
It("should return error", func() {
err := os.Chdir("../..")
Expect(err).Should(Succeed())
_, err = LoadConfig("config.yml", true)
_, err = LoadConfig(tmpDir.JoinPath("config.yml"), true)
Expect(err).Should(HaveOccurred())
Expect(err.Error()).Should(ContainSubstring("no such file or directory"))
})
It("should use default config if config is not mandatory", func() {
err := os.Chdir("../..")
Expect(err).Should(Succeed())
_, err = LoadConfig("config.yml", false)
_, err = LoadConfig(tmpDir.JoinPath("config.yml"), false)
Expect(err).Should(Succeed())
Expect(config.LogLevel).Should(Equal(LevelInfo))
@ -381,6 +426,10 @@ bootstrapDns:
"tcp-tls:4.4.4.4",
Upstream{Net: NetProtocolTcpTls, Host: "4.4.4.4", Port: 853},
false),
Entry("tcp-tls with common name",
"tcp-tls:1.1.1.2#security.cloudflare-dns.com",
Upstream{Net: NetProtocolTcpTls, Host: "1.1.1.2", Port: 853, CommonName: "security.cloudflare-dns.com"},
false),
Entry("DoH without port, use default",
"https:4.4.4.4",
Upstream{Net: NetProtocolHttps, Host: "4.4.4.4", Port: 443},
@ -578,6 +627,126 @@ func defaultTestFileConfig() {
Expect(config.DoHUserAgent).Should(Equal("testBlocky"))
Expect(config.MinTLSServeVer).Should(Equal("1.3"))
Expect(config.StartVerifyUpstream).Should(BeFalse())
Expect(GetConfig()).Should(Not(BeNil()))
}
func writeConfigYml(tmpDir *helpertest.TmpFolder) *helpertest.TmpFile {
return tmpDir.CreateStringFile("config.yml",
"upstream:",
" default:",
" - tcp+udp:8.8.8.8",
" - tcp+udp:8.8.4.4",
" - 1.1.1.1",
"customDNS:",
" mapping:",
" my.duckdns.org: 192.168.178.3",
" multiple.ips: 192.168.178.3,192.168.178.4,2001:0db8:85a3:08d3:1319:8a2e:0370:7344",
"conditional:",
" mapping:",
" fritz.box: tcp+udp:192.168.178.1",
" multiple.resolvers: tcp+udp:192.168.178.1,tcp+udp:192.168.178.2",
"filtering:",
" queryTypes:",
" - AAAA",
" - A",
"blocking:",
" blackLists:",
" ads:",
" - https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt",
" - https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts",
" - https://mirror1.malwaredomains.com/files/justdomains",
" - http://sysctl.org/cameleon/hosts",
" - https://zeustracker.abuse.ch/blocklist.php?download=domainblocklist",
" - https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt",
" special:",
" - https://hosts-file.net/ad_servers.txt",
" whiteLists:",
" ads:",
" - whitelist.txt",
" clientGroupsBlock:",
" default:",
" - ads",
" - special",
" Laptop-D.fritz.box:",
" - ads",
" blockTTL: 1m",
" refreshPeriod: 120",
"clientLookup:",
" upstream: 192.168.178.1",
" singleNameOrder:",
" - 2",
" - 1",
"queryLog:",
" type: csv-client",
" target: /opt/log",
"port: 55553,:55554,[::1]:55555",
"logLevel: debug",
"dohUserAgent: testBlocky",
"minTlsServeVersion: 1.3",
"startVerifyUpstream: false")
}
func writeConfigDir(tmpDir *helpertest.TmpFolder) error {
f1 := tmpDir.CreateStringFile("config1.yaml",
"upstream:",
" default:",
" - tcp+udp:8.8.8.8",
" - tcp+udp:8.8.4.4",
" - 1.1.1.1",
"customDNS:",
" mapping:",
" my.duckdns.org: 192.168.178.3",
" multiple.ips: 192.168.178.3,192.168.178.4,2001:0db8:85a3:08d3:1319:8a2e:0370:7344",
"conditional:",
" mapping:",
" fritz.box: tcp+udp:192.168.178.1",
" multiple.resolvers: tcp+udp:192.168.178.1,tcp+udp:192.168.178.2",
"filtering:",
" queryTypes:",
" - AAAA",
" - A")
if f1.Error != nil {
return f1.Error
}
f2 := tmpDir.CreateStringFile("config2.yaml",
"blocking:",
" blackLists:",
" ads:",
" - https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt",
" - https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts",
" - https://mirror1.malwaredomains.com/files/justdomains",
" - http://sysctl.org/cameleon/hosts",
" - https://zeustracker.abuse.ch/blocklist.php?download=domainblocklist",
" - https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt",
" special:",
" - https://hosts-file.net/ad_servers.txt",
" whiteLists:",
" ads:",
" - whitelist.txt",
" clientGroupsBlock:",
" default:",
" - ads",
" - special",
" Laptop-D.fritz.box:",
" - ads",
" blockTTL: 1m",
" refreshPeriod: 120",
"clientLookup:",
" upstream: 192.168.178.1",
" singleNameOrder:",
" - 2",
" - 1",
"queryLog:",
" type: csv-client",
" target: /opt/log",
"port: 55553,:55554,[::1]:55555",
"logLevel: debug",
"dohUserAgent: testBlocky",
"minTlsServeVersion: 1.3",
"startVerifyUpstream: false")
return f2.Error
}

View File

@ -0,0 +1,882 @@
{
"__inputs": [
{
"name": "DS_POSTGRES",
"label": "Postgres",
"description": "",
"type": "datasource",
"pluginId": "postgres",
"pluginName": "Postgres"
}
],
"__requires": [
{
"type": "panel",
"id": "barchart",
"name": "Bar chart",
"version": ""
},
{
"type": "panel",
"id": "bargauge",
"name": "Bar gauge",
"version": ""
},
{
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "8.1.2"
},
{
"type": "datasource",
"id": "postgres",
"name": "Postgres",
"version": "1.0.0"
},
{
"type": "panel",
"id": "piechart",
"name": "Pie chart",
"version": ""
},
{
"type": "panel",
"id": "table",
"name": "Table",
"version": ""
},
{
"type": "panel",
"id": "timeseries",
"name": "Time series",
"version": ""
}
],
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": null,
"iteration": 1631130053746,
"links": [],
"panels": [
{
"cacheTimeout": null,
"datasource": "${DS_POSTGRES}",
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
}
},
"displayName": "${__field.labels.response_type}",
"mappings": [],
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 6,
"x": 0,
"y": 0
},
"id": 14,
"interval": null,
"links": [],
"options": {
"displayLabels": [],
"legend": {
"displayMode": "table",
"placement": "right",
"values": [
"value"
]
},
"pieType": "pie",
"reduceOptions": {
"calcs": [
"sum"
],
"fields": "",
"values": false
},
"tooltip": {
"mode": "single"
}
},
"pluginVersion": "8.1.2",
"repeatDirection": "v",
"targets": [
{
"format": "time_series",
"group": [],
"metricColumn": "none",
"rawQuery": true,
"rawSql": "SELECT t.response_type, max(t.request_Ts) as time, count(*) as cnt from log_entries t \n WHERE $__timeFilter(t.request_Ts) and \n t.response_type in ($response_type) and \n t.client_name in ($client_name) and \n (length('$question') = 0 or POSITION(lower('$question') IN t.question_name) > 0)\n group by t.response_type\n order by time",
"refId": "A",
"select": [
[
{
"params": [
"value"
],
"type": "column"
}
]
],
"timeColumn": "time",
"where": [
{
"name": "$__timeFilter",
"params": [],
"type": "macro"
}
]
}
],
"timeFrom": null,
"timeShift": null,
"title": "Query count by response type",
"transformations": [],
"type": "piechart"
},
{
"datasource": "${DS_POSTGRES}",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
}
},
"mappings": []
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 6,
"x": 6,
"y": 0
},
"id": 16,
"options": {
"displayLabels": [],
"legend": {
"displayMode": "table",
"placement": "right",
"values": [
"value"
]
},
"pieType": "pie",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"format": "time_series",
"group": [],
"metricColumn": "none",
"rawQuery": true,
"rawSql": "SELECT max(t.request_ts) AS time,\n case when t.reason like 'BLOCKED%' then SPLIT_PART(SPLIT_PART(t.reason,'(',-1), ')',1) else '' end AS metric,\n count(t.reason) AS cnt\nFROM log_entries t\nWHERE t.response_type ='BLOCKED'\n AND $__timeFilter(t.request_Ts)\n AND t.client_name in ($client_name)\n AND (length('$question') = 0 or POSITION(lower('$question') IN t.question_name) > 0)\nGROUP BY 2\nORDER BY time",
"refId": "A",
"select": [
[
{
"params": [
"duration_ms"
],
"type": "column"
}
]
],
"table": "log_entries",
"timeColumn": "request_ts",
"timeColumnType": "timestamp",
"where": [
{
"name": "$__timeFilter",
"params": [],
"type": "macro"
}
]
}
],
"title": "Blocked by Blacklist",
"type": "piechart"
},
{
"cacheTimeout": null,
"datasource": "${DS_POSTGRES}",
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 13,
"interval": null,
"links": [],
"options": {
"displayMode": "gradient",
"orientation": "horizontal",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": true
},
"showUnfilled": true,
"text": {}
},
"pluginVersion": "8.1.2",
"repeatDirection": "v",
"targets": [
{
"format": "table",
"group": [],
"metricColumn": "none",
"rawQuery": true,
"rawSql": "SELECT max(t.request_Ts) as time, t.client_name as metric, count(*) as cnt from log_entries t \n WHERE $__timeFilter(t.request_Ts) and \n t.response_type in ($response_type) and \n t.client_name in ($client_name) and \n (length('$question') = 0 or POSITION(lower('$question') IN t.question_name) > 0)\n group by t.client_name\n order by 3 desc",
"refId": "A",
"select": [
[
{
"params": [
"value"
],
"type": "column"
}
]
],
"timeColumn": "time",
"where": [
{
"name": "$__timeFilter",
"params": [],
"type": "macro"
}
]
}
],
"timeFrom": null,
"timeShift": null,
"title": "Query count by client",
"transformations": [],
"type": "bargauge"
},
{
"datasource": "${DS_POSTGRES}",
"description": "Top 20 effective top level domain plus one more label",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"axisSoftMin": 0,
"fillOpacity": 67,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineWidth": 2
},
"displayName": "count",
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 8
},
"id": 11,
"options": {
"barWidth": 0.26,
"groupWidth": 0.7,
"legend": {
"calcs": [],
"displayMode": "hidden",
"placement": "bottom"
},
"orientation": "horizontal",
"showValue": "never",
"stacking": "none",
"text": {
"valueSize": 10
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"format": "table",
"group": [],
"hide": false,
"metricColumn": "question_name",
"rawQuery": true,
"rawSql": "SELECT t.effective_tldp as metric, count(*) as value from log_entries t \nWHERE $__timeFilter(t.request_Ts) \n and t.response_type in ($response_type) \n and t.client_name in ($client_name) \n and (length('$question') = 0 or POSITION(lower('$question') IN t.question_name) > 0) \n group by t.effective_tldp order by count(*) desc limit 20",
"refId": "A",
"select": [
[
{
"params": [
"value"
],
"type": "column"
}
]
],
"table": "log_entries",
"timeColumn": "request_ts",
"where": []
}
],
"title": "Top 20 effective TLD+1",
"type": "barchart"
},
{
"datasource": "${DS_POSTGRES}",
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"axisSoftMin": 0,
"fillOpacity": 67,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineWidth": 2
},
"displayName": "count",
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 8
},
"id": 8,
"options": {
"barWidth": 0.26,
"groupWidth": 0.7,
"legend": {
"calcs": [],
"displayMode": "hidden",
"placement": "bottom"
},
"orientation": "horizontal",
"showValue": "never",
"stacking": "none",
"text": {
"valueSize": 10
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"format": "table",
"group": [],
"hide": false,
"metricColumn": "question_name",
"rawQuery": true,
"rawSql": "SELECT t.question_name as metric, count(*) as value from log_entries t \n WHERE $__timeFilter(t.request_Ts) and \n t.response_type in ($response_type) and \n t.client_name in ($client_name) and \n (length('$question') = 0 or POSITION(lower('$question') IN t.question_name) > 0) \n group by t.question_name order by count(*) desc limit 20",
"refId": "A",
"select": [
[
{
"params": [
"value"
],
"type": "column"
}
]
],
"table": "log_entries",
"timeColumn": "request_ts",
"where": []
}
],
"title": "Top 20 queried domains",
"type": "barchart"
},
{
"datasource": "${DS_POSTGRES}",
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "queries count",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "bars",
"fillOpacity": 35,
"gradientMode": "hue",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineStyle": {
"fill": "solid"
},
"lineWidth": 1,
"pointSize": 12,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": 3600000,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"displayName": "${__field.labels.client_name}",
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 24,
"x": 0,
"y": 16
},
"id": 12,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "right"
},
"tooltip": {
"mode": "single"
}
},
"pluginVersion": "8.1.2",
"targets": [
{
"format": "time_series",
"group": [],
"metricColumn": "none",
"rawQuery": true,
"rawSql": "SELECT\n $__timeGroupAlias(t.request_Ts, '30m'),\n t.client_name,\n count(*) as c\nFROM log_entries t\nWHERE\n $__timeFilter(t.request_Ts) and \n t.response_type in ($response_type) and \n t.client_name in ($client_name) and \n (length('$question') = 0 or POSITION(lower('$question') IN t.question_name) > 0)\nGROUP BY 1,2\nORDER BY 1",
"refId": "A",
"select": [
[
{
"params": [
"duration_ms"
],
"type": "column"
}
]
],
"table": "log_entries",
"timeColumn": "request_ts",
"timeColumnType": "timestamp",
"where": [
{
"name": "$__timeFilter",
"params": [],
"type": "macro"
}
]
}
],
"title": "Queries number per client (30m)",
"type": "timeseries"
},
{
"datasource": "${DS_POSTGRES}",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": -1,
"drawStyle": "bars",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "stepBefore",
"lineStyle": {
"fill": "solid"
},
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "line"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "dtdurationms"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 23
},
"id": 10,
"options": {
"legend": {
"calcs": [],
"displayMode": "hidden",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"format": "time_series",
"group": [],
"metricColumn": "none",
"rawQuery": true,
"rawSql": "SELECT\n EXTRACT(EPOCH from t.request_Ts) as time,\n t.duration_ms\nFROM log_entries t\nWHERE\n $__timeFilter(t.request_Ts) and \n t.response_type in ($response_type) and \n t.client_name in ($client_name) and \n (length('$question') = 0 or POSITION(lower('$question') IN t.question_name) > 0)\nORDER BY request_ts",
"refId": "A",
"select": [
[
{
"params": [
"duration_ms"
],
"type": "column"
}
]
],
"table": "log_entries",
"timeColumn": "request_ts",
"timeColumnType": "timestamp",
"where": [
{
"name": "$__timeFilter",
"params": [],
"type": "macro"
}
]
}
],
"title": "Query duration",
"type": "timeseries"
},
{
"datasource": "${DS_POSTGRES}",
"description": "Last 100 queries, newest on top",
"fieldConfig": {
"defaults": {
"custom": {
"align": null,
"displayMode": "auto",
"filterable": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "time"
},
"properties": [
{
"id": "unit",
"value": "dateTimeAsIsoNoDateIfToday"
}
]
}
]
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 31
},
"id": 4,
"options": {
"showHeader": true
},
"pluginVersion": "8.1.2",
"targets": [
{
"format": "table",
"group": [],
"metricColumn": "none",
"rawQuery": true,
"rawSql": "SELECT EXTRACT(EPOCH from t.request_Ts) as \"time\", \n t.client_ip as \"client IP\", \n t.client_name as \"client name\", \n t.duration_ms as \"duration in ms\", \n t.response_type as \"response type\", \n t.question_type as \"question type\", \n t.question_name as \"question name\", \n t.effective_tldp as \"effective TLD+1\", \n t.answer as \"answer\" from log_entries t \n WHERE $__timeFilter(t.request_Ts) and \n t.response_type in ($response_type) and \n t.client_name in ($client_name) and \n (length('$question') = 0 or POSITION(lower('$question') IN t.question_name) > 0) \n order by t.request_Ts desc limit 100",
"refId": "A",
"select": [
[
{
"params": [
"value"
],
"type": "column"
}
]
],
"timeColumn": "time",
"where": [
{
"name": "$__timeFilter",
"params": [],
"type": "macro"
}
]
}
],
"timeFrom": null,
"timeShift": null,
"title": "Last queries",
"type": "table"
}
],
"refresh": "",
"schemaVersion": 30,
"style": "dark",
"tags": [],
"templating": {
"list": [
{
"allValue": "",
"current": {},
"datasource": "${DS_POSTGRES}",
"definition": "select distinct client_name from log_entries",
"description": null,
"error": null,
"hide": 0,
"includeAll": true,
"label": "Client name",
"multi": true,
"name": "client_name",
"options": [],
"query": "select distinct client_name from log_entries",
"refresh": 2,
"regex": "",
"skipUrlSync": false,
"sort": 1,
"tagValuesQuery": "",
"tagsQuery": "",
"type": "query",
"useTags": false
},
{
"allValue": null,
"current": {},
"datasource": "${DS_POSTGRES}",
"definition": "select distinct response_type from log_entries",
"description": null,
"error": null,
"hide": 0,
"includeAll": true,
"label": "Response type",
"multi": true,
"name": "response_type",
"options": [],
"query": "select distinct response_type from log_entries",
"refresh": 2,
"regex": "",
"skipUrlSync": false,
"sort": 1,
"tagValuesQuery": "",
"tagsQuery": "",
"type": "query",
"useTags": false
},
{
"current": {
"selected": false,
"text": "",
"value": ""
},
"description": null,
"error": null,
"hide": 0,
"label": "Domain (contains)",
"name": "question",
"options": [
{
"selected": true,
"text": "",
"value": ""
}
],
"query": "",
"skipUrlSync": false,
"type": "textbox"
}
]
},
"time": {
"from": "now-24h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Blocky query",
"uid": "AVmWSVWgz",
"version": 3
}

View File

@ -19,6 +19,14 @@ upstream:
# optional: timeout to query the upstream resolver. Default: 2s
upstreamTimeout: 2s
# optional: If true, blocky will fail to start unless at least one upstream server per group is reachable. Default: false
startVerifyUpstream: true
# optional: Determines how blocky will create outgoing connections. This impacts both upstreams, and lists.
# accepted: dual, v4, v6
# default: dual
connectIPVersion: dual
# optional: custom IP address(es) for domain name (with all sub-domains). Multiple addresses must be separated by a comma
# example: query "printer.lan" or "my.printer.lan" will return 192.168.178.3
customDNS:
@ -35,6 +43,10 @@ customDNS:
# optional: definition, which DNS resolver(s) should be used for queries to the domain (with all sub-domains). Multiple resolvers must be separated by a comma
# Example: Query client.fritz.box will ask DNS server 192.168.178.1. This is necessary for local network, to resolve clients by host name
conditional:
# optional: if false (default), return empty result if after rewrite, the mapped resolver returned an empty answer. If true, the original query will be sent to the upstream resolver
# Example: The query "blog.example.com" will be rewritten to "blog.fritz.box" and also redirected to the resolver at 192.168.178.1. If not found and if `fallbackUpstream` was set to `true`, the original query "blog.example.com" will be sent upstream.
# Usage: One usecase when having split DNS for internal and external (internet facing) users, but not all subdomains are listed in the internal domain.
fallbackUpstream: false
# optional: replace domain in the query with other domain before resolver lookup in the mapping
rewrite:
example.com: fritz.box
@ -97,8 +109,8 @@ blocking:
downloadAttempts: 5
# optional: Time between the download attempts. Default: 1s
downloadCooldown: 10s
# optional: if true, application startup will fail if at least one list can't be downloaded / opened. Default: false
failStartOnListError: false
# optional: if failOnError, application startup will fail if at least one list can't be downloaded / opened. Default: blocking
startStrategy: failOnError
# optional: configuration for caching of DNS responses
caching:
@ -128,6 +140,9 @@ caching:
# Max number of domains to be kept in cache for prefetching (soft limit). Useful on systems with limited amount of RAM.
# Default (0): unlimited
prefetchMaxItemsCount: 0
# Time how long negative results (NXDOMAIN response or empty result) are cached. A value of -1 will disable caching for negative results.
# Default: 30m
cacheTimeNegative: 30m
# optional: configuration of client name resolution
clientLookup:
@ -206,6 +221,8 @@ hostsFile:
hostsTTL: 60m
# optional: Time between hosts file refresh, default: 1h
refreshPeriod: 30m
# optional: Whether loopback hosts addresses (127.0.0.0/8 and ::1) should be filtered or not, default: false
filterLoopback: true
# optional: Log level (one from debug, info, warn, error). Default: info
logLevel: info
# optional: Log format (text or json). Default: text
@ -214,3 +231,8 @@ logFormat: text
logTimestamp: true
# optional: obfuscate log output (replace all alphanumeric characters with *) for user sensitive data like request domains or responses to increase privacy. Default: false
logPrivacy: false
# optional: add EDE error codes to dns response
ede:
# enabled if true, Default: false
enable: true

View File

@ -25,6 +25,8 @@ configuration properties as [JSON](config.yml).
| logPrivacy | bool | no | false | Obfuscate log output (replace all alphanumeric characters with *) for user sensitive data like request domains or responses to increase privacy. |
| dohUserAgent | string | no | | HTTP User Agent for DoH upstreams |
| minTlsServeVersion | string | no | 1.2 | Minimum TLS version that the DoT and DoH server use to serve those encrypted DNS requests |
| startVerifyUpstream | bool | no | false | If true, blocky will fail to start unless at least one upstream server per group is reachable. |
| connectIPVersion | bool | no | dual | IP version to use for outgoing connections (dual, v4, v6) |
!!! example
@ -50,13 +52,16 @@ following network protocols (net part of the resolver URL):
returns the answer from the fastest one. This improves your network speed and increases your privacy - your DNS traffic
will be distributed over multiple providers.
Each resolver must be defined as a string in following format: `[net:]host:[port][/path]`.
Each resolver must be defined as a string in following format: `[net:]host:[port][/path][#commonName]`.
| Parameter | Type | Mandatory | Default value |
|-----------|----------------------------------|-----------|---------------------------------------------------|
| net | enum (tcp+udp, tcp-tls or https) | no | tcp+udp |
| host | IP or hostname | yes | |
| port | int (1 - 65535) | no | 53 for udp/tcp, 853 for tcp-tls and 443 for https |
| net | enum (tcp+udp, tcp-tls or https) | no | tcp+udp |
| host | IP or hostname | yes | |
| port | int (1 - 65535) | no | 53 for udp/tcp, 853 for tcp-tls and 443 for https |
| commonName | string | no | the host value |
The commonName parameter overrides the expected certificate common name value used for verification.
Blocky needs at least the configuration of the **default** group. This group will be used as a fallback, if no client
specific resolver configuration is available.
@ -134,7 +139,6 @@ Works only on Linux/\*nix OS due to golang limitations under Windows.
- 123.123.123.123
```
## Filtering
Under certain circumstances, it may be useful to filter some types of DNS queries. You can define one or more DNS query
@ -150,6 +154,18 @@ types, all queries with these types will be dropped (empty answer will be return
This configuration will drop all 'AAAA' (IPv6) queries.
## FQDN only
In domain environments, it may be usefull to only response to FQDN requests. If this option is enabled blocky respond immidiatly
with NXDOMAIN if the request is not a valid FQDN. The request is therfore not further processed by other options like custom or conditional.
Please be aware that by enabling it your hostname resolution will break unless every hostname is part of a domain.
!!! example
```yaml
fqdnOnly: true
```
## 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
@ -186,7 +202,7 @@ The query "printer.home" will be rewritten to "printer.lan" and return 192.168.1
With parameter `filterUnmappedTypes = true` (default), blocky will filter all queries with unmapped types, for example:
AAAA for "printer.lan" or TXT for "otherdevice.lan".
With `filterUnmappedTypes = true` a query AAAA "printer.lan" will be forwarded to the upstream DNS server.
With `filterUnmappedTypes = false` a query AAAA "printer.lan" will be forwarded to the upstream DNS server.
## Conditional DNS resolution
@ -196,10 +212,15 @@ hostname belongs to which IP address, all DNS queries for the local network shou
The optional parameter `rewrite` behaves the same as with custom DNS.
The optional parameter fallbackUpstream, if false (default), return empty result if after rewrite, the mapped resolver returned an empty answer. If true, the original query will be sent to the upstream resolver.
### Usage: One usecase when having split DNS for internal and external (internet facing) users, but not all subdomains are listed in the internal domain
!!! example
```yaml
conditional:
fallbackUpstream: false
rewrite:
example.com: fritz.box
replace-me.com: with-this.com
@ -217,9 +238,13 @@ The optional parameter `rewrite` behaves the same as with custom DNS.
You can use `.` as wildcard for all non full qualified domains (domains without dot)
In this example, a DNS query "client.fritz.box" will be redirected to the router's DNS server at 192.168.178.1 and client.lan.net to 192.170.1.2 and 192.170.1.3.
The query "client.example.com" will be rewritten to "client.fritz.box" and also redirected to the resolver at 192.168.178.1. All unqualified hostnames (e.g. "test")
will be redirected to the DNS server at 168.168.0.1
The query "client.example.com" will be rewritten to "client.fritz.box" and also redirected to the resolver at 192.168.178.1.
If not found and if `fallbackUpstream` was set to `true`, the original query "blog.example.com" will be sent upstream.
All unqualified hostnames (e.g. "test") will be redirected to the DNS server at 168.168.0.1.
One usecase for `fallbackUpstream` is when having split DNS for internal and external (internet facing) users, but not all subdomains are listed in the internal domain.
## Client name lookup
@ -436,16 +461,22 @@ You can configure the list download attempts according to your internet connecti
downloadCooldown: 10s
```
### Fail on start
### Start strategy
You can ensure with parameter `failStartOnListError = true` that the application will fail if at least one list can't be
downloaded or opened. Default value is `false`.
You can configure the blocking behavior during application start of blocky.
If no starategy is selected blocking will be used.
| startStrategy | Description |
|---------------|-------------------------------------------------------------------------------------------------------|
| blocking | all blocking lists will be loaded before DNS resoulution starts |
| failOnError | like blocking but blocky shutsdown if an download fails |
| fast | DNS resolution starts immediately without blocking which will be enabled after list load is completed |
!!! example
```yaml
blocking:
failStartOnListError: false
startStrategy: failOnError
```
### Concurrency
@ -454,7 +485,7 @@ Blocky downloads and processes links in a single group concurrently. With parame
how many links can be processed in the same time. Higher value can reduce the overall list refresh time, but more parallel
download and processing jobs need more RAM. Please consider to reduce this value on systems with limited memory. Default value is 4.
!!! example
!!! example
```yaml
blocking:
@ -483,7 +514,7 @@ With following parameters you can tune the caching behavior:
| caching.prefetchExpires | duration format | no | 2h | Prefetch track time window |
| caching.prefetchThreshold | int | no | 5 | Name queries threshold for prefetch |
| caching.prefetchMaxItemsCount | int | no | 0 (unlimited) | Max number of domains to be kept in cache for prefetching (soft limit). Default (0): unlimited. Useful on systems with limited amount of RAM. |
| caching.cacheTimeNegative | duration format | no | 30m | Time how long negative results are cached. A value of -1 will disable caching for negative results. |
| caching.cacheTimeNegative | duration format | no | 30m | Time how long negative results (NXDOMAIN response or empty result) are cached. A value of -1 will disable caching for negative results. |
!!! example
@ -593,17 +624,18 @@ example for Database
logRetentionDays: 7
```
### Hosts file
## Hosts file
You can enable resolving of entries, located in local hosts file.
Configuration parameters:
| Parameter | Type | Mandatory | Default value | Description |
|--------------------------|--------------------------------|-----------|---------------|-----------------------------------------------|
| hostsFile.filePath | string | no | | Path to hosts file (e.g. /etc/hosts on Linux) |
| hostsFile.hostsTTL | duration (no units is minutes) | no | 1h | TTL |
| hostsFile.refreshPeriod | duration format | no | 1h | Time between hosts file refresh |
| Parameter | Type | Mandatory | Default value | Description |
|--------------------------|--------------------------------|-----------|---------------|--------------------------------------------------|
| hostsFile.filePath | string | no | | Path to hosts file (e.g. /etc/hosts on Linux) |
| hostsFile.hostsTTL | duration (no units is minutes) | no | 1h | TTL |
| hostsFile.refreshPeriod | duration format | no | 1h | Time between hosts file refresh |
| hostsFile.filterLoopback | bool | no | false | Filter loopback addresses (127.0.0.0/8 and ::1) |
!!! example
@ -614,6 +646,23 @@ Configuration parameters:
refreshPeriod: 30m
```
### Deliver EDE codes as EDNS0 option
DNS responses can be extended with EDE codes according to [RFC8914](https://datatracker.ietf.org/doc/rfc8914/).
Configuration parameters:
| Parameter | Type | Mandatory | Default value | Description |
|--------------------------|--------------------------------|-----------|---------------|----------------------------------------------------|
| ede.enable | bool | no | false | If true, DNS responses are deliverd with EDE codes |
!!! example
```yaml
ede:
enable: true
```
## SSL certificate configuration (DoH / TLS listener)
See [Wiki - Configuration of HTTPS](https://github.com/0xERR0R/blocky/wiki/Configuration-of-HTTPS-for-DoH-and-Rest-API)

View File

@ -52,4 +52,8 @@ or [at grafana.com](https://grafana.com/grafana/dashboards/14980)
Please define the MySQL source in Grafana, which points to the database with blocky's log entries.
## Postgres
The JSON for a Grafana dashboard equivalent to the MySQL/MariaDB version is located [here](https://github.com/0xERR0R/blocky/blob/master/docs/blocky-query-grafana-postgres.json)
--8<-- "docs/includes/abbreviations.md"

116
go.mod
View File

@ -1,90 +1,112 @@
module github.com/0xERR0R/blocky
go 1.18
go 1.19
require (
github.com/alicebob/miniredis/v2 v2.21.0
github.com/abice/go-enum v0.5.3
github.com/alicebob/miniredis/v2 v2.23.1
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef
github.com/avast/retry-go/v4 v4.3.0
github.com/creasty/defaults v1.6.0
github.com/go-chi/chi/v5 v5.0.7
github.com/go-chi/cors v1.2.1
github.com/go-redis/redis/v8 v8.11.5
github.com/google/uuid v1.3.0
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
github.com/hashicorp/go-multierror v1.1.1
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/hashicorp/golang-lru v0.5.4
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/miekg/dns v1.1.49
github.com/miekg/dns v1.1.50
github.com/mroth/weightedrand v0.4.1
github.com/onsi/gomega v1.19.0
github.com/prometheus/client_golang v1.12.2
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.4.0
github.com/stretchr/testify v1.7.2
github.com/onsi/ginkgo/v2 v2.5.0
github.com/onsi/gomega v1.24.1
github.com/prometheus/client_golang v1.14.0
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.6.1
github.com/stretchr/testify v1.8.1
github.com/swaggo/swag v1.8.7
github.com/x-cray/logrus-prefixed-formatter v0.5.2
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4
golang.org/x/net v0.2.0
gopkg.in/yaml.v2 v2.4.0
gorm.io/driver/mysql v1.3.4
gorm.io/driver/sqlite v1.3.2
gorm.io/gorm v1.23.5
gorm.io/driver/mysql v1.4.3
gorm.io/driver/postgres v1.4.5
gorm.io/driver/sqlite v1.4.3
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/avast/retry-go/v4 v4.0.5
github.com/go-chi/chi/v5 v5.0.7
github.com/hashicorp/golang-lru v0.5.4
github.com/onsi/ginkgo/v2 v2.1.4
github.com/swaggo/swag v1.8.2
gorm.io/driver/postgres v1.3.7
github.com/go-logr/logr v1.2.3 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/lib/pq v1.10.6 // indirect
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde // indirect
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
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.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/spec v0.20.7 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.12.1 // indirect
github.com/jackc/pgconn v1.13.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.1 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgtype v1.11.0 // indirect
github.com/jackc/pgx/v4 v4.16.1 // indirect
github.com/jackc/pgtype v1.12.0 // indirect
github.com/jackc/pgx/v4 v4.17.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-sqlite3 v1.14.12 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
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/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/ramr/go-reaper v0.2.1
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.3.0 // indirect
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 // indirect
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b // indirect
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.10 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/urfave/cli/v2 v2.23.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 // indirect
golang.org/x/crypto v0.1.0 // indirect
golang.org/x/mod v0.6.0 // indirect
golang.org/x/sys v0.2.0 // indirect
golang.org/x/term v0.2.0 // indirect
golang.org/x/text v0.4.0 // indirect
golang.org/x/tools v0.2.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

259
go.sum
View File

@ -35,30 +35,34 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/abice/go-enum v0.5.3 h1:Ghq0aWp+tCNZFAb4lFK7UnjzUJQTS1atIMjHkX+Gex4=
github.com/abice/go-enum v0.5.3/go.mod h1:jf915DI7NxXZRwk8qDgZJKq2raAtwcPBXJRh9WVgtU0=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.21.0 h1:CdmwIlKUWFBDS+4464GtQiQ0R1vpzOgu4Vnd74rBL7M=
github.com/alicebob/miniredis/v2 v2.21.0/go.mod h1:XNqvJdQJv5mSuVMc0ynneafpnL/zv52acZ6kqeS0t88=
github.com/alicebob/miniredis/v2 v2.23.1 h1:jR6wZggBxwWygeXcdNyguCOCIjPsZyNUNlAkTx2fu0U=
github.com/alicebob/miniredis/v2 v2.23.1/go.mod h1:84TWKZlxYkfgMucPBf5SOQBYJceZeQRFIaQgNMiCX6Q=
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef h1:2JGTg6JapxP9/R33ZaagQtAM4EkkSYnIAlOG5EI8gkM=
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef/go.mod h1:JS7hed4L1fj0hXcyEejnW57/7LCetXggd+vwrRnYeII=
github.com/avast/retry-go/v4 v4.0.5 h1:C0Fm9MjPCmgLW6Jb1zBTVRx0ycr+VUaaUZO5wpqYjqg=
github.com/avast/retry-go/v4 v4.0.5/go.mod h1:HqmLvS2VLdStPCGDFjSuZ9pzlTqVRldCI4w2dO4m1Ms=
github.com/avast/retry-go/v4 v4.3.0 h1:cqI48aXx0BExKoM7XPklDpoHAg7/srPPLAfWG5z62jo=
github.com/avast/retry-go/v4 v4.3.0/go.mod h1:bqOlT4nxk4phk9buiQFaghzjpqdchOSwPgjdfdQBtdg=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bradleyjkemp/cupaloy v2.3.0+incompatible h1:UafIjBvWQmS9i/xRg+CamMrnLTKNzo+bdmT/oH34c2Y=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
@ -72,7 +76,8 @@ github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creasty/defaults v1.6.0 h1:ltuE9cfphUtlrBeomuu8PEyISTXnxqkBIoQfXgv7BSc=
@ -82,11 +87,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dosgo/zigtool v0.0.0-20210923085854-9c6fc1d62198 h1:3b37D/Oxs95GmDsGKNx21aBYWF270emHjqUExsAL01g=
github.com/dosgo/zigtool v0.0.0-20210923085854-9c6fc1d62198/go.mod h1:NUrh34aXXgbs4C2HkTmRmkzsKhtrFPRitYkbZMDDONo=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
@ -97,24 +107,31 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
github.com/go-openapi/spec v0.20.7 h1:1Rlu/ZrOCCob0n+JKKJAWhNWMPW8bOZRg8FJaY+0SKI=
github.com/go-openapi/spec v0.20.7/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@ -129,6 +146,8 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -156,8 +175,9 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@ -168,6 +188,8 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@ -184,9 +206,14 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
@ -197,8 +224,8 @@ github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsU
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.12.1 h1:rsDFzIpRk7xT4B8FufgpCCeyjdNpKyghZeSefViE5W8=
github.com/jackc/pgconn v1.12.1/go.mod h1:ZkhRC59Llhrq3oSfrikvwQ5NaxYExr6twkdkMLaKono=
github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys=
github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
@ -214,26 +241,26 @@ github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvW
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.3.0 h1:brH0pCGBDkBW07HWlN/oSBXrmo3WB0UvZd1pIuDcL8Y=
github.com/jackc/pgproto3/v2 v2.3.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y=
github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
github.com/jackc/pgtype v1.11.0 h1:u4uiGPz/1hryuXzyaBhSk6dnIyyG2683olG2OV+UUgs=
github.com/jackc/pgtype v1.11.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w=
github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.16.1 h1:JzTglcal01DrghUqt+PmzWsZx/Yh7SC/CTQmSBMTd0Y=
github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ=
github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E=
github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
@ -256,43 +283,50 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxv
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/goveralls v0.0.11 h1:eJXea6R6IFlL1QMKNMzDvvHv/hwGrnvyig4N+0+XiMM=
github.com/mattn/goveralls v0.0.11/go.mod h1:gU8SyhNswsJKchEV93xRQxX6X3Ei4PJdQk/6ZHvrvRk=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/miekg/dns v1.1.49 h1:qe0mQU3Z/XpFeE+AEBo2rqaS1IPBJ3anmqZ4XiZJVG8=
github.com/miekg/dns v1.1.49/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
@ -305,11 +339,10 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo/v2 v2.1.4 h1:GNapqRSid3zijZ9H77KrgVG4/8KqiyRsxcSxe+7ApXY=
github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU=
github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
github.com/pierrre/gotestcover v0.0.0-20160517101806-924dca7d15f0/go.mod h1:4xpMLz7RBWyB+ElzHu8Llua96TRCB3YwX+l5EP1wmHk=
github.com/onsi/ginkgo/v2 v2.5.0 h1:TRtrvv2vdQqzkwrQ1ke6vtXf7IK34RBUJafIy1wMwls=
github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw=
github.com/onsi/gomega v1.24.1 h1:KORJXNNTzJXzu4ScJWssJfJMnJ+2QJqhoQSRwNlze9E=
github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@ -320,29 +353,36 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34=
github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s=
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/ramr/go-reaper v0.2.1 h1:zww+wlQOvTjBZuk1920R/e0GFEb6O7+B0WQLV6dM924=
github.com/ramr/go-reaper v0.2.1/go.mod h1:AVypdzrcCXjSc/JYnlXl8TsB+z84WyFzxWE8Jh0MOJc=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
@ -352,36 +392,44 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As=
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/swaggo/swag v1.8.2 h1:D4aBiVS2a65zhyk3WFqOUz7Rz0sOaUcgeErcid5uGL4=
github.com/swaggo/swag v1.8.2/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/swaggo/swag v1.8.7 h1:2K9ivTD3teEO+2fXV6zrZKDqk5IuU2aJtBDo8U7omWU=
github.com/swaggo/swag v1.8.7/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk=
github.com/urfave/cli/v2 v2.23.0 h1:pkly7gKIeYv3olPAeNajNpLjeJrmTPYCoZWaV+2VfvE=
github.com/urfave/cli/v2 v2.23.0/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 h1:k/gmLsJDWwWqbLCur2yWnJzwQEKRcAHXo6seXGuSwWw=
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA=
github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 h1:5mLPGnFdSsevFRFc9q3yYbBkB6tsm4aCwwQV/j1JQAQ=
github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
@ -410,9 +458,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b h1:QAqMVf3pSa6eeTsuklijukjXBlj7Es2QQplab+/RbQ4=
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -444,8 +492,8 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -477,18 +525,20 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -498,8 +548,9 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde h1:ejfdSekXMDxDLbRrJMwUk6KnSLZ2McaUCVcIKM+N6jc=
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -540,25 +591,25 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211107104306-e0b2ad06fe42/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 h1:OH54vjqzRWmbJ62fjuhxy7AxFFgoHN0/DPc/UrL8cAs=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -566,8 +617,9 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -616,16 +668,15 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
@ -702,15 +753,14 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
@ -723,17 +773,20 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.3.4 h1:/KoBMgsUHC3bExsekDcmNYaBnfH2WNeFuXqqrqMc98Q=
gorm.io/driver/mysql v1.3.4/go.mod h1:s4Tq0KmD0yhPGHbZEwg1VPlH0vT/GBHJZorPzhcxBUE=
gorm.io/driver/postgres v1.3.7 h1:FKF6sIMDHDEvvMF/XJvbnCl0nu6KSKUaPXevJ4r+VYQ=
gorm.io/driver/postgres v1.3.7/go.mod h1:f02ympjIcgtHEGFMZvdgTxODZ9snAHDb4hXfigBVuNI=
gorm.io/driver/sqlite v1.3.2 h1:nWTy4cE52K6nnMhv23wLmur9Y3qWbZvOBz+V4PrGAxg=
gorm.io/driver/sqlite v1.3.2/go.mod h1:B+8GyC9K7VgzJAcrcXMRPdnMcck+8FgJynEehEPM16U=
gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM=
gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/driver/mysql v1.4.3 h1:/JhWJhO2v17d8hjApTltKNADm7K7YI2ogkR7avJUL3k=
gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
gorm.io/driver/postgres v1.4.5 h1:mTeXTTtHAgnS9PgmhN2YeUbazYpLhUI1doLnw42XUZc=
gorm.io/driver/postgres v1.4.5/go.mod h1:GKNQYSJ14qvWkvPwXljMGehpKrhlDNsqYRr5HnYGncg=
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.24.1-0.20221019064659-5dd2bb482755 h1:7AdrbfcvKnzejfqP5g37fdSZOXH/JvaPIzBIHTOqXKk=
gorm.io/gorm v1.24.1-0.20221019064659-5dd2bb482755/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -3,7 +3,6 @@ package helpertest
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
@ -16,7 +15,7 @@ import (
// TempFile creates temp file with passed data
func TempFile(data string) *os.File {
f, err := ioutil.TempFile("", "prefix")
f, err := os.CreateTemp("", "prefix")
if err != nil {
log.Log().Fatal(err)
}
@ -40,7 +39,8 @@ func TestServer(data string) *httptest.Server {
}
// DoGetRequest performs a GET request
func DoGetRequest(url string, fn func(w http.ResponseWriter, r *http.Request)) (code int, body *bytes.Buffer) {
func DoGetRequest(url string,
fn func(w http.ResponseWriter, r *http.Request)) (*httptest.ResponseRecorder, *bytes.Buffer) {
r, _ := http.NewRequest("GET", url, nil)
rr := httptest.NewRecorder()
@ -48,7 +48,7 @@ func DoGetRequest(url string, fn func(w http.ResponseWriter, r *http.Request)) (
handler.ServeHTTP(rr, r)
return rr.Code, rr.Body
return rr, rr.Body
}
// BeDNSRecord returns new dns matcher

168
helpertest/tmpdata.go Normal file
View File

@ -0,0 +1,168 @@
package helpertest
import (
"bufio"
"io/fs"
"os"
"path/filepath"
)
type TmpFolder struct {
Path string
Error error
prefix string
}
type TmpFile struct {
Path string
Error error
Folder *TmpFolder
}
func NewTmpFolder(prefix string) *TmpFolder {
ipref := prefix
if len(ipref) == 0 {
ipref = "blocky"
}
path, err := os.MkdirTemp("", ipref)
res := &TmpFolder{
Path: path,
Error: err,
prefix: ipref,
}
return res
}
func (tf *TmpFolder) Clean() error {
if len(tf.Path) > 0 {
return os.RemoveAll(tf.Path)
}
return nil
}
func (tf *TmpFolder) CreateSubFolder(name string) *TmpFolder {
var path string
var err error
if len(name) > 0 {
path = filepath.Join(tf.Path, name)
err = os.Mkdir(path, fs.ModePerm)
} else {
path, err = os.MkdirTemp(tf.Path, tf.prefix)
}
res := &TmpFolder{
Path: path,
Error: err,
prefix: tf.prefix,
}
return res
}
func (tf *TmpFolder) CreateEmptyFile(name string) *TmpFile {
f, err := tf.createFile(name)
if err != nil {
return tf.newErrorTmpFile(err)
}
return tf.checkState(f, err)
}
func (tf *TmpFolder) CreateStringFile(name string, lines ...string) *TmpFile {
f, err := tf.createFile(name)
if err != nil {
return tf.newErrorTmpFile(err)
}
first := true
w := bufio.NewWriter(f)
for _, l := range lines {
if first {
first = false
} else {
_, err = w.WriteString("\n")
}
if err != nil {
break
}
_, err = w.WriteString(l)
if err != nil {
break
}
}
w.Flush()
return tf.checkState(f, err)
}
func (tf *TmpFolder) JoinPath(name string) string {
return filepath.Join(tf.Path, name)
}
func (tf *TmpFolder) CountFiles() (int, error) {
files, err := os.ReadDir(tf.Path)
if err != nil {
return 0, err
}
return len(files), nil
}
func (tf *TmpFolder) createFile(name string) (*os.File, error) {
if len(name) > 0 {
return os.Create(filepath.Join(tf.Path, name))
}
return os.CreateTemp(tf.Path, "temp")
}
func (tf *TmpFolder) newErrorTmpFile(err error) *TmpFile {
return &TmpFile{
Path: "",
Error: err,
Folder: tf,
}
}
func (tf *TmpFolder) checkState(file *os.File, ierr error) *TmpFile {
err := ierr
filepath := ""
if file != nil {
filepath = file.Name()
file.Close()
_, err = os.Stat(filepath)
}
return &TmpFile{
Path: filepath,
Error: err,
Folder: tf,
}
}
func (tf *TmpFile) Stat() error {
if tf.Error != nil {
return tf.Error
}
_, res := os.Stat(tf.Path)
return res
}

View File

@ -183,9 +183,10 @@ var _ = Describe("Downloader", func() {
It("Should perform a retry until max retry attempt count is reached and return TransientError", func() {
reader, err := sut.DownloadFile(server.URL)
Expect(err).Should(HaveOccurred())
var transientErr *TransientError
Expect(errors.As(err, &transientErr)).To(BeTrue())
Expect(transientErr.Unwrap().Error()).Should(ContainSubstring("Timeout"))
err2 := unwrapTransientErr(err)
Expect(err2.Error()).Should(ContainSubstring("Timeout"))
Expect(reader).Should(BeNil())
// failed download event was emitted 3 times
@ -196,15 +197,18 @@ var _ = Describe("Downloader", func() {
When("DNS resolution of passed URL fails", func() {
BeforeEach(func() {
sut = NewDownloader(
WithTimeout(100*time.Millisecond),
WithTimeout(500*time.Millisecond),
WithAttempts(3),
WithCooldown(time.Millisecond))
WithCooldown(200*time.Millisecond))
})
It("Should perform a retry until max retry attempt count is reached and return DNSError", func() {
reader, err := sut.DownloadFile("http://some.domain.which.does.not.exist")
Expect(err).Should(HaveOccurred())
err2 := unwrapTransientErr(err)
var dnsError *net.DNSError
Expect(errors.As(err, &dnsError)).To(BeTrue(), "received error %w", err)
Expect(errors.As(err2, &dnsError)).To(BeTrue(), "received error %w", err)
Expect(reader).Should(BeNil())
// failed download event was emitted 3 times
@ -215,3 +219,12 @@ var _ = Describe("Downloader", func() {
})
})
})
func unwrapTransientErr(origErr error) error {
var transientErr *TransientError
if errors.As(origErr, &transientErr) {
return transientErr.Unwrap()
}
return origErr
}

View File

@ -1,6 +1,6 @@
package lists
//go:generate go-enum -f=$GOFILE --marshal --names
//go:generate go run github.com/abice/go-enum -f=$GOFILE --marshal --names
import (
"bufio"
"errors"
@ -95,7 +95,7 @@ func (b *ListCache) Configuration() (result []string) {
// NewListCache creates new list instance
func NewListCache(t ListCacheType, groupToLinks map[string][]string, refreshPeriod time.Duration,
downloader FileDownloader, processingConcurrency uint) (*ListCache, error) {
downloader FileDownloader, processingConcurrency uint, async bool) (*ListCache, error) {
groupCaches := make(map[string]stringcache.StringCache)
if processingConcurrency == 0 {
@ -110,7 +110,13 @@ func NewListCache(t ListCacheType, groupToLinks map[string][]string, refreshPeri
listType: t,
processingConcurrency: processingConcurrency,
}
initError := b.refresh(true)
var initError error
if async {
initError = nil
} else {
initError = b.refresh(true)
}
if initError == nil {
go periodicUpdate(b)

View File

@ -12,7 +12,7 @@ func BenchmarkRefresh(b *testing.B) {
"gr1": {file1, file2, file3},
}
cache, _ := NewListCache(ListCacheTypeBlacklist, lists, -1, NewDownloader(), 5)
cache, _ := NewListCache(ListCacheTypeBlacklist, lists, -1, NewDownloader(), 5, false)
b.ReportAllocs()

View File

@ -20,6 +20,8 @@ const (
ListCacheTypeWhitelist
)
var ErrInvalidListCacheType = fmt.Errorf("not a valid ListCacheType, try [%s]", strings.Join(_ListCacheTypeNames, ", "))
const _ListCacheTypeName = "blacklistwhitelist"
var _ListCacheTypeNames = []string{
@ -57,7 +59,7 @@ func ParseListCacheType(name string) (ListCacheType, error) {
if x, ok := _ListCacheTypeValue[name]; ok {
return x, nil
}
return ListCacheType(0), fmt.Errorf("%s is not a valid ListCacheType, try [%s]", name, strings.Join(_ListCacheTypeNames, ", "))
return ListCacheType(0), fmt.Errorf("%s is %w", name, ErrInvalidListCacheType)
}
// MarshalText implements the text marshaller method.

View File

@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"math/rand"
"net/http/httptest"
@ -22,36 +21,39 @@ import (
var _ = Describe("ListCache", func() {
var (
emptyFile, file1, file2, file3 *os.File
tmpDir *TmpFolder
emptyFile, file1, file2, file3 *TmpFile
server1, server2, server3 *httptest.Server
)
BeforeEach(func() {
emptyFile = TempFile("#empty file\n\n")
server1 = TestServer("blocked1.com\nblocked1a.com\n192.168.178.55")
server2 = TestServer("blocked2.com")
server3 = TestServer("blocked3.com\nblocked1a.com")
tmpDir = NewTmpFolder("ListCache")
Expect(tmpDir.Error).Should(Succeed())
DeferCleanup(tmpDir.Clean)
file1 = TempFile("blocked1.com\nblocked1a.com")
file2 = TempFile("blocked2.com")
file3 = TempFile("blocked3.com\nblocked1a.com")
})
AfterEach(func() {
_ = os.Remove(emptyFile.Name())
_ = os.Remove(file1.Name())
_ = os.Remove(file2.Name())
_ = os.Remove(file3.Name())
server1.Close()
server2.Close()
server3.Close()
server1 = TestServer("blocked1.com\nblocked1a.com\n192.168.178.55")
DeferCleanup(server1.Close)
server2 = TestServer("blocked2.com")
DeferCleanup(server2.Close)
server3 = TestServer("blocked3.com\nblocked1a.com")
DeferCleanup(server3.Close)
emptyFile = tmpDir.CreateStringFile("empty", "#empty file")
Expect(emptyFile.Error).Should(Succeed())
file1 = tmpDir.CreateStringFile("file1", "blocked1.com", "blocked1a.com")
Expect(file1.Error).Should(Succeed())
file2 = tmpDir.CreateStringFile("file2", "blocked2.com")
Expect(file2.Error).Should(Succeed())
file3 = tmpDir.CreateStringFile("file3", "blocked3.com", "blocked1a.com")
Expect(file3.Error).Should(Succeed())
})
Describe("List cache and matching", func() {
When("Query with empty", func() {
It("should not panic", func() {
lists := map[string][]string{
"gr0": {emptyFile.Name()},
"gr0": {emptyFile.Path},
}
sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency)
sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency, false)
Expect(err).Should(Succeed())
found, group := sut.Match("", []string{"gr0"})
@ -63,9 +65,9 @@ var _ = Describe("ListCache", func() {
When("List is empty", func() {
It("should not match anything", func() {
lists := map[string][]string{
"gr1": {emptyFile.Name()},
"gr1": {emptyFile.Path},
}
sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency)
sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency, false)
Expect(err).Should(Succeed())
found, group := sut.Match("google.com", []string{"gr1"})
@ -79,12 +81,15 @@ var _ = Describe("ListCache", func() {
// should produce a transient error on second and third attempt
data := make(chan func() (io.ReadCloser, error), 3)
mockDownloader := &MockDownloader{data: data}
// nolint:unparam
data <- func() (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader("blocked1.com")), nil
}
// nolint:unparam
data <- func() (io.ReadCloser, error) {
return nil, &TransientError{inner: errors.New("boom")}
}
// nolint:unparam
data <- func() (io.ReadCloser, error) {
return nil, &TransientError{inner: errors.New("boom")}
}
@ -97,6 +102,7 @@ var _ = Describe("ListCache", func() {
4*time.Hour,
mockDownloader,
defaultProcessingConcurrency,
false,
)
Expect(err).Should(Succeed())
@ -131,6 +137,7 @@ var _ = Describe("ListCache", func() {
// should produce a 404 err on second attempt
data := make(chan func() (io.ReadCloser, error), 2)
mockDownloader := &MockDownloader{data: data}
//nolint:unparam
data <- func() (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader("blocked1.com")), nil
}
@ -141,7 +148,8 @@ var _ = Describe("ListCache", func() {
"gr1": {"http://dummy"},
}
sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, mockDownloader, defaultProcessingConcurrency)
sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, mockDownloader,
defaultProcessingConcurrency, false)
Expect(err).Should(Succeed())
By("Lists loaded without err", func() {
@ -171,7 +179,7 @@ var _ = Describe("ListCache", func() {
"gr2": {server3.URL},
}
sut, _ := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency)
sut, _ := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency, false)
found, group := sut.Match("blocked1.com", []string{"gr1", "gr2"})
Expect(found).Should(BeTrue())
@ -193,7 +201,7 @@ var _ = Describe("ListCache", func() {
"gr2": {server3.URL, "someotherfile"},
}
sut, _ := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency)
sut, _ := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency, false)
found, group := sut.Match("blocked1.com", []string{"gr1", "gr2"})
Expect(found).Should(BeTrue())
@ -220,7 +228,7 @@ var _ = Describe("ListCache", func() {
resultCnt = cnt
})
sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency)
sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency, false)
Expect(err).Should(Succeed())
found, group := sut.Match("blocked1.com", []string{})
@ -232,11 +240,11 @@ var _ = Describe("ListCache", func() {
When("multiple groups are passed", func() {
It("should match", func() {
lists := map[string][]string{
"gr1": {file1.Name(), file2.Name()},
"gr2": {"file://" + file3.Name()},
"gr1": {file1.Path, file2.Path},
"gr2": {"file://" + file3.Path},
}
sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency)
sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency, false)
Expect(err).Should(Succeed())
Expect(sut.groupCaches["gr1"].ElementCount()).Should(Equal(3))
@ -264,7 +272,8 @@ var _ = Describe("ListCache", func() {
"gr1": {file1, file2, file3},
}
sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency)
sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(),
defaultProcessingConcurrency, false)
Expect(err).Should(Succeed())
Expect(sut.groupCaches["gr1"].ElementCount()).Should(Equal(38000))
@ -276,7 +285,8 @@ var _ = Describe("ListCache", func() {
"gr1": {"inlinedomain1.com\n#some comment\ninlinedomain2.com"},
}
sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency)
sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(),
defaultProcessingConcurrency, false)
Expect(err).Should(Succeed())
Expect(sut.groupCaches["gr1"].ElementCount()).Should(Equal(2))
@ -296,7 +306,8 @@ var _ = Describe("ListCache", func() {
"gr1": {"inlinedomain1.com\n" + strings.Repeat("longString", 100000)},
}
sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency)
sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(),
defaultProcessingConcurrency, false)
Expect(err).Should(Succeed())
found, group := sut.Match("inlinedomain1.com", []string{"gr1"})
@ -310,7 +321,8 @@ var _ = Describe("ListCache", func() {
"gr1": {"/^apple\\.(de|com)$/\n"},
}
sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency)
sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(),
defaultProcessingConcurrency, false)
Expect(err).Should(Succeed())
found, group := sut.Match("apple.com", []string{"gr1"})
@ -331,7 +343,8 @@ var _ = Describe("ListCache", func() {
"gr2": {"inline\ndefinition\n"},
}
sut, err := NewListCache(ListCacheTypeBlacklist, lists, time.Hour, NewDownloader(), defaultProcessingConcurrency)
sut, err := NewListCache(ListCacheTypeBlacklist, lists, time.Hour, NewDownloader(),
defaultProcessingConcurrency, false)
Expect(err).Should(Succeed())
c := sut.Configuration()
@ -342,10 +355,11 @@ var _ = Describe("ListCache", func() {
When("refresh is disabled", func() {
It("should print 'refresh disabled'", func() {
lists := map[string][]string{
"gr1": {emptyFile.Name()},
"gr1": {emptyFile.Path},
}
sut, err := NewListCache(ListCacheTypeBlacklist, lists, -1, NewDownloader(), defaultProcessingConcurrency)
sut, err := NewListCache(ListCacheTypeBlacklist, lists, -1, NewDownloader(),
defaultProcessingConcurrency, false)
Expect(err).Should(Succeed())
c := sut.Configuration()
@ -353,6 +367,20 @@ var _ = Describe("ListCache", func() {
})
})
})
Describe("StartStrategy", func() {
When("async load is enabled", func() {
It("should never return an error", func() {
lists := map[string][]string{
"gr1": {"doesnotexist"},
}
_, err := NewListCache(ListCacheTypeBlacklist, lists, -1, NewDownloader(),
defaultProcessingConcurrency, true)
Expect(err).Should(Succeed())
})
})
})
})
type MockDownloader struct {
@ -366,7 +394,7 @@ func (m *MockDownloader) DownloadFile(_ string) (io.ReadCloser, error) {
}
func createTestListFile(dir string, totalLines int) string {
file, err := ioutil.TempFile(dir, "blocky")
file, err := os.CreateTemp(dir, "blocky")
if err != nil {
log.Fatal(err)
}

View File

@ -1,9 +1,9 @@
package log
//go:generate go-enum -f=$GOFILE --marshal --names
//go:generate go run github.com/abice/go-enum -f=$GOFILE --marshal --names
import (
"io/ioutil"
"io"
"strings"
"github.com/sirupsen/logrus"
@ -69,7 +69,7 @@ func ConfigureLogger(logLevel Level, formatType FormatType, logTimestamp bool) {
TimestampFormat: "2006-01-02 15:04:05",
FullTimestamp: true,
ForceFormatting: true,
ForceColors: true,
ForceColors: false,
QuoteEmptyFields: true,
DisableTimestamp: !logTimestamp}
@ -87,5 +87,5 @@ func ConfigureLogger(logLevel Level, formatType FormatType, logTimestamp bool) {
// Silence disables the logger output
func Silence() {
logger.Out = ioutil.Discard
logger.Out = io.Discard
}

View File

@ -20,6 +20,8 @@ const (
FormatTypeJson
)
var ErrInvalidFormatType = fmt.Errorf("not a valid FormatType, try [%s]", strings.Join(_FormatTypeNames, ", "))
const _FormatTypeName = "textjson"
var _FormatTypeNames = []string{
@ -57,7 +59,7 @@ func ParseFormatType(name string) (FormatType, error) {
if x, ok := _FormatTypeValue[name]; ok {
return x, nil
}
return FormatType(0), fmt.Errorf("%s is not a valid FormatType, try [%s]", name, strings.Join(_FormatTypeNames, ", "))
return FormatType(0), fmt.Errorf("%s is %w", name, ErrInvalidFormatType)
}
// MarshalText implements the text marshaller method.
@ -91,6 +93,8 @@ const (
LevelFatal
)
var ErrInvalidLevel = fmt.Errorf("not a valid Level, try [%s]", strings.Join(_LevelNames, ", "))
const _LevelName = "infotracedebugwarnerrorfatal"
var _LevelNames = []string{
@ -140,7 +144,7 @@ func ParseLevel(name string) (Level, error) {
if x, ok := _LevelValue[name]; ok {
return x, nil
}
return Level(0), fmt.Errorf("%s is not a valid Level, try [%s]", name, strings.Join(_LevelNames, ", "))
return Level(0), fmt.Errorf("%s is %w", name, ErrInvalidLevel)
}
// MarshalText implements the text marshaller method.

41
main_static.go Normal file
View File

@ -0,0 +1,41 @@
//go:build linux
// +build linux
package main
import (
"os"
"time"
_ "time/tzdata"
reaper "github.com/ramr/go-reaper"
)
//nolint:gochecknoinits
func init() {
go reaper.Reap()
setLocaltime()
}
// set localtime to /etc/localtime if available
// or modify the system time with the TZ environment variable if it is provided
func setLocaltime() {
// load /etc/localtime without modifying it
if lt, err := os.ReadFile("/etc/localtime"); err == nil {
if t, err := time.LoadLocationFromTZData("", lt); err == nil {
time.Local = t
return
}
}
// use zoneinfo from time/tzdata and set location with the TZ environment variable
if tz := os.Getenv("TZ"); tz != "" {
if t, err := time.LoadLocation(tz); err == nil {
time.Local = t
return
}
}
}

View File

@ -1,6 +1,6 @@
package model
//go:generate go-enum -f=$GOFILE --marshal --names
//go:generate go run github.com/abice/go-enum -f=$GOFILE --marshal --names
import (
"net"
"time"
@ -17,6 +17,8 @@ import (
// CUSTOMDNS // the query was resolved by a custom rule
// HOSTSFILE // the query was resolved by looking up the hosts file
// FILTERED // the query was filtered by query type
// NOTFQDN // the query was filtered as it is not fqdn conform
// SPECIAL // the query was resolved by the special use domain name resolver
// )
type ResponseType int

View File

@ -20,6 +20,8 @@ const (
RequestProtocolUDP
)
var ErrInvalidRequestProtocol = fmt.Errorf("not a valid RequestProtocol, try [%s]", strings.Join(_RequestProtocolNames, ", "))
const _RequestProtocolName = "TCPUDP"
var _RequestProtocolNames = []string{
@ -57,7 +59,7 @@ func ParseRequestProtocol(name string) (RequestProtocol, error) {
if x, ok := _RequestProtocolValue[name]; ok {
return x, nil
}
return RequestProtocol(0), fmt.Errorf("%s is not a valid RequestProtocol, try [%s]", name, strings.Join(_RequestProtocolNames, ", "))
return RequestProtocol(0), fmt.Errorf("%s is %w", name, ErrInvalidRequestProtocol)
}
// MarshalText implements the text marshaller method.
@ -98,9 +100,17 @@ const (
// ResponseTypeFILTERED is a ResponseType of type FILTERED.
// the query was filtered by query type
ResponseTypeFILTERED
// ResponseTypeNOTFQDN is a ResponseType of type NOTFQDN.
// the query was filtered as it is not fqdn conform
ResponseTypeNOTFQDN
// ResponseTypeSPECIAL is a ResponseType of type SPECIAL.
// the query was resolved by the special use domain name resolver
ResponseTypeSPECIAL
)
const _ResponseTypeName = "RESOLVEDCACHEDBLOCKEDCONDITIONALCUSTOMDNSHOSTSFILEFILTERED"
var ErrInvalidResponseType = fmt.Errorf("not a valid ResponseType, try [%s]", strings.Join(_ResponseTypeNames, ", "))
const _ResponseTypeName = "RESOLVEDCACHEDBLOCKEDCONDITIONALCUSTOMDNSHOSTSFILEFILTEREDNOTFQDNSPECIAL"
var _ResponseTypeNames = []string{
_ResponseTypeName[0:8],
@ -110,6 +120,8 @@ var _ResponseTypeNames = []string{
_ResponseTypeName[32:41],
_ResponseTypeName[41:50],
_ResponseTypeName[50:58],
_ResponseTypeName[58:65],
_ResponseTypeName[65:72],
}
// ResponseTypeNames returns a list of possible string values of ResponseType.
@ -127,6 +139,8 @@ var _ResponseTypeMap = map[ResponseType]string{
ResponseTypeCUSTOMDNS: _ResponseTypeName[32:41],
ResponseTypeHOSTSFILE: _ResponseTypeName[41:50],
ResponseTypeFILTERED: _ResponseTypeName[50:58],
ResponseTypeNOTFQDN: _ResponseTypeName[58:65],
ResponseTypeSPECIAL: _ResponseTypeName[65:72],
}
// String implements the Stringer interface.
@ -145,6 +159,8 @@ var _ResponseTypeValue = map[string]ResponseType{
_ResponseTypeName[32:41]: ResponseTypeCUSTOMDNS,
_ResponseTypeName[41:50]: ResponseTypeHOSTSFILE,
_ResponseTypeName[50:58]: ResponseTypeFILTERED,
_ResponseTypeName[58:65]: ResponseTypeNOTFQDN,
_ResponseTypeName[65:72]: ResponseTypeSPECIAL,
}
// ParseResponseType attempts to convert a string to a ResponseType.
@ -152,7 +168,7 @@ func ParseResponseType(name string) (ResponseType, error) {
if x, ok := _ResponseTypeValue[name]; ok {
return x, nil
}
return ResponseType(0), fmt.Errorf("%s is not a valid ResponseType, try [%s]", name, strings.Join(_ResponseTypeNames, ", "))
return ResponseType(0), fmt.Errorf("%s is %w", name, ErrInvalidResponseType)
}
// MarshalText implements the text marshaller method.

View File

@ -2,6 +2,7 @@ package querylog
import (
"fmt"
"reflect"
"strings"
"sync"
"time"
@ -70,7 +71,7 @@ func newDatabaseWriter(target gorm.Dialector, logRetentionDays uint64,
}
// Migrate the schema
if err := db.AutoMigrate(&logEntry{}); err != nil {
if err := databaseMigration(db); err != nil {
return nil, fmt.Errorf("can't perform auto migration: %w", err)
}
@ -84,6 +85,35 @@ func newDatabaseWriter(target gorm.Dialector, logRetentionDays uint64,
return w, nil
}
func databaseMigration(db *gorm.DB) error {
if err := db.AutoMigrate(&logEntry{}); err != nil {
return err
}
tableName := db.NamingStrategy.TableName(reflect.TypeOf(logEntry{}).Name())
// create unmapped primary key
switch db.Config.Name() {
case "mysql":
tx := db.Exec("ALTER TABLE `" + tableName + "` ADD `id` INT PRIMARY KEY AUTO_INCREMENT")
if tx.Error != nil {
// mysql doesn't support "add column if not exist"
if strings.Contains(tx.Error.Error(), "1060") {
// error 1060: duplicate column name
// ignore it
return nil
}
return tx.Error
}
case "postgres":
return db.Exec("ALTER TABLE " + tableName + " ADD column if not exists id serial primary key").Error
}
return nil
}
func (d *DatabaseWriter) periodicFlush() {
ticker := time.NewTicker(d.dbFlushPeriod)
defer ticker.Stop()

View File

@ -9,61 +9,102 @@ import (
"github.com/miekg/dns"
"github.com/sirupsen/logrus"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
. "github.com/onsi/gomega"
. "github.com/onsi/ginkgo/v2"
)
var err error
var _ = Describe("DatabaseWriter", func() {
Describe("Database query log", func() {
When("New log entry was created", func() {
It("should be persisted in the database", func() {
sqlite := sqlite.Open("file::memory:")
writer, err := newDatabaseWriter(sqlite, 7, time.Millisecond)
Expect(err).Should(Succeed())
request := &model.Request{
Req: util.NewMsgWithQuestion("google.de.", dns.Type(dns.TypeA)),
Log: logrus.NewEntry(log.Log()),
}
res, err := util.NewMsgWithAnswer("example.com", 123, dns.Type(dns.TypeA), "123.124.122.122")
Describe("Database query log to sqlite", func() {
var (
sqliteDB gorm.Dialector
writer *DatabaseWriter
request *model.Request
)
BeforeEach(func() {
sqliteDB = sqlite.Open("file::memory:")
request = &model.Request{
Req: util.NewMsgWithQuestion("google.de.", dns.Type(dns.TypeA)),
Log: logrus.NewEntry(log.Log()),
}
})
When("New log entry was created", func() {
BeforeEach(func() {
writer, err = newDatabaseWriter(sqliteDB, 7, time.Millisecond)
Expect(err).Should(Succeed())
})
It("should be persisted in the database", func() {
res, err := util.NewMsgWithAnswer("example.com", 123, dns.Type(dns.TypeA), "123.124.122.122")
Expect(err).Should(Succeed())
response := &model.Response{
Res: res,
Reason: "Resolved",
RType: model.ResponseTypeRESOLVED,
}
// one entry with now as timestamp
writer.Write(&LogEntry{
Request: request,
Response: response,
Start: time.Now(),
DurationMs: 20,
})
// one entry before 2 days
writer.Write(&LogEntry{
Request: request,
Response: response,
Start: time.Now().AddDate(0, 0, -2),
DurationMs: 20,
})
// force write
writer.doDBWrite()
// 2 entries in the database
Eventually(func() int64 {
var res int64
result := writer.db.Find(&logEntry{})
result.Count(&res)
return res
}, "5s").Should(BeNumerically("==", 2))
// do cleanup now
writer.CleanUp()
// now only 1 entry in the database
Eventually(func() (res int64) {
result := writer.db.Find(&logEntry{})
result.Count(&res)
return res
}, "1s").Should(BeNumerically("==", 1))
}, "5s").Should(BeNumerically("==", 2))
})
})
When("There are log entries with timestamp exceeding the retention period", func() {
BeforeEach(func() {
writer, err = newDatabaseWriter(sqliteDB, 1, time.Millisecond)
Expect(err).Should(Succeed())
})
It("these old entries should be deleted", func() {
sqlite := sqlite.Open("file::memory:")
writer, err := newDatabaseWriter(sqlite, 1, time.Millisecond)
Expect(err).Should(Succeed())
request := &model.Request{
Req: util.NewMsgWithQuestion("google.de.", dns.Type(dns.TypeA)),
Log: logrus.NewEntry(log.Log()),
}
res, err := util.NewMsgWithAnswer("example.com", 123, dns.Type(dns.TypeA), "123.124.122.122")
Expect(err).Should(Succeed())
response := &model.Response{
Res: res,
Reason: "Resolved",
@ -86,6 +127,9 @@ var _ = Describe("DatabaseWriter", func() {
DurationMs: 20,
})
// force write
writer.doDBWrite()
// 2 entries in the database
Eventually(func() int64 {
var res int64
@ -94,7 +138,7 @@ var _ = Describe("DatabaseWriter", func() {
result.Count(&res)
return res
}, "1s").Should(BeNumerically("==", 2))
}, "5s").Should(BeNumerically("==", 2))
// do cleanup now
writer.CleanUp()
@ -106,10 +150,12 @@ var _ = Describe("DatabaseWriter", func() {
result.Count(&res)
return res
}, "1s").Should(BeNumerically("==", 1))
}, "5s").Should(BeNumerically("==", 1))
})
})
})
Describe("Database query log fails", func() {
When("mysql connection parameters wrong", func() {
It("should be log with fatal", func() {
_, err := NewDatabaseWriter("mysql", "wrong param", 7, 1)

View File

@ -4,7 +4,6 @@ import (
"encoding/csv"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"regexp"
@ -59,14 +58,13 @@ func (d *FileWriter) Write(entry *LogEntry) {
"can't create/open file", err)
if err == nil {
defer file.Close()
writer := createCsvWriter(file)
err := writer.Write(createQueryLogRow(entry))
util.LogOnErrorWithEntry(log.PrefixedLog(loggerPrefixFileWriter).WithField("file_name", writePath),
"can't write to file", err)
writer.Flush()
_ = file.Close()
}
}
@ -78,7 +76,7 @@ func (d *FileWriter) CleanUp() {
logger.Trace("starting clean up")
files, err := ioutil.ReadDir(d.target)
files, err := os.ReadDir(d.target)
util.LogOnErrorWithEntry(logger.WithField("target", d.target), "can't list log directory: ", err)

View File

@ -6,11 +6,10 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"time"
"github.com/0xERR0R/blocky/helpertest"
"github.com/0xERR0R/blocky/log"
"github.com/0xERR0R/blocky/model"
@ -21,14 +20,16 @@ import (
)
var _ = Describe("FileWriter", func() {
var tmpDir string
var err error
BeforeEach(func() {
tmpDir, err = ioutil.TempDir("", "fileWriter")
Expect(err).Should(Succeed())
})
AfterEach(func() {
_ = os.RemoveAll(tmpDir)
var (
tmpDir *helpertest.TmpFolder
err error
writer *FileWriter
)
JustBeforeEach(func() {
tmpDir = helpertest.NewTmpFolder("fileWriter")
Expect(tmpDir.Error).Should(Succeed())
DeferCleanup(tmpDir.Clean)
})
Describe("CSV writer", func() {
@ -40,9 +41,10 @@ var _ = Describe("FileWriter", func() {
})
When("New log entry was created", func() {
It("should be logged in one file", func() {
tmpDir, err = ioutil.TempDir("", "queryLoggingResolver")
writer, err = NewCSVWriter(tmpDir.Path, false, 0)
Expect(err).Should(Succeed())
writer, _ := NewCSVWriter(tmpDir, false, 0)
res, err := util.NewMsgWithAnswer("example.com", 123, dns.Type(dns.TypeA), "123.124.122.122")
Expect(err).Should(Succeed())
@ -81,15 +83,17 @@ var _ = Describe("FileWriter", func() {
})
})
csvLines := readCsv(filepath.Join(tmpDir, fmt.Sprintf("%s_ALL.log", time.Now().Format("2006-01-02"))))
Expect(csvLines).Should(HaveLen(2))
Eventually(func(g Gomega) int {
return len(readCsv(tmpDir.JoinPath(
fmt.Sprintf("%s_ALL.log", time.Now().Format("2006-01-02")))))
}).Should(Equal(2))
})
It("should be logged in separate files per client", func() {
tmpDir, err = ioutil.TempDir("", "queryLoggingResolver")
writer, err = NewCSVWriter(tmpDir.Path, true, 0)
Expect(err).Should(Succeed())
writer, _ := NewCSVWriter(tmpDir, true, 0)
res, err := util.NewMsgWithAnswer("example.com", 123, dns.Type(dns.TypeA), "123.124.122.122")
Expect(err).Should(Succeed())
@ -128,19 +132,24 @@ var _ = Describe("FileWriter", func() {
})
})
csvLines := readCsv(filepath.Join(tmpDir, fmt.Sprintf("%s_client1.log", time.Now().Format("2006-01-02"))))
Expect(csvLines).Should(HaveLen(1))
Eventually(func(g Gomega) int {
return len(readCsv(tmpDir.JoinPath(
fmt.Sprintf("%s_client1.log", time.Now().Format("2006-01-02")))))
}).Should(Equal(1))
csvLines = readCsv(filepath.Join(tmpDir, fmt.Sprintf("%s_client2.log", time.Now().Format("2006-01-02"))))
Expect(csvLines).Should(HaveLen(1))
Eventually(func(g Gomega) int {
return len(readCsv(tmpDir.JoinPath(
fmt.Sprintf("%s_client2.log", time.Now().Format("2006-01-02")))))
}).Should(Equal(1))
})
})
When("Cleanup is called", func() {
It("should delete old files", func() {
tmpDir, err = ioutil.TempDir("", "queryLoggingResolver")
writer, err = NewCSVWriter(tmpDir.Path, false, 1)
Expect(err).Should(Succeed())
writer, _ := NewCSVWriter(tmpDir, false, 1)
res, err := util.NewMsgWithAnswer("example.com", 123, dns.Type(dns.TypeA), "123.124.122.122")
Expect(err).Should(Succeed())
@ -173,19 +182,27 @@ var _ = Describe("FileWriter", func() {
Reason: "Resolved",
RType: model.ResponseTypeRESOLVED,
},
Start: time.Now().AddDate(0, 0, -2),
Start: time.Now().AddDate(0, 0, -3),
DurationMs: 20,
})
})
fmt.Println(tmpDir.Path)
files, err := ioutil.ReadDir(tmpDir)
Expect(err).Should(Succeed())
Expect(files).Should(HaveLen(2))
writer.CleanUp()
Eventually(func(g Gomega) int {
filesCount, err := tmpDir.CountFiles()
g.Expect(err).Should(Succeed())
files, err = ioutil.ReadDir(tmpDir)
Expect(err).Should(Succeed())
Expect(files).Should(HaveLen(1))
return filesCount
}, "20s", "1s").Should(Equal(2))
go writer.CleanUp()
Eventually(func(g Gomega) int {
filesCount, err := tmpDir.CountFiles()
g.Expect(err).Should(Succeed())
return filesCount
}, "20s", "1s").Should(Equal(1))
})
})
})

View File

@ -101,13 +101,15 @@ func NewBlockingResolver(cfg config.BlockingConfig,
refreshPeriod := time.Duration(cfg.RefreshPeriod)
downloader := createDownloader(cfg, bootstrap)
blacklistMatcher, blErr := lists.NewListCache(lists.ListCacheTypeBlacklist, cfg.BlackLists,
refreshPeriod, downloader, cfg.ProcessingConcurrency)
refreshPeriod, downloader, cfg.ProcessingConcurrency,
(cfg.StartStrategy == config.StartStrategyTypeFast))
whitelistMatcher, wlErr := lists.NewListCache(lists.ListCacheTypeWhitelist, cfg.WhiteLists,
refreshPeriod, downloader, cfg.ProcessingConcurrency)
refreshPeriod, downloader, cfg.ProcessingConcurrency,
(cfg.StartStrategy == config.StartStrategyTypeFast))
whitelistOnlyGroups := determineWhitelistOnlyGroups(&cfg)
err = multierror.Append(err, blErr, wlErr).ErrorOrNil()
if err != nil && cfg.FailStartOnListError {
if err != nil && cfg.StartStrategy == config.StartStrategyTypeFailOnError {
return nil, err
}
@ -465,6 +467,9 @@ func (r *BlockingResolver) isGroupDisabled(group string) bool {
// returns groups which should be checked for client's request
func (r *BlockingResolver) groupsToCheckForClient(request *model.Request) []string {
r.status.lock.RLock()
defer r.status.lock.RUnlock()
var groups []string
// try client names
for _, cName := range request.ClientNames {
@ -613,6 +618,9 @@ func (r *BlockingResolver) queryForFQIdentifierIPs(identifier string) (result []
}
func (r *BlockingResolver) initFQDNIPCache() {
r.status.lock.Lock()
defer r.status.lock.Unlock()
identifiers := make([]string, 0)
for identifier := range r.clientGroupsBlock {

View File

@ -11,7 +11,6 @@ import (
"github.com/alicebob/miniredis/v2"
"github.com/creasty/defaults"
"os"
"time"
"github.com/miekg/dns"
@ -20,22 +19,26 @@ import (
"github.com/stretchr/testify/mock"
)
var group1File, group2File, defaultGroupFile *os.File
var group1File, group2File, defaultGroupFile *TmpFile
var tmpDir *TmpFolder
var _ = BeforeSuite(func() {
group1File = TempFile("DOMAIN1.com")
group2File = TempFile("blocked2.com")
defaultGroupFile = TempFile(
`blocked3.com
123.145.123.145
2001:db8:85a3:08d3::370:7344
badcnamedomain.com`)
})
tmpDir = NewTmpFolder("BlockingResolver")
Expect(tmpDir.Error).Should(Succeed())
DeferCleanup(tmpDir.Clean)
var _ = AfterSuite(func() {
_ = group1File.Close()
_ = group2File.Close()
_ = defaultGroupFile.Close()
group1File = tmpDir.CreateStringFile("group1File", "DOMAIN1.com")
Expect(group1File.Error).Should(Succeed())
group2File = tmpDir.CreateStringFile("group2File", "blocked2.com")
Expect(group2File.Error).Should(Succeed())
defaultGroupFile = tmpDir.CreateStringFile("defaultGroupFile",
"blocked3.com",
"123.145.123.145",
"2001:db8:85a3:08d3::370:7344",
"badcnamedomain.com")
Expect(defaultGroupFile.Error).Should(Succeed())
})
var _ = Describe("BlockingResolver", Label("blockingResolver"), func() {
@ -85,8 +88,8 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() {
BlockType: "ZEROIP",
BlockTTL: config.Duration(time.Minute),
BlackLists: map[string][]string{
"gr1": {group1File.Name()},
"gr2": {group2File.Name()},
"gr1": {group1File.Path},
"gr2": {group2File.Path},
},
}
})
@ -114,8 +117,8 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() {
BlockType: "ZEROIP",
BlockTTL: config.Duration(time.Minute),
BlackLists: map[string][]string{
"gr1": {group1File.Name()},
"gr2": {group2File.Name()},
"gr1": {group1File.Path},
"gr2": {group2File.Path},
},
ClientGroupsBlock: map[string][]string{
"default": {"gr1"},
@ -137,13 +140,12 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() {
return nil
}
Bus().Publish(ApplicationStarted, "")
time.Sleep(time.Second)
Eventually(func(g Gomega) {
resp, err = sut.Resolve(newRequestWithClient("blocked2.com.", dns.Type(dns.TypeA), "192.168.178.39", "client1"))
g.Expect(err).NotTo(HaveOccurred())
g.Expect(resp.Res.Answer).ShouldNot(BeNil())
g.Expect(resp.Res.Answer).Should(BeDNSRecord("blocked2.com.", dns.TypeA, 60, "0.0.0.0"))
}, "1s").Should(Succeed())
}, "10s", "1s").Should(Succeed())
})
})
})
@ -154,9 +156,9 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() {
sutConfig = config.BlockingConfig{
BlockTTL: config.Duration(6 * time.Hour),
BlackLists: map[string][]string{
"gr1": {group1File.Name()},
"gr2": {group2File.Name()},
"defaultGroup": {defaultGroupFile.Name()},
"gr1": {group1File.Path},
"gr2": {group2File.Path},
"defaultGroup": {defaultGroupFile.Path},
},
ClientGroupsBlock: map[string][]string{
"client1": {"gr1"},
@ -293,7 +295,7 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() {
sutConfig = config.BlockingConfig{
BlockTTL: config.Duration(time.Minute),
BlackLists: map[string][]string{
"defaultGroup": {defaultGroupFile.Name()},
"defaultGroup": {defaultGroupFile.Path},
},
ClientGroupsBlock: map[string][]string{
"default": {"defaultGroup"},
@ -318,7 +320,7 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() {
sutConfig = config.BlockingConfig{
BlockType: "ZEROIP",
BlackLists: map[string][]string{
"defaultGroup": {defaultGroupFile.Name()},
"defaultGroup": {defaultGroupFile.Path},
},
ClientGroupsBlock: map[string][]string{
"default": {"defaultGroup"},
@ -353,7 +355,7 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() {
sutConfig = config.BlockingConfig{
BlockTTL: config.Duration(6 * time.Hour),
BlackLists: map[string][]string{
"defaultGroup": {defaultGroupFile.Name()},
"defaultGroup": {defaultGroupFile.Path},
},
ClientGroupsBlock: map[string][]string{
"default": {"defaultGroup"},
@ -381,7 +383,7 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() {
BeforeEach(func() {
sutConfig = config.BlockingConfig{
BlackLists: map[string][]string{
"defaultGroup": {defaultGroupFile.Name()},
"defaultGroup": {defaultGroupFile.Path},
},
ClientGroupsBlock: map[string][]string{
"default": {"defaultGroup"},
@ -451,8 +453,8 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() {
sutConfig = config.BlockingConfig{
BlockType: "ZEROIP",
BlockTTL: config.Duration(time.Minute),
BlackLists: map[string][]string{"gr1": {group1File.Name()}},
WhiteLists: map[string][]string{"gr1": {group1File.Name()}},
BlackLists: map[string][]string{"gr1": {group1File.Path}},
WhiteLists: map[string][]string{"gr1": {group1File.Path}},
ClientGroupsBlock: map[string][]string{
"default": {"gr1"},
},
@ -472,8 +474,8 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() {
BlockType: "zeroIP",
BlockTTL: config.Duration(60 * time.Second),
WhiteLists: map[string][]string{
"gr1": {group1File.Name()},
"gr2": {group2File.Name()},
"gr1": {group1File.Path},
"gr2": {group2File.Path},
},
ClientGroupsBlock: map[string][]string{
"default": {"gr1"},
@ -533,8 +535,8 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() {
sutConfig = config.BlockingConfig{
BlockType: "ZEROIP",
BlockTTL: config.Duration(time.Minute),
BlackLists: map[string][]string{"gr1": {group1File.Name()}},
WhiteLists: map[string][]string{"gr1": {defaultGroupFile.Name()}},
BlackLists: map[string][]string{"gr1": {group1File.Path}},
WhiteLists: map[string][]string{"gr1": {defaultGroupFile.Path}},
ClientGroupsBlock: map[string][]string{
"default": {"gr1"},
},
@ -555,7 +557,7 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() {
sutConfig = config.BlockingConfig{
BlockType: "ZEROIP",
BlockTTL: config.Duration(time.Minute),
BlackLists: map[string][]string{"gr1": {group1File.Name()}},
BlackLists: map[string][]string{"gr1": {group1File.Path}},
ClientGroupsBlock: map[string][]string{
"default": {"gr1"},
},
@ -590,8 +592,8 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() {
BeforeEach(func() {
sutConfig = config.BlockingConfig{
BlackLists: map[string][]string{
"defaultGroup": {defaultGroupFile.Name()},
"group1": {group1File.Name()},
"defaultGroup": {defaultGroupFile.Path},
"group1": {group1File.Path},
},
ClientGroupsBlock: map[string][]string{
"default": {"defaultGroup", "group1"},
@ -821,7 +823,7 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() {
sutConfig = config.BlockingConfig{
BlockType: "ZEROIP",
BlockTTL: config.Duration(time.Minute),
BlackLists: map[string][]string{"gr1": {group1File.Name()}},
BlackLists: map[string][]string{"gr1": {group1File.Path}},
ClientGroupsBlock: map[string][]string{
"default": {"gr1"},
},
@ -856,14 +858,13 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() {
MatchError("unknown blockType 'wrong', please use one of: ZeroIP, NxDomain or specify destination IP address(es)"))
})
})
When("failStartOnListError is active", func() {
When("startStrategy is failOnError", func() {
It("should fail if lists can't be downloaded", func() {
_, err := NewBlockingResolver(config.BlockingConfig{
BlackLists: map[string][]string{"gr1": {"wrongPath"}},
WhiteLists: map[string][]string{"whitelist": {"wrongPath"}},
FailStartOnListError: true,
BlockType: "zeroIp",
BlackLists: map[string][]string{"gr1": {"wrongPath"}},
WhiteLists: map[string][]string{"whitelist": {"wrongPath"}},
StartStrategy: config.StartStrategyTypeFailOnError,
BlockType: "zeroIp",
}, nil, skipUpstreamCheck)
Expect(err).Should(HaveOccurred())
})

View File

@ -26,7 +26,8 @@ var (
// Bootstrap allows resolving hostnames using the configured bootstrap DNS.
type Bootstrap struct {
log *logrus.Entry
log *logrus.Entry
startVerifyUpstream bool
resolver Resolver
upstream Resolver // the upstream that's part of the above resolver
@ -64,9 +65,10 @@ func NewBootstrap(cfg *config.Config) (b *Bootstrap, err error) {
// This also prevents the GC to clean up these two structs, but is not currently an
// issue since they stay allocated until the process terminates
b = &Bootstrap{
log: log,
upstreamIPs: ips,
systemResolver: net.DefaultResolver, // allow replacing it during tests
log: log,
upstreamIPs: ips,
systemResolver: net.DefaultResolver, // allow replacing it during tests
startVerifyUpstream: cfg.StartVerifyUpstream,
}
if upstream.IsDefault() {
@ -96,26 +98,18 @@ func (b *Bootstrap) UpstreamIPs(r *UpstreamResolver) (*IPSet, error) {
func (b *Bootstrap) resolveUpstream(r Resolver, host string) ([]net.IP, error) {
// Use system resolver if no bootstrap is configured
if b.resolver == nil {
filteredQTypes := config.GetConfig().Filtering.QueryTypes
network := "ip"
if filteredQTypes.Contains(dns.Type(dns.TypeAAAA)) {
network = "ip4"
} else if filteredQTypes.Contains(dns.Type(dns.TypeA)) {
network = "ip6"
}
cfg := config.GetConfig()
ctx := context.Background()
timeout := config.GetConfig().UpstreamTimeout
timeout := cfg.UpstreamTimeout
if timeout != 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(context.Background(), time.Duration(timeout))
ctx, cancel = context.WithTimeout(ctx, time.Duration(timeout))
defer cancel()
}
return b.systemResolver.LookupIP(ctx, network, host)
return b.systemResolver.LookupIP(ctx, cfg.ConnectIPVersion.Net(), host)
}
if r == b.upstream {
@ -145,14 +139,16 @@ func (b *Bootstrap) NewHTTPTransport() *http.Transport {
return nil, err
}
filteredQTypes := config.GetConfig().Filtering.QueryTypes
connectIPVersion := config.GetConfig().ConnectIPVersion
var qTypes []dns.Type
switch {
case strings.HasSuffix(network, "4") || filteredQTypes.Contains(dns.Type(dns.TypeAAAA)):
case connectIPVersion != config.IPVersionDual: // ignore `network` if a specific version is configured
qTypes = connectIPVersion.QTypes()
case strings.HasSuffix(network, "4"):
qTypes = []dns.Type{dns.Type(dns.TypeA)}
case strings.HasSuffix(network, "6") || filteredQTypes.Contains(dns.Type(dns.TypeA)):
case strings.HasSuffix(network, "6"):
qTypes = []dns.Type{dns.Type(dns.TypeAAAA)}
default:
qTypes = v4v6QTypes

View File

@ -112,7 +112,7 @@ func (r *CachingResolver) onExpired(cacheKey string) (val interface{}, ttl time.
if response.Res.Rcode == dns.RcodeSuccess {
evt.Bus().Publish(evt.CachingDomainPrefetched, domainName)
return cacheValue{response.Res.Answer, true}, time.Duration(r.adjustTTLs(response.Res.Answer)) * time.Second
return cacheValue{response.Res.Answer, true}, r.adjustTTLs(response.Res.Answer)
}
} else {
util.LogOnError(fmt.Sprintf("can't prefetch '%s' ", domainName), err)
@ -232,7 +232,7 @@ func (r *CachingResolver) putInCache(cacheKey string, response *model.Response,
if response.Res.Rcode == dns.RcodeSuccess {
// put value into cache
r.resultCache.Put(cacheKey, cacheValue{answer, prefetch}, time.Duration(r.adjustTTLs(answer))*time.Second)
r.resultCache.Put(cacheKey, cacheValue{answer, prefetch}, r.adjustTTLs(answer))
} else if response.Res.Rcode == dns.RcodeNameError {
if r.cacheTimeNegative > 0 {
// put return code if NXDOMAIN
@ -249,7 +249,16 @@ func (r *CachingResolver) putInCache(cacheKey string, response *model.Response,
}
}
func (r *CachingResolver) adjustTTLs(answer []dns.RR) (maxTTL uint32) {
// adjustTTLs calculates and returns the max TTL (considers also the min and max cache time)
// for all records from answer or a negative cache time for empty answer
// adjust the TTL in the answer header accordingly
func (r *CachingResolver) adjustTTLs(answer []dns.RR) (maxTTL time.Duration) {
var max uint32
if len(answer) == 0 {
return r.cacheTimeNegative
}
for _, a := range answer {
// if TTL < mitTTL -> adjust the value, set minTTL
if r.minCacheTimeSec > 0 {
@ -264,10 +273,10 @@ func (r *CachingResolver) adjustTTLs(answer []dns.RR) (maxTTL uint32) {
}
}
if maxTTL < a.Header().Ttl {
maxTTL = a.Header().Ttl
if max < a.Header().Ttl {
max = a.Header().Ttl
}
}
return
return time.Duration(max) * time.Second
}

View File

@ -355,66 +355,98 @@ var _ = Describe("CachingResolver", func() {
})
Describe("Negative cache (caching if upstream resolver returns NXDOMAIN)", func() {
When("Upstream resolver returns NXDOMAIN with caching", func() {
BeforeEach(func() {
mockAnswer.Rcode = dns.RcodeNameError
})
It("response should be cached", func() {
By("first request", func() {
resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA)))
Expect(err).Should(Succeed())
Expect(resp.RType).Should(Equal(ResponseTypeRESOLVED))
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError))
Expect(m.Calls).Should(HaveLen(1))
Context("Caching if upstream resolver returns NXDOMAIN", func() {
When("Upstream resolver returns NXDOMAIN with caching", func() {
BeforeEach(func() {
mockAnswer.Rcode = dns.RcodeNameError
})
By("second request", func() {
Eventually(func(g Gomega) {
It("response should be cached", func() {
By("first request", func() {
resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA)))
g.Expect(err).Should(Succeed())
g.Expect(resp.RType).Should(Equal(ResponseTypeCACHED))
g.Expect(resp.Reason).Should(Equal("CACHED NEGATIVE"))
g.Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError))
// still one call to resolver
g.Expect(m.Calls).Should(HaveLen(1))
}, "500ms").Should(Succeed())
Expect(err).Should(Succeed())
Expect(resp.RType).Should(Equal(ResponseTypeRESOLVED))
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError))
Expect(m.Calls).Should(HaveLen(1))
})
By("second request", func() {
Eventually(func(g Gomega) {
resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA)))
g.Expect(err).Should(Succeed())
g.Expect(resp.RType).Should(Equal(ResponseTypeCACHED))
g.Expect(resp.Reason).Should(Equal("CACHED NEGATIVE"))
g.Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError))
// still one call to resolver
g.Expect(m.Calls).Should(HaveLen(1))
}, "500ms").Should(Succeed())
})
})
})
When("Upstream resolver returns NXDOMAIN without caching", func() {
BeforeEach(func() {
mockAnswer.Rcode = dns.RcodeNameError
sutConfig = config.CachingConfig{
CacheTimeNegative: config.Duration(time.Minute * -1),
}
})
It("response shouldn't be cached", func() {
By("first request", func() {
resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA)))
Expect(err).Should(Succeed())
Expect(resp.RType).Should(Equal(ResponseTypeRESOLVED))
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError))
Expect(m.Calls).Should(HaveLen(1))
})
By("second request", func() {
Eventually(func(g Gomega) {
resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA)))
g.Expect(err).Should(Succeed())
g.Expect(resp.RType).Should(Equal(ResponseTypeRESOLVED))
g.Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError))
g.Expect(m.Calls).Should(HaveLen(2))
}, "500ms").Should(Succeed())
})
})
})
})
When("Upstream resolver returns NXDOMAIN without caching", func() {
BeforeEach(func() {
mockAnswer.Rcode = dns.RcodeNameError
sutConfig = config.CachingConfig{
CacheTimeNegative: config.Duration(time.Minute * -1),
}
})
It("response shouldn't be cached", func() {
By("first request", func() {
resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA)))
Expect(err).Should(Succeed())
Expect(resp.RType).Should(Equal(ResponseTypeRESOLVED))
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError))
Expect(m.Calls).Should(HaveLen(1))
Context("Caching if upstream resolver returns empty result", func() {
When("Upstream resolver returns empty result with caching", func() {
BeforeEach(func() {
mockAnswer.Rcode = dns.RcodeSuccess
mockAnswer.Answer = make([]dns.RR, 0)
})
By("second request", func() {
Eventually(func(g Gomega) {
It("response should be cached", func() {
By("first request", func() {
resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA)))
g.Expect(err).Should(Succeed())
g.Expect(resp.RType).Should(Equal(ResponseTypeRESOLVED))
g.Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError))
g.Expect(m.Calls).Should(HaveLen(2))
}, "500ms").Should(Succeed())
})
})
Expect(err).Should(Succeed())
Expect(resp.RType).Should(Equal(ResponseTypeRESOLVED))
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeSuccess))
Expect(m.Calls).Should(HaveLen(1))
})
By("second request", func() {
Eventually(func(g Gomega) {
resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA)))
g.Expect(err).Should(Succeed())
g.Expect(resp.RType).Should(Equal(ResponseTypeCACHED))
g.Expect(resp.Reason).Should(Equal("CACHED"))
g.Expect(resp.Res.Rcode).Should(Equal(dns.RcodeSuccess))
// still one call to resolver
g.Expect(m.Calls).Should(HaveLen(1))
}, "500ms").Should(Succeed())
})
})
})
})
})
Describe("Not A / AAAA queries should also cached", func() {
Describe("Not A / AAAA queries should also be cached", func() {
When("MX query will be performed", func() {
BeforeEach(func() {
mockAnswer, _ = util.NewMsgWithAnswer("google.de.", 180, dns.Type(dns.TypeMX), "10 alt1.aspmx.l.google.com.")

View File

@ -129,8 +129,8 @@ var _ = Describe("CustomDNSResolver", func() {
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeSuccess))
Expect(resp.Res.Answer).Should(HaveLen(2))
Expect(resp.Res.Answer).Should(ContainElements(
BeDNSRecord("multiple.ips.", dns.TypeA, TTL, "192.168.143.123")),
BeDNSRecord("multiple.ips.", dns.TypeA, TTL, "192.168.143.125"))
BeDNSRecord("multiple.ips.", dns.TypeA, TTL, "192.168.143.123"),
BeDNSRecord("multiple.ips.", dns.TypeA, TTL, "192.168.143.125")))
// will not delegate to next resolver
m.AssertNotCalled(GinkgoT(), "Resolve", mock.Anything)
})
@ -157,9 +157,9 @@ var _ = Describe("CustomDNSResolver", func() {
Expect(resp.Res.Answer).Should(HaveLen(2))
Expect(resp.Res.Answer).Should(ContainElements(
BeDNSRecord("4.3.3.7.0.7.3.0.e.2.a.8.0.0.0.0.0.0.0.0.3.a.5.8.8.b.d.0.1.0.0.2.ip6.arpa.",
dns.TypePTR, TTL, "ip6.domain.")),
dns.TypePTR, TTL, "ip6.domain."),
BeDNSRecord("4.3.3.7.0.7.3.0.e.2.a.8.0.0.0.0.0.0.0.0.3.a.5.8.8.b.d.0.1.0.0.2.ip6.arpa.",
dns.TypePTR, TTL, "multiple.ips."))
dns.TypePTR, TTL, "multiple.ips.")))
// will not delegate to next resolver
m.AssertNotCalled(GinkgoT(), "Resolve", mock.Anything)
})

82
resolver/ede_resolver.go Normal file
View File

@ -0,0 +1,82 @@
package resolver
import (
"github.com/0xERR0R/blocky/config"
"github.com/0xERR0R/blocky/model"
"github.com/miekg/dns"
)
type EdeResolver struct {
NextResolver
config config.EdeConfig
}
func NewEdeResolver(cfg config.EdeConfig) ChainedResolver {
return &EdeResolver{
config: cfg,
}
}
func (r *EdeResolver) Resolve(request *model.Request) (*model.Response, error) {
resp, err := r.next.Resolve(request)
if r.config.Enable {
addExtraReasoning(resp)
}
return resp, err
}
func (r *EdeResolver) Configuration() (result []string) {
if r.config.Enable {
result = []string{"activated"}
} else {
result = []string{"deactivated"}
}
return result
}
func addExtraReasoning(res *model.Response) {
// dns.ExtendedErrorCodeOther seams broken in some clients
infocode := convertToExtendedErrorCode(res.RType)
if infocode > 0 {
opt := new(dns.OPT)
opt.Hdr.Name = "."
opt.Hdr.Rrtype = dns.TypeOPT
opt.Option = append(opt.Option, convertExtendedError(res, infocode))
res.Res.Extra = append(res.Res.Extra, opt)
}
}
func convertExtendedError(input *model.Response, infocode uint16) *dns.EDNS0_EDE {
return &dns.EDNS0_EDE{
InfoCode: infocode,
ExtraText: input.Reason,
}
}
func convertToExtendedErrorCode(input model.ResponseType) uint16 {
switch input {
case model.ResponseTypeRESOLVED:
return dns.ExtendedErrorCodeOther
case model.ResponseTypeCACHED:
return dns.ExtendedErrorCodeCachedError
case model.ResponseTypeCONDITIONAL:
return dns.ExtendedErrorCodeForgedAnswer
case model.ResponseTypeCUSTOMDNS:
return dns.ExtendedErrorCodeForgedAnswer
case model.ResponseTypeHOSTSFILE:
return dns.ExtendedErrorCodeForgedAnswer
case model.ResponseTypeNOTFQDN:
return dns.ExtendedErrorCodeBlocked
case model.ResponseTypeBLOCKED:
return dns.ExtendedErrorCodeBlocked
case model.ResponseTypeFILTERED:
return dns.ExtendedErrorCodeFiltered
case model.ResponseTypeSPECIAL:
return dns.ExtendedErrorCodeFiltered
default:
return dns.ExtendedErrorCodeOther
}
}

View File

@ -0,0 +1,87 @@
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("EdeResolver", func() {
var (
sut *EdeResolver
sutConfig config.EdeConfig
m *MockResolver
mockAnswer *dns.Msg
)
BeforeEach(func() {
mockAnswer = new(dns.Msg)
})
JustBeforeEach(func() {
m = &MockResolver{}
m.On("Resolve", mock.Anything).Return(&model.Response{
Res: mockAnswer,
RType: model.ResponseTypeCUSTOMDNS,
Reason: "Test",
}, nil)
sut = NewEdeResolver(sutConfig).(*EdeResolver)
sut.Next(m)
})
When("Ede is disabled", func() {
BeforeEach(func() {
sutConfig = config.EdeConfig{
Enable: false,
}
})
It("Shouldn't add EDE information", func() {
resp, err := sut.Resolve(newRequest("example.com", dns.Type(dns.TypeA)))
Expect(err).Should(Succeed())
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeSuccess))
Expect(resp.RType).Should(Equal(model.ResponseTypeCUSTOMDNS))
Expect(resp.Res.Answer).Should(BeEmpty())
Expect(resp.Res.Extra).Should(BeEmpty())
// delegated to next resolver
Expect(m.Calls).Should(HaveLen(1))
})
It("Configure should output deactivated", func() {
c := sut.Configuration()
Expect(c).Should(HaveLen(1))
Expect(c[0]).Should(Equal("deactivated"))
})
})
When("Ede is enabled", func() {
BeforeEach(func() {
sutConfig = config.EdeConfig{
Enable: true,
}
})
It("Should add EDE information", func() {
resp, err := sut.Resolve(newRequest("example.com", dns.Type(dns.TypeA)))
Expect(err).Should(Succeed())
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeSuccess))
Expect(resp.RType).Should(Equal(model.ResponseTypeCUSTOMDNS))
Expect(resp.Res.Answer).Should(BeEmpty())
Expect(resp.Res.Extra).Should(HaveLen(1))
opt, ok := resp.Res.Extra[0].(*dns.OPT)
Expect(ok).Should(BeTrue())
Expect(opt).ShouldNot(BeNil())
ede, ok := opt.Option[0].(*dns.EDNS0_EDE)
Expect(ok).Should(BeTrue())
Expect(ede.InfoCode).Should(Equal(dns.ExtendedErrorCodeForgedAnswer))
Expect(ede.ExtraText).Should(Equal("Test"))
})
It("Configure should output activated", func() {
c := sut.Configuration()
Expect(c).Should(HaveLen(1))
Expect(c[0]).Should(Equal("activated"))
})
})
})

View File

@ -0,0 +1,45 @@
package resolver
import (
"strings"
"github.com/0xERR0R/blocky/config"
"github.com/0xERR0R/blocky/model"
"github.com/0xERR0R/blocky/util"
"github.com/miekg/dns"
)
type FqdnOnlyResolver struct {
NextResolver
enabled bool
}
func NewFqdnOnlyResolver(cfg config.Config) ChainedResolver {
return &FqdnOnlyResolver{
enabled: cfg.FqdnOnly,
}
}
func (r *FqdnOnlyResolver) Resolve(request *model.Request) (*model.Response, error) {
if r.enabled {
domainFromQuestion := util.ExtractDomain(request.Req.Question[0])
if !strings.Contains(domainFromQuestion, ".") {
response := new(dns.Msg)
response.Rcode = dns.RcodeNameError
return &model.Response{Res: response, RType: model.ResponseTypeNOTFQDN, Reason: "NOTFQDN"}, nil
}
}
return r.next.Resolve(request)
}
func (r *FqdnOnlyResolver) Configuration() (result []string) {
if r.enabled {
result = []string{"activated"}
} else {
result = []string{"deactivated"}
}
return result
}

View File

@ -0,0 +1,97 @@
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("FqdnOnlyResolver", func() {
var (
sut *FqdnOnlyResolver
sutConfig config.Config
m *MockResolver
mockAnswer *dns.Msg
)
BeforeEach(func() {
mockAnswer = new(dns.Msg)
})
JustBeforeEach(func() {
sut = NewFqdnOnlyResolver(sutConfig).(*FqdnOnlyResolver)
m = &MockResolver{}
m.On("Resolve", mock.Anything).Return(&Response{Res: mockAnswer}, nil)
sut.Next(m)
})
When("Fqdn only is activated", func() {
BeforeEach(func() {
sutConfig = config.Config{
FqdnOnly: true,
}
})
It("Should delegate to next resolver if request query is fqdn", func() {
resp, err := sut.Resolve(newRequest("example.com", dns.Type(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 NXDOMAIN if request query is not fqdn", func() {
resp, err := sut.Resolve(newRequest("example", dns.Type(dns.TypeAAAA)))
Expect(err).Should(Succeed())
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError))
Expect(resp.RType).Should(Equal(ResponseTypeNOTFQDN))
Expect(resp.Res.Answer).Should(BeEmpty())
// no call of next resolver
Expect(m.Calls).Should(BeZero())
})
It("Configure should output activated", func() {
c := sut.Configuration()
Expect(c).Should(HaveLen(1))
Expect(c[0]).Should(Equal("activated"))
})
})
When("Fqdn only is deactivated", func() {
BeforeEach(func() {
sutConfig = config.Config{
FqdnOnly: false,
}
})
It("Should delegate to next resolver if request query is fqdn", func() {
resp, err := sut.Resolve(newRequest("example.com", dns.Type(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 delegate to next resolver if request query is not fqdn", func() {
resp, err := sut.Resolve(newRequest("example", dns.Type(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(BeEmpty())
// delegated to next resolver
Expect(m.Calls).Should(HaveLen(1))
})
It("Configure should output deactivated", func() {
c := sut.Configuration()
Expect(c).Should(HaveLen(1))
Expect(c[0]).Should(Equal("deactivated"))
})
})
})

View File

@ -14,16 +14,23 @@ import (
"github.com/sirupsen/logrus"
)
//nolint:gochecknoglobals
var (
_, loopback4, _ = net.ParseCIDR("127.0.0.0/8")
loopback6 = net.ParseIP("::1")
)
const (
hostsFileResolverLogger = "hosts_file_resolver"
)
type HostsFileResolver struct {
NextResolver
HostsFilePath string
hosts []host
ttl uint32
refreshPeriod time.Duration
HostsFilePath string
hosts []host
ttl uint32
refreshPeriod time.Duration
filterLoopback bool
}
func (r *HostsFileResolver) handleReverseDNS(request *model.Request) *model.Response {
@ -119,6 +126,7 @@ func (r *HostsFileResolver) Configuration() (result []string) {
result = append(result, fmt.Sprintf("hosts file path: %s", r.HostsFilePath))
result = append(result, fmt.Sprintf("hosts TTL: %d", r.ttl))
result = append(result, fmt.Sprintf("hosts refresh period: %s", r.refreshPeriod.String()))
result = append(result, fmt.Sprintf("filter loopback addresses: %t", r.filterLoopback))
} else {
result = []string{"deactivated"}
}
@ -128,9 +136,10 @@ func (r *HostsFileResolver) Configuration() (result []string) {
func NewHostsFileResolver(cfg config.HostsFileConfig) ChainedResolver {
r := HostsFileResolver{
HostsFilePath: cfg.Filepath,
ttl: uint32(time.Duration(cfg.HostsTTL).Seconds()),
refreshPeriod: time.Duration(cfg.RefreshPeriod),
HostsFilePath: cfg.Filepath,
ttl: uint32(time.Duration(cfg.HostsTTL).Seconds()),
refreshPeriod: time.Duration(cfg.RefreshPeriod),
filterLoopback: cfg.FilterLoopback,
}
if err := r.parseHostsFile(); err != nil {
@ -150,6 +159,7 @@ type host struct {
Aliases []string
}
// nolint:funlen
func (r *HostsFileResolver) parseHostsFile() error {
const minColumnCount = 2
@ -197,6 +207,11 @@ func (r *HostsFileResolver) parseHostsFile() error {
h.IP = net.ParseIP(fields[0])
h.Hostname = fields[1]
// Check if loopback
if r.filterLoopback && (loopback4.Contains(h.IP) || loopback6.Equal(h.IP)) {
continue
}
if len(fields) > minColumnCount {
for i := 2; i < len(fields); i++ {
h.Aliases = append(h.Aliases, fields[i])

View File

@ -16,19 +16,29 @@ import (
var _ = Describe("HostsFileResolver", func() {
var (
sut *HostsFileResolver
m *MockResolver
err error
resp *Response
sut *HostsFileResolver
m *MockResolver
err error
resp *Response
tmpDir *TmpFolder
tmpFile *TmpFile
)
TTL := uint32(time.Now().Second())
BeforeEach(func() {
tmpDir = NewTmpFolder("HostsFileResolver")
Expect(tmpDir.Error).Should(Succeed())
DeferCleanup(tmpDir.Clean)
tmpFile = writeHostFile(tmpDir)
Expect(tmpFile.Error).Should(Succeed())
cfg := config.HostsFileConfig{
Filepath: "../testdata/hosts.txt",
HostsTTL: config.Duration(time.Duration(TTL) * time.Second),
RefreshPeriod: config.Duration(30 * time.Minute),
Filepath: tmpFile.Path,
HostsTTL: config.Duration(time.Duration(TTL) * time.Second),
RefreshPeriod: config.Duration(30 * time.Minute),
FilterLoopback: true,
}
sut = NewHostsFileResolver(cfg).(*HostsFileResolver)
m = &MockResolver{}
@ -79,8 +89,8 @@ var _ = Describe("HostsFileResolver", func() {
When("Hosts file can be located", func() {
It("should parse it successfully", func() {
Expect(sut).Should(Not(BeNil()))
Expect(sut.hosts).Should(HaveLen(7))
Expect(sut).ShouldNot(BeNil())
Expect(sut.hosts).Should(HaveLen(4))
})
})
@ -165,7 +175,7 @@ var _ = Describe("HostsFileResolver", func() {
When("hosts file is provided", func() {
It("should return configuration", func() {
c := sut.Configuration()
Expect(c).Should(HaveLen(3))
Expect(c).Should(HaveLen(4))
})
})
@ -192,3 +202,21 @@ var _ = Describe("HostsFileResolver", func() {
})
})
})
func writeHostFile(tmpDir *TmpFolder) *TmpFile {
return tmpDir.CreateStringFile("hosts.txt",
"# Random comment",
"127.0.0.1 localhost",
"127.0.1.1 localhost2 localhost2.local.lan",
"::1 localhost",
"# Two empty lines to follow",
"",
"",
"faaf:faaf:faaf:faaf::1 ipv6host ipv6host.local.lan",
"192.168.2.1 ipv4host ipv4host.local.lan",
"10.0.0.1 router0 router1 router2",
"10.0.0.2 router3 # Another comment",
"10.0.0.3 # Invalid entry",
"300.300.300.300 invalid4 # Invalid IPv4",
"abcd:efgh:ijkl::1 invalid6 # Invalud IPv6")
}

View File

@ -1,7 +1,7 @@
package resolver
import (
"io/ioutil"
"io"
"net"
"net/http"
"net/http/httptest"
@ -101,7 +101,7 @@ func TestBootstrap(response *dns.Msg) *Bootstrap {
func TestDOHUpstream(fn func(request *dns.Msg) (response *dns.Msg),
reqFn ...func(w http.ResponseWriter)) config.Upstream {
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
body, err := io.ReadAll(r.Body)
util.FatalOnError("can't read request: ", err)

View File

@ -10,6 +10,7 @@ import (
"github.com/0xERR0R/blocky/config"
"github.com/0xERR0R/blocky/model"
"github.com/0xERR0R/blocky/util"
"github.com/miekg/dns"
"github.com/mroth/weightedrand"
"github.com/sirupsen/logrus"
@ -36,23 +37,56 @@ type requestResponse struct {
err error
}
// testResolver sends a test query to verify the resolver is reachable and working
func testResolver(r *UpstreamResolver) error {
request := newRequest("github.com.", dns.Type(dns.TypeA))
resp, err := r.Resolve(request)
if err != nil || resp.RType != model.ResponseTypeRESOLVED {
return fmt.Errorf("test resolve of upstream server failed: %w", err)
}
return nil
}
// NewParallelBestResolver creates new resolver instance
func NewParallelBestResolver(upstreamResolvers map[string][]config.Upstream, bootstrap *Bootstrap) (Resolver, error) {
s := make(map[string][]*upstreamResolverStatus, len(upstreamResolvers))
logger := logger("parallel resolver")
s := make(map[string][]*upstreamResolverStatus)
for name, res := range upstreamResolvers {
resolvers := make([]*upstreamResolverStatus, len(res))
var resolvers []*upstreamResolverStatus
for i, u := range res {
var errResolvers int
for _, u := range res {
r, err := NewUpstreamResolver(u, bootstrap)
if err != nil {
return nil, err
logger.Warnf("upstream group %s: %v", name, err)
errResolvers++
continue
}
resolvers[i] = &upstreamResolverStatus{
if bootstrap != skipUpstreamCheck {
err = testResolver(r)
if err != nil {
logger.Warn(err)
errResolvers++
}
}
resolver := &upstreamResolverStatus{
resolver: r,
}
resolvers[i].lastErrorTime.Store(time.Unix(0, 0))
resolver.lastErrorTime.Store(time.Unix(0, 0))
resolvers = append(resolvers, resolver)
}
if bootstrap != skipUpstreamCheck {
if bootstrap.startVerifyUpstream && errResolvers == len(res) {
return nil, fmt.Errorf("unable to reach any DNS resolvers configured for resolver group %s", name)
}
}
s[name] = resolvers

View File

@ -26,6 +26,67 @@ var _ = Describe("ParallelBestResolver", Label("parallelBestResolver"), func() {
})
})
Describe("Some default upstream resolvers cannot be reached", func() {
It("should start normally", func() {
skipUpstreamCheck.startVerifyUpstream = true
mockUpstream := NewMockUDPUpstreamServer().WithAnswerFn(func(request *dns.Msg) (response *dns.Msg) {
response, _ = util.NewMsgWithAnswer(request.Question[0].Name, 123, dns.Type(dns.TypeA), "123.124.122.122")
return
})
defer mockUpstream.Close()
upstream := map[string][]config.Upstream{
upstreamDefaultCfgName: {
config.Upstream{
Host: "wrong",
},
mockUpstream.Start(),
},
}
_, err := NewParallelBestResolver(upstream, skipUpstreamCheck)
Expect(err).Should(Not(HaveOccurred()))
})
})
Describe("All default upstream resolvers cannot be reached", func() {
var (
upstream map[string][]config.Upstream
b *Bootstrap
)
BeforeEach(func() {
b = TestBootstrap(&dns.Msg{MsgHdr: dns.MsgHdr{Rcode: dns.RcodeServerFailure}})
upstream = map[string][]config.Upstream{
upstreamDefaultCfgName: {
config.Upstream{
Host: "wrong",
},
config.Upstream{
Host: "127.0.0.2",
},
},
}
})
It("should fail to start if strict checking is enabled", func() {
b.startVerifyUpstream = true
_, err := NewParallelBestResolver(upstream, b)
Expect(err).Should(HaveOccurred())
})
It("should start if strict checking is disabled", func() {
b.startVerifyUpstream = false
_, err := NewParallelBestResolver(upstream, b)
Expect(err).Should(Not(HaveOccurred()))
})
})
Describe("Resolving result from fastest upstream resolver", func() {
var (
sut Resolver
@ -310,6 +371,8 @@ var _ = Describe("ParallelBestResolver", Label("parallelBestResolver"), func() {
sut Resolver
)
BeforeEach(func() {
config.GetConfig().StartVerifyUpstream = false
sut, _ = NewParallelBestResolver(map[string][]config.Upstream{upstreamDefaultCfgName: {
{Host: "host1"},
{Host: "host2"},

View File

@ -6,11 +6,10 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"time"
"github.com/0xERR0R/blocky/helpertest"
"github.com/0xERR0R/blocky/querylog"
"github.com/0xERR0R/blocky/config"
@ -30,7 +29,7 @@ type SlowMockWriter struct {
func (m *SlowMockWriter) Write(entry *querylog.LogEntry) {
m.entries = append(m.entries, entry)
time.Sleep(time.Millisecond)
time.Sleep(time.Second)
}
func (m *SlowMockWriter) CleanUp() {
@ -43,29 +42,26 @@ var _ = Describe("QueryLoggingResolver", func() {
err error
resp *Response
m *MockResolver
tmpDir string
tmpDir *helpertest.TmpFolder
mockAnswer *dns.Msg
)
BeforeEach(func() {
mockAnswer = new(dns.Msg)
tmpDir, err = ioutil.TempDir("", "queryLoggingResolver")
Expect(err).Should(Succeed())
tmpDir = helpertest.NewTmpFolder("queryLoggingResolver")
Expect(tmpDir.Error).Should(Succeed())
DeferCleanup(tmpDir.Clean)
})
JustBeforeEach(func() {
sut = NewQueryLoggingResolver(sutConfig).(*QueryLoggingResolver)
DeferCleanup(func() { close(sut.logChan) })
m = &MockResolver{}
m.On("Resolve", mock.Anything).Return(&Response{Res: mockAnswer, Reason: "reason"}, nil)
sut.Next(m)
})
AfterEach(func() {
Expect(err).Should(Succeed())
_ = os.RemoveAll(tmpDir)
})
Describe("Process request", func() {
When("Resolver has no configuration", func() {
BeforeEach(func() {
sutConfig = config.QueryLogConfig{
@ -83,7 +79,7 @@ var _ = Describe("QueryLoggingResolver", func() {
When("Configuration with logging per client", func() {
BeforeEach(func() {
sutConfig = config.QueryLogConfig{
Target: tmpDir,
Target: tmpDir.Path,
Type: config.QueryLogTypeCsvClient,
CreationAttempts: 1,
CreationCooldown: config.Duration(time.Millisecond),
@ -106,7 +102,8 @@ var _ = Describe("QueryLoggingResolver", func() {
By("check log for client1", func() {
Eventually(func(g Gomega) {
csvLines, err := readCsv(filepath.Join(tmpDir, fmt.Sprintf("%s_client1.log", time.Now().Format("2006-01-02"))))
csvLines, err := readCsv(tmpDir.JoinPath(
fmt.Sprintf("%s_client1.log", time.Now().Format("2006-01-02"))))
g.Expect(err).Should(Succeed())
g.Expect(csvLines).Should(Not(BeEmpty()))
@ -121,7 +118,7 @@ var _ = Describe("QueryLoggingResolver", func() {
By("check log for client2", func() {
Eventually(func(g Gomega) {
csvLines, err := readCsv(filepath.Join(tmpDir,
csvLines, err := readCsv(tmpDir.JoinPath(
fmt.Sprintf("%s_cl_ient2_test.log", time.Now().Format("2006-01-02"))))
g.Expect(err).Should(Succeed())
@ -138,7 +135,7 @@ var _ = Describe("QueryLoggingResolver", func() {
When("Configuration with logging in one file for all clients", func() {
BeforeEach(func() {
sutConfig = config.QueryLogConfig{
Target: tmpDir,
Target: tmpDir.Path,
Type: config.QueryLogTypeCsv,
CreationAttempts: 1,
CreationCooldown: config.Duration(time.Millisecond),
@ -159,7 +156,8 @@ var _ = Describe("QueryLoggingResolver", func() {
By("check log", func() {
Eventually(func(g Gomega) {
csvLines, err := readCsv(filepath.Join(tmpDir, fmt.Sprintf("%s_ALL.log", time.Now().Format("2006-01-02"))))
csvLines, err := readCsv(tmpDir.JoinPath(
fmt.Sprintf("%s_ALL.log", time.Now().Format("2006-01-02"))))
g.Expect(err).Should(Succeed())
g.Expect(csvLines).Should(HaveLen(2))
@ -196,16 +194,13 @@ var _ = Describe("QueryLoggingResolver", func() {
mockWriter := &SlowMockWriter{}
sut.writer = mockWriter
// run 10000 requests
for i := 0; i < 10000; i++ {
resp, err = sut.Resolve(newRequestWithClient("example.com.", dns.Type(dns.TypeA), "192.168.178.25", "client1"))
Expect(err).Should(Succeed())
}
Eventually(func() int {
_, ierr := sut.Resolve(newRequestWithClient("example.com.", dns.Type(dns.TypeA), "192.168.178.25", "client1"))
Expect(ierr).Should(Succeed())
// log channel is full
Expect(sut.logChan).Should(Not(BeEmpty()))
return len(sut.logChan)
}, "20s", "1µs").Should(Equal(cap(sut.logChan)))
})
})
})
@ -213,7 +208,7 @@ var _ = Describe("QueryLoggingResolver", func() {
When("resolver is enabled", func() {
BeforeEach(func() {
sutConfig = config.QueryLogConfig{
Target: tmpDir,
Target: tmpDir.Path,
Type: config.QueryLogTypeCsvClient,
LogRetentionDays: 0,
CreationAttempts: 1,
@ -229,81 +224,81 @@ var _ = Describe("QueryLoggingResolver", func() {
Describe("Clean up of query log directory", func() {
When("fallback logger is enabled, log retention is enabled", func() {
It("should do nothing", func() {
sut := NewQueryLoggingResolver(config.QueryLogConfig{
BeforeEach(func() {
sutConfig = config.QueryLogConfig{
LogRetentionDays: 7,
Type: config.QueryLogTypeConsole,
CreationAttempts: 1,
CreationCooldown: config.Duration(time.Millisecond),
}).(*QueryLoggingResolver)
}
})
It("should do nothing", func() {
sut.doCleanUp()
})
})
When("log directory contains old files", func() {
It("should remove files older than defined log retention", func() {
// create 2 files, 7 and 8 days old
dateBefore7Days := time.Now().AddDate(0, 0, -7)
dateBefore8Days := time.Now().AddDate(0, 0, -8)
f1, err := os.Create(filepath.Join(tmpDir, fmt.Sprintf("%s-test.log", dateBefore7Days.Format("2006-01-02"))))
Expect(err).Should(Succeed())
f2, err := os.Create(filepath.Join(tmpDir, fmt.Sprintf("%s-test.log", dateBefore8Days.Format("2006-01-02"))))
Expect(err).Should(Succeed())
sut := NewQueryLoggingResolver(config.QueryLogConfig{
Target: tmpDir,
BeforeEach(func() {
sutConfig = config.QueryLogConfig{
Target: tmpDir.Path,
Type: config.QueryLogTypeCsv,
LogRetentionDays: 7,
CreationAttempts: 1,
CreationCooldown: config.Duration(time.Millisecond),
})
}
})
It("should remove files older than defined log retention", func() {
// create 2 files, 7 and 8 days old
dateBefore7Days := time.Now().AddDate(0, 0, -7)
dateBefore9Days := time.Now().AddDate(0, 0, -9)
sut.(*QueryLoggingResolver).doCleanUp()
f1 := tmpDir.CreateEmptyFile(fmt.Sprintf("%s-test.log", dateBefore7Days.Format("2006-01-02")))
Expect(f1.Error).Should(Succeed())
// file 1 exist
_, err = os.Stat(f1.Name())
Expect(err).Should(Succeed())
f2 := tmpDir.CreateEmptyFile(fmt.Sprintf("%s-test.log", dateBefore9Days.Format("2006-01-02")))
Expect(f2.Error).Should(Succeed())
// file 2 was deleted
_, err = os.Stat(f2.Name())
Expect(err).Should(HaveOccurred())
Expect(os.IsNotExist(err)).Should(BeTrue())
sut.doCleanUp()
Eventually(func(g Gomega) {
// file 1 exist
g.Expect(f1.Stat()).Should(Succeed())
// file 2 was deleted
ierr2 := f2.Stat()
g.Expect(ierr2).Should(HaveOccurred())
g.Expect(os.IsNotExist(ierr2)).Should(BeTrue())
}).Should(Succeed())
})
})
})
})
var _ = Describe("Wrong target configuration", func() {
When("mysql database path is wrong", func() {
It("should use fallback", func() {
sutConfig := config.QueryLogConfig{
Target: "dummy",
Type: config.QueryLogTypeMysql,
CreationAttempts: 1,
CreationCooldown: config.Duration(time.Millisecond),
}
resolver := NewQueryLoggingResolver(sutConfig)
loggingResolver := resolver.(*QueryLoggingResolver)
Expect(loggingResolver.logType).Should(Equal(config.QueryLogTypeConsole))
Describe("Wrong target configuration", func() {
When("mysql database path is wrong", func() {
BeforeEach(func() {
sutConfig = config.QueryLogConfig{
Target: "dummy",
Type: config.QueryLogTypeMysql,
CreationAttempts: 1,
CreationCooldown: config.Duration(time.Millisecond),
}
})
It("should use fallback", func() {
Expect(sut.logType).Should(Equal(config.QueryLogTypeConsole))
})
})
})
When("postgresql database path is wrong", func() {
It("should use fallback", func() {
sutConfig := config.QueryLogConfig{
Target: "dummy",
Type: config.QueryLogTypePostgresql,
CreationAttempts: 1,
CreationCooldown: config.Duration(time.Millisecond),
}
resolver := NewQueryLoggingResolver(sutConfig)
loggingResolver := resolver.(*QueryLoggingResolver)
Expect(loggingResolver.logType).Should(Equal(config.QueryLogTypeConsole))
When("postgresql database path is wrong", func() {
BeforeEach(func() {
sutConfig = config.QueryLogConfig{
Target: "dummy",
Type: config.QueryLogTypePostgresql,
CreationAttempts: 1,
CreationCooldown: config.Duration(time.Millisecond),
}
})
It("should use fallback", func() {
Expect(sut.logType).Should(Equal(config.QueryLogTypeConsole))
})
})
})
})
@ -315,6 +310,7 @@ func readCsv(file string) ([][]string, error) {
if err != nil {
return nil, err
}
defer csvFile.Close()
reader := csv.NewReader(bufio.NewReader(csvFile))
reader.Comma = '\t'

View File

@ -40,6 +40,23 @@ func newRequestWithClient(question string, rType dns.Type, ip string, clientName
}
}
// newResponseMsg creates a new dns.Msg as response for a request
func newResponseMsg(request *model.Request) *dns.Msg {
response := new(dns.Msg)
response.SetReply(request.Req)
return response
}
// returnResponseModel wrapps a dns.Msg into a model.Response
func returnResponseModel(response *dns.Msg, rtype model.ResponseType, reason string) (*model.Response, error) {
return &model.Response{
Res: response,
RType: rtype,
Reason: reason,
}, nil
}
func newRequestWithClientID(question string, rType dns.Type, ip string, requestClientID string) *model.Request {
return &model.Request{
ClientIP: net.ParseIP(ip),

View File

@ -18,8 +18,9 @@ import (
// yield a result, the normal resolving is continued.
type RewriterResolver struct {
NextResolver
rewrite map[string]string
inner Resolver
rewrite map[string]string
inner Resolver
fallbackUpstream bool
}
func NewRewriterResolver(cfg config.RewriteConfig, inner ChainedResolver) ChainedResolver {
@ -34,8 +35,9 @@ func NewRewriterResolver(cfg config.RewriteConfig, inner ChainedResolver) Chaine
inner.Next(NewNoOpResolver())
return &RewriterResolver{
rewrite: cfg.Rewrite,
inner: inner,
rewrite: cfg.Rewrite,
inner: inner,
fallbackUpstream: cfg.FallbackUpstream,
}
}
@ -70,13 +72,23 @@ func (r *RewriterResolver) Resolve(request *model.Request) (*model.Response, err
logger.WithField("resolver", Name(r.inner)).Trace("go to inner resolver")
response, err := r.inner.Resolve(request)
if err != nil {
return response, err
}
// Test for error after checking for fallbackUpstream
// Revert the request: must be done before calling r.next
request.Req = original
fallbackCondition := err != nil || (response != NoResponse && response.Res.Answer == nil)
if r.fallbackUpstream && fallbackCondition {
// Inner resolver had no answer, configuration requests fallback, continue with the normal chain
logger.WithField("next_resolver", Name(r.next)).Trace("fallback to next resolver")
return r.next.Resolve(request)
}
if err != nil {
return response, err
}
if response == NoResponse {
// Inner resolver had no response, continue with the normal chain
logger.WithField("next_resolver", Name(r.next)).Trace("go to next resolver")

View File

@ -11,6 +11,11 @@ import (
"github.com/stretchr/testify/mock"
)
const (
sampleOriginal = "test.original."
sampleRewritten = "test.rewritten."
)
var _ = Describe("RewriterResolver", func() {
var (
sut ChainedResolver
@ -53,6 +58,11 @@ var _ = Describe("RewriterResolver", func() {
When("has rewrite", func() {
var request *model.Request
var expectNilAnswer bool
BeforeEach(func() {
expectNilAnswer = false
})
AfterEach(func() {
request = newRequest(fqdnOriginal, dns.Type(dns.TypeA))
@ -80,13 +90,17 @@ var _ = Describe("RewriterResolver", func() {
Expect(err).Should(Succeed())
if resp != mNextResponse {
Expect(resp.Res.Question[0].Name).Should(Equal(fqdnOriginal))
Expect(resp.Res.Answer[0].Header().Name).Should(Equal(fqdnOriginal))
if expectNilAnswer {
Expect(resp.Res.Answer).Should(BeEmpty())
} else {
Expect(resp.Res.Answer[0].Header().Name).Should(Equal(fqdnOriginal))
}
}
})
It("should modify names", func() {
fqdnOriginal = "test.original."
fqdnRewritten = "test.rewritten."
fqdnOriginal = sampleOriginal
fqdnRewritten = sampleRewritten
})
It("should modify subdomains", func() {
@ -105,8 +119,9 @@ var _ = Describe("RewriterResolver", func() {
})
It("should call next resolver", func() {
fqdnOriginal = "test.original."
fqdnRewritten = "test.rewritten."
fqdnOriginal = sampleOriginal
fqdnRewritten = sampleRewritten
expectNilAnswer = true
// Make inner call the NoOpResolver
mInner.ResolveFn = func(req *model.Request) (*model.Response, error) {
@ -126,6 +141,54 @@ var _ = Describe("RewriterResolver", func() {
return mNextResponse, nil
}
})
It("should not call next resolver", func() {
fqdnOriginal = sampleOriginal
fqdnRewritten = sampleRewritten
expectNilAnswer = true
// Make inner return a nil Answer but not an empty Response
mInner.ResolveFn = func(req *model.Request) (*model.Response, error) {
Expect(req).Should(Equal(request))
// Inner should see fqdnRewritten
Expect(req.Req.Question[0].Name).Should(Equal(fqdnRewritten))
return &model.Response{Res: &dns.Msg{Question: req.Req.Question, Answer: nil}}, nil
}
// Resolver after RewriterResolver should not be called `fqdnOriginal`
mNext.AssertNotCalled(GinkgoT(), "Resolve", mock.Anything)
})
When("has fallbackUpstream", func() {
BeforeEach(func() {
sutConfig.FallbackUpstream = true
})
It("should call next resolver", func() {
fqdnOriginal = sampleOriginal
fqdnRewritten = sampleRewritten
// Make inner return a nil Answer but not an empty Response
mInner.ResolveFn = func(req *model.Request) (*model.Response, error) {
Expect(req).Should(Equal(request))
// Inner should see fqdnRewritten
Expect(req.Req.Question[0].Name).Should(Equal(fqdnRewritten))
return &model.Response{Res: &dns.Msg{Question: req.Req.Question, Answer: nil}}, nil
}
// Resolver after RewriterResolver should see `fqdnOriginal`
mNext.On("Resolve", mock.Anything)
mNext.ResolveFn = func(req *model.Request) (*model.Response, error) {
Expect(req.Req.Question[0].Name).Should(Equal(fqdnOriginal))
return mNextResponse, nil
}
})
})
})
Describe("Configuration output", func() {

154
resolver/sudn_resolver.go Normal file
View File

@ -0,0 +1,154 @@
package resolver
import (
"fmt"
"net"
"strings"
"github.com/0xERR0R/blocky/model"
"github.com/miekg/dns"
)
const (
sudnTest = "test."
sudnInvalid = "invalid."
sudnLocalhost = "localhost."
mdnsLocal = "local."
)
func sudnArpaSlice() []string {
return []string{
"10.in-addr.arpa.",
"21.172.in-addr.arpa.",
"26.172.in-addr.arpa.",
"16.172.in-addr.arpa.",
"22.172.in-addr.arpa.",
"27.172.in-addr.arpa.",
"17.172.in-addr.arpa.",
"30.172.in-addr.arpa.",
"28.172.in-addr.arpa.",
"18.172.in-addr.arpa.",
"23.172.in-addr.arpa.",
"29.172.in-addr.arpa.",
"19.172.in-addr.arpa.",
"24.172.in-addr.arpa.",
"31.172.in-addr.arpa.",
"20.172.in-addr.arpa.",
"25.172.in-addr.arpa.",
"168.192.in-addr.arpa.",
}
}
type defaultIPs struct {
loopbackV4 net.IP
loopbackV6 net.IP
}
type SpecialUseDomainNamesResolver struct {
NextResolver
defaults *defaultIPs
}
func NewSpecialUseDomainNamesResolver() ChainedResolver {
return &SpecialUseDomainNamesResolver{
defaults: &defaultIPs{
loopbackV4: net.ParseIP("127.0.0.1"),
loopbackV6: net.IPv6loopback,
},
}
}
func (r *SpecialUseDomainNamesResolver) Resolve(request *model.Request) (*model.Response, error) {
// RFC 6761 - negative
if r.isSpecial(request, sudnArpaSlice()...) ||
r.isSpecial(request, sudnInvalid) ||
r.isSpecial(request, sudnTest) {
return r.negativeResponse(request)
}
// RFC 6761 - switched
if r.isSpecial(request, sudnLocalhost) {
return r.responseSwitch(request, sudnLocalhost, r.defaults.loopbackV4, r.defaults.loopbackV6)
}
// RFC 6762 - negative
if r.isSpecial(request, mdnsLocal) {
return r.negativeResponse(request)
}
return r.next.Resolve(request)
}
// RFC 6761 & 6762 are always active
func (r *SpecialUseDomainNamesResolver) Configuration() []string {
return []string{}
}
func (r *SpecialUseDomainNamesResolver) isSpecial(request *model.Request, names ...string) bool {
domainFromQuestion := request.Req.Question[0].Name
for _, n := range names {
if domainFromQuestion == n ||
strings.HasSuffix(domainFromQuestion, fmt.Sprintf(".%s", n)) {
return true
}
}
return false
}
func (r *SpecialUseDomainNamesResolver) responseSwitch(request *model.Request,
name string, ipV4, ipV6 net.IP) (*model.Response, error) {
qtype := request.Req.Question[0].Qtype
switch qtype {
case dns.TypeA:
return r.positiveResponse(request, name, dns.TypeA, ipV4)
case dns.TypeAAAA:
return r.positiveResponse(request, name, dns.TypeAAAA, ipV6)
default:
return r.negativeResponse(request)
}
}
func (r *SpecialUseDomainNamesResolver) positiveResponse(request *model.Request,
name string, rtype uint16, ip net.IP) (*model.Response, error) {
response := newResponseMsg(request)
response.Rcode = dns.RcodeSuccess
hdr := dns.RR_Header{
Name: name,
Rrtype: rtype,
Class: dns.ClassINET,
Ttl: 0,
}
if rtype != dns.TypeA && rtype != dns.TypeAAAA {
return nil, fmt.Errorf("invalid response type")
}
var rr dns.RR
if rtype == dns.TypeA {
rr = &dns.A{
A: ip,
Hdr: hdr,
}
} else {
rr = &dns.AAAA{
AAAA: ip,
Hdr: hdr,
}
}
response.Answer = []dns.RR{rr}
return r.returnResponseModel(response)
}
func (r *SpecialUseDomainNamesResolver) negativeResponse(request *model.Request) (*model.Response, error) {
response := newResponseMsg(request)
response.Rcode = dns.RcodeNameError
return r.returnResponseModel(response)
}
func (r *SpecialUseDomainNamesResolver) returnResponseModel(response *dns.Msg) (*model.Response, error) {
return returnResponseModel(response, model.ResponseTypeSPECIAL, "Special-Use Domain Name")
}

View File

@ -0,0 +1,105 @@
package resolver
import (
"net"
. "github.com/0xERR0R/blocky/model"
"github.com/0xERR0R/blocky/util"
"github.com/miekg/dns"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/stretchr/testify/mock"
)
var _ = Describe("SudnResolver", Label("sudnResolver"), func() {
var (
sut *SpecialUseDomainNamesResolver
m *MockResolver
mockAnswer *dns.Msg
err error
resp *Response
)
BeforeEach(func() {
mockAnswer, err = util.NewMsgWithAnswer("example.com.", 300, dns.Type(dns.TypeA), "123.145.123.145")
Expect(err).Should(Succeed())
m = &MockResolver{}
m.On("Resolve", mock.Anything).Return(&Response{Res: mockAnswer}, nil)
sut = NewSpecialUseDomainNamesResolver().(*SpecialUseDomainNamesResolver)
sut.Next(m)
})
Describe("Blocking special names", func() {
It("should block arpa", func() {
for _, arpa := range sudnArpaSlice() {
resp, err = sut.Resolve(newRequest(arpa, dns.Type(dns.TypeA)))
Expect(err).Should(Succeed())
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError))
}
})
It("should block test", func() {
resp, err = sut.Resolve(newRequest(sudnTest, dns.Type(dns.TypeA)))
Expect(err).Should(Succeed())
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError))
})
It("should block invalid", func() {
resp, err = sut.Resolve(newRequest(sudnInvalid, dns.Type(dns.TypeA)))
Expect(err).Should(Succeed())
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError))
})
It("should block localhost none A", func() {
resp, err = sut.Resolve(newRequest(sudnLocalhost, dns.Type(dns.TypeHTTPS)))
Expect(err).Should(Succeed())
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError))
})
It("should block local", func() {
resp, err = sut.Resolve(newRequest(mdnsLocal, dns.Type(dns.TypeA)))
Expect(err).Should(Succeed())
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError))
})
It("should block localhost none A", func() {
resp, err = sut.Resolve(newRequest(mdnsLocal, dns.Type(dns.TypeHTTPS)))
Expect(err).Should(Succeed())
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError))
})
})
Describe("Resolve localhost", func() {
It("should resolve IPv4 loopback", func() {
resp, err = sut.Resolve(newRequest(sudnLocalhost, dns.Type(dns.TypeA)))
Expect(err).Should(Succeed())
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeSuccess))
Expect(resp.Res.Answer[0].(*dns.A).A).Should(Equal(sut.defaults.loopbackV4))
})
It("should resolve IPv6 loopback", func() {
resp, err = sut.Resolve(newRequest(sudnLocalhost, dns.Type(dns.TypeAAAA)))
Expect(err).Should(Succeed())
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeSuccess))
Expect(resp.Res.Answer[0].(*dns.AAAA).AAAA).Should(Equal(sut.defaults.loopbackV6))
})
})
Describe("Forward other", func() {
It("should forward example.com", func() {
resp, err = sut.Resolve(newRequest("example.com", dns.Type(dns.TypeA)))
Expect(err).Should(Succeed())
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeSuccess))
Expect(resp.Res.Answer[0].(*dns.A).A).Should(Equal(net.ParseIP("123.145.123.145")))
})
})
Describe("Configuration pseudo test", func() {
It("should always be empty", func() {
Expect(sut.Configuration()).Should(HaveLen(0))
})
})
})

View File

@ -5,7 +5,7 @@ import (
"crypto/tls"
"errors"
"fmt"
"io/ioutil"
"io"
"net"
"net/http"
"strconv"
@ -53,15 +53,21 @@ type dnsUpstreamClient struct {
type httpUpstreamClient struct {
client *http.Client
host string
}
func createUpstreamClient(cfg config.Upstream) upstreamClient {
timeout := time.Duration(config.GetConfig().UpstreamTimeout)
tlsConfig := tls.Config{
ServerName: cfg.Host,
MinVersion: tls.VersionTLS12,
}
if cfg.CommonName != "" {
tlsConfig.ServerName = cfg.CommonName
}
switch cfg.Net {
case config.NetProtocolHttps:
return &httpUpstreamClient{
@ -73,6 +79,7 @@ func createUpstreamClient(cfg config.Upstream) upstreamClient {
},
Timeout: timeout,
},
host: cfg.Host,
}
case config.NetProtocolTcpTls:
@ -106,7 +113,7 @@ func createUpstreamClient(cfg config.Upstream) upstreamClient {
}
func (r *httpUpstreamClient) fmtURL(ip net.IP, port uint16, path string) string {
return fmt.Sprintf("https://%s:%d%s", ip.String(), port, path)
return fmt.Sprintf("https://%s%s", net.JoinHostPort(ip.String(), strconv.Itoa(int(port))), path)
}
func (r *httpUpstreamClient) callExternal(msg *dns.Msg,
@ -127,6 +134,8 @@ func (r *httpUpstreamClient) callExternal(msg *dns.Msg,
req.Header.Set("User-Agent", config.GetConfig().DoHUserAgent)
req.Header.Set("Content-Type", dnsContentType)
req.Host = r.host
httpResponse, err := r.client.Do(req)
if err != nil {
@ -147,7 +156,7 @@ func (r *httpUpstreamClient) callExternal(msg *dns.Msg,
dnsContentType, contentType)
}
body, err := ioutil.ReadAll(httpResponse.Body)
body, err := io.ReadAll(httpResponse.Body)
if err != nil {
return nil, 0, fmt.Errorf("can't read response body: %w", err)
}
@ -162,7 +171,7 @@ func (r *httpUpstreamClient) callExternal(msg *dns.Msg,
return &response, time.Since(start), nil
}
func (r *dnsUpstreamClient) fmtURL(ip net.IP, port uint16, _path string) string {
func (r *dnsUpstreamClient) fmtURL(ip net.IP, port uint16, _ string) string {
return net.JoinHostPort(ip.String(), strconv.Itoa(int(port)))
}

View File

@ -205,7 +205,7 @@ var _ = Describe("UpstreamResolver", Label("upstreamResolver"), func() {
It("should return error", func() {
_, err := sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeA)))
Expect(err).Should(HaveOccurred())
Expect(err.Error()).Should(ContainSubstring("no such host"))
Expect(err.Error()).Should(Or(ContainSubstring("no such host"), ContainSubstring("i/o timeout")))
})
})
})

View File

@ -2,12 +2,14 @@ package server
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"math"
"math/big"
mrand "math/rand"
"net"
@ -36,7 +38,6 @@ const (
maxUDPBufferSize = 65535
caExpiryYears = 10
certExpiryYears = 5
certRSAsize = 4096
)
// Server controls the endpoints for DNS and HTTP
@ -295,7 +296,7 @@ func createUDPServer(address string) (*dns.Server, error) {
func createSelfSignedCert() (tls.Certificate, error) {
// Create CA
ca := &x509.Certificate{
SerialNumber: big.NewInt(int64(mrand.Intn(certRSAsize))), //nolint:gosec
SerialNumber: big.NewInt(int64(mrand.Intn(math.MaxInt))), //nolint:gosec
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(caExpiryYears, 0, 0),
IsCA: true,
@ -304,7 +305,7 @@ func createSelfSignedCert() (tls.Certificate, error) {
BasicConstraintsValid: true,
}
caPrivKey, err := rsa.GenerateKey(rand.Reader, certRSAsize)
caPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return tls.Certificate{}, err
}
@ -323,16 +324,22 @@ func createSelfSignedCert() (tls.Certificate, error) {
}
caPrivKeyPEM := new(bytes.Buffer)
b, err := x509.MarshalECPrivateKey(caPrivKey)
if err != nil {
return tls.Certificate{}, err
}
if err = pem.Encode(caPrivKeyPEM, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey),
Type: "EC PRIVATE KEY",
Bytes: b,
}); err != nil {
return tls.Certificate{}, err
}
// Create certificate
cert := &x509.Certificate{
SerialNumber: big.NewInt(int64(mrand.Intn(certRSAsize))), //nolint:gosec
SerialNumber: big.NewInt(int64(mrand.Intn(math.MaxInt))), //nolint:gosec
DNSNames: []string{"*"},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(certExpiryYears, 0, 0),
@ -341,7 +348,7 @@ func createSelfSignedCert() (tls.Certificate, error) {
KeyUsage: x509.KeyUsageDigitalSignature,
}
certPrivKey, err := rsa.GenerateKey(rand.Reader, certRSAsize)
certPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return tls.Certificate{}, err
}
@ -360,9 +367,15 @@ func createSelfSignedCert() (tls.Certificate, error) {
}
certPrivKeyPEM := new(bytes.Buffer)
b, err = x509.MarshalECPrivateKey(certPrivKey)
if err != nil {
return tls.Certificate{}, err
}
if err = pem.Encode(certPrivKeyPEM, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey),
Type: "EC PRIVATE KEY",
Bytes: b,
}); err != nil {
return tls.Certificate{}, err
}
@ -397,7 +410,9 @@ func createQueryResolver(
r = resolver.Chain(
resolver.NewFilteringResolver(cfg.Filtering),
resolver.NewFqdnOnlyResolver(*cfg),
clientNamesResolver,
resolver.NewEdeResolver(cfg.Ede),
resolver.NewQueryLoggingResolver(cfg.QueryLog),
resolver.NewMetricsResolver(cfg.Prometheus),
resolver.NewRewriterResolver(cfg.CustomDNS.RewriteConfig, resolver.NewCustomDNSResolver(cfg.CustomDNS)),
@ -405,6 +420,7 @@ func createQueryResolver(
blockingResolver,
resolver.NewCachingResolver(cfg.Caching, redisClient),
resolver.NewRewriterResolver(cfg.Conditional.RewriteConfig, conditionalUpstreamResolver),
resolver.NewSpecialUseDomainNamesResolver(),
parallelResolver,
)
@ -467,6 +483,12 @@ func toMB(b uint64) uint64 {
return b / bytesInKB / bytesInKB
}
const (
readHeaderTimeout = 20 * time.Second
readTimeout = 20 * time.Second
writeTimeout = 20 * time.Second
)
// Start starts the server
func (s *Server) Start(errCh chan<- error) {
logger().Info("Starting server")
@ -488,7 +510,14 @@ func (s *Server) Start(errCh chan<- error) {
go func() {
logger().Infof("http server is up and running on addr/port %s", address)
if err := http.Serve(listener, s.httpMux); err != nil {
srv := &http.Server{
ReadTimeout: readTimeout,
ReadHeaderTimeout: readHeaderTimeout,
WriteTimeout: writeTimeout,
Handler: s.httpsMux,
}
if err := srv.Serve(listener); err != nil {
errCh <- fmt.Errorf("start http listener failed: %w", err)
}
}()
@ -502,7 +531,10 @@ func (s *Server) Start(errCh chan<- error) {
logger().Infof("https server is up and running on addr/port %s", address)
server := http.Server{
Handler: s.httpsMux,
Handler: s.httpsMux,
ReadTimeout: readTimeout,
ReadHeaderTimeout: readHeaderTimeout,
WriteTimeout: writeTimeout,
//nolint:gosec
TLSConfig: &tls.Config{
MinVersion: minTLSVersion(),

View File

@ -5,7 +5,7 @@ import (
"encoding/json"
"fmt"
"html/template"
"io/ioutil"
"io"
"net"
"net/http"
"strings"
@ -25,9 +25,12 @@ import (
)
const (
dohMessageLimit = 512
dnsContentType = "application/dns-message"
corsMaxAge = 5 * time.Minute
dohMessageLimit = 512
contentTypeHeader = "content-type"
dnsContentType = "application/dns-message"
jsonContentType = "application/json"
htmlContentType = "text/html; charset=UTF-8"
corsMaxAge = 5 * time.Minute
)
func secureHeader(next http.Handler) http.Handler {
@ -83,7 +86,7 @@ func (s *Server) dohPostRequestHandler(rw http.ResponseWriter, req *http.Request
return
}
rawMsg, err := ioutil.ReadAll(req.Body)
rawMsg, err := io.ReadAll(req.Body)
if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
@ -173,6 +176,9 @@ func extractIP(r *http.Request) string {
// @Router /query [post]
func (s *Server) apiQuery(rw http.ResponseWriter, req *http.Request) {
var queryRequest api.QueryRequest
rw.Header().Set(contentTypeHeader, jsonContentType)
err := json.NewDecoder(req.Body).Decode(&queryRequest)
if err != nil {
@ -253,7 +259,7 @@ func createRouter(cfg *config.Config) *chi.Mux {
func configureRootHandler(cfg *config.Config, router *chi.Mux) {
router.Get("/", func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("content-type", dnsContentType)
writer.Header().Set(contentTypeHeader, htmlContentType)
t := template.New("index")
_, _ = t.Parse(web.IndexTmpl)

View File

@ -4,7 +4,7 @@ import (
"bytes"
"encoding/base64"
"encoding/json"
"io/ioutil"
"io"
"net"
"net/http"
"strings"
@ -61,8 +61,15 @@ var _ = BeforeSuite(func() {
DeferCleanup(fritzboxMockUpstream.Close)
clientMockUpstream := resolver.NewMockUDPUpstreamServer().WithAnswerFn(func(request *dns.Msg) (response *dns.Msg) {
var clientName string
client := mockClientName.Load()
if client != nil {
clientName = mockClientName.Load().(string)
}
response, err := util.NewMsgWithAnswer(
util.ExtractDomain(request.Question[0]), 3600, dns.Type(dns.TypePTR), mockClientName.Load().(string),
util.ExtractDomain(request.Question[0]), 3600, dns.Type(dns.TypePTR), clientName,
)
Expect(err).Should(Succeed())
@ -75,6 +82,28 @@ var _ = BeforeSuite(func() {
upstreamFritzbox = fritzboxMockUpstream.Start()
upstreamGoogle = googleMockUpstream.Start()
tmpDir := NewTmpFolder("server")
Expect(tmpDir.Error).Should(Succeed())
DeferCleanup(tmpDir.Clean)
certPem := writeCertPem(tmpDir)
Expect(certPem.Error).Should(Succeed())
keyPem := writeKeyPem(tmpDir)
Expect(keyPem.Error).Should(Succeed())
doubleclickFile := tmpDir.CreateStringFile("doubleclick.net.txt", "doubleclick.net", "doubleclick.net.cn")
Expect(doubleclickFile.Error).Should(Succeed())
bildFile := tmpDir.CreateStringFile("www.bild.de.txt", "www.bild.de")
Expect(bildFile.Error).Should(Succeed())
heiseFile := tmpDir.CreateStringFile("heise.de.txt", "heise.de")
Expect(heiseFile.Error).Should(Succeed())
youtubeFile := tmpDir.CreateStringFile("youtube.com.txt", "youtube.com")
Expect(youtubeFile.Error).Should(Succeed())
// create server
sut, err = NewServer(&config.Config{
CustomDNS: config.CustomDNSConfig{
@ -97,13 +126,13 @@ var _ = BeforeSuite(func() {
Blocking: config.BlockingConfig{
BlackLists: map[string][]string{
"ads": {
"../testdata/doubleclick.net.txt",
"../testdata/www.bild.de.txt",
"../testdata/heise.de.txt"},
"youtube": {"../testdata/youtube.com.txt"}},
doubleclickFile.Path,
bildFile.Path,
heiseFile.Path},
"youtube": {youtubeFile.Path}},
WhiteLists: map[string][]string{
"ads": {"../testdata/heise.de.txt"},
"whitelist": {"../testdata/heise.de.txt"},
"ads": {heiseFile.Path},
"whitelist": {heiseFile.Path},
},
ClientGroupsBlock: map[string][]string{
"default": {"ads"},
@ -123,8 +152,8 @@ var _ = BeforeSuite(func() {
DNSPorts: config.ListenConfig{"55555"},
TLSPorts: config.ListenConfig{"8853"},
CertFile: "../testdata/cert.pem",
KeyFile: "../testdata/key.pem",
CertFile: certPem.Path,
KeyFile: keyPem.Path,
HTTPPorts: config.ListenConfig{"4000"},
HTTPSPorts: config.ListenConfig{"4443"},
Prometheus: config.PrometheusConfig{
@ -290,18 +319,19 @@ var _ = Describe("Running DNS server", func() {
Describe("Prometheus endpoint", func() {
When("Prometheus URL is called", func() {
It("should return prometheus data", func() {
r, err := http.Get("http://localhost:4000/metrics")
resp, err := http.Get("http://localhost:4000/metrics")
Expect(err).Should(Succeed())
Expect(r.StatusCode).Should(Equal(http.StatusOK))
Expect(resp).Should(HaveHTTPStatus(http.StatusOK))
})
})
})
Describe("Root endpoint", func() {
When("Root URL is called", func() {
It("should return root page", func() {
r, err := http.Get("http://localhost:4000/")
resp, err := http.Get("http://localhost:4000/")
Expect(err).Should(Succeed())
Expect(r.StatusCode).Should(Equal(http.StatusOK))
Expect(resp).Should(HaveHTTPStatus(http.StatusOK))
Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "text/html; charset=UTF-8"))
})
})
})
@ -321,7 +351,8 @@ var _ = Describe("Running DNS server", func() {
Expect(err).Should(Succeed())
defer resp.Body.Close()
Expect(resp.StatusCode).Should(Equal(http.StatusOK))
Expect(resp).Should(HaveHTTPStatus(http.StatusOK))
Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/json"))
var result api.QueryResult
err = json.NewDecoder(resp.Body).Decode(&result)
@ -384,7 +415,9 @@ var _ = Describe("Running DNS server", func() {
defer resp.Body.Close()
Expect(resp).Should(HaveHTTPStatus(http.StatusOK))
rawMsg, err := ioutil.ReadAll(resp.Body)
Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/dns-message"))
rawMsg, err := io.ReadAll(resp.Body)
Expect(err).Should(Succeed())
msg := new(dns.Msg)
@ -446,7 +479,8 @@ var _ = Describe("Running DNS server", func() {
Expect(err).Should(Succeed())
defer resp.Body.Close()
Expect(resp).Should(HaveHTTPStatus(http.StatusOK))
rawMsg, err := ioutil.ReadAll(resp.Body)
Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/dns-message"))
rawMsg, err := io.ReadAll(resp.Body)
Expect(err).Should(Succeed())
msg = new(dns.Msg)
@ -465,7 +499,8 @@ var _ = Describe("Running DNS server", func() {
Expect(err).Should(Succeed())
defer resp.Body.Close()
Expect(resp).Should(HaveHTTPStatus(http.StatusOK))
rawMsg, err := ioutil.ReadAll(resp.Body)
Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/dns-message"))
rawMsg, err := io.ReadAll(resp.Body)
Expect(err).Should(Succeed())
msg = new(dns.Msg)
@ -522,7 +557,7 @@ var _ = Describe("Running DNS server", func() {
Expect(cErr).Should(Succeed())
cfg.Upstream.ExternalResolvers = map[string][]config.Upstream{
"default": {config.Upstream{Net: config.NetProtocolTcpUdp, Host: "4.4.4.4", Port: 53}}}
"default": {config.Upstream{Net: config.NetProtocolTcpUdp, Host: "1.1.1.1", Port: 53}}}
cfg.Redis.Address = "test-fail"
})
@ -652,7 +687,7 @@ var _ = Describe("Running DNS server", func() {
Expect(cErr).Should(Succeed())
cfg.Upstream.ExternalResolvers = map[string][]config.Upstream{
"default": {config.Upstream{Net: config.NetProtocolTcpUdp, Host: "4.4.4.4", Port: 53}}}
"default": {config.Upstream{Net: config.NetProtocolTcpUdp, Host: "1.1.1.1", Port: 53}}}
})
It("should create self-signed certificate if key/cert files are not provided", func() {
@ -701,3 +736,41 @@ func requestServer(request *dns.Msg) *dns.Msg {
return nil
}
func writeCertPem(tmpDir *TmpFolder) *TmpFile {
return tmpDir.CreateStringFile("cert.pem",
"-----BEGIN CERTIFICATE-----",
"MIICMzCCAZygAwIBAgIRAJCCrDTGEtZfRpxDY1KAoswwDQYJKoZIhvcNAQELBQAw",
"EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2",
"MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw",
"gYkCgYEA4mEaF5yWYYrTfMgRXdBpgGnqsHIADQWlw7BIJWD/gNp+fgp4TUZ/7ggV",
"rrvRORvRFjw14avd9L9EFP7XLi8ViU3uoE1UWI32MlrKqLbGNCXyUIApIoqlbRg6",
"iErxIk5+ChzFuysQOx01S2yv/ML6dx7NOGHs1S38MUzRZtcXBH8CAwEAAaOBhjCB",
"gzAOBgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/",
"BAUwAwEB/zAdBgNVHQ4EFgQUslNI6tYIv909RttHaZVMS/u/VYYwLAYDVR0RBCUw",
"I4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3DQEB",
"CwUAA4GBAJ2gRpQHr5Qj7dt26bYVMdN4JGXTsvjbVrJfKI0VfPGJ+SUY/uTVBUeX",
"+Cwv4DFEPBlNx/lzuUkwmRaExC4/w81LWwxe5KltYsjyJuYowiUbLZ6tzLaQ9Bcx",
"jxClAVvgj90TGYOwsv6ESOX7GWteN1FlD3+jk7vefjFagaKKFYR9",
"-----END CERTIFICATE-----")
}
func writeKeyPem(tmpDir *TmpFolder) *TmpFile {
return tmpDir.CreateStringFile("key.pem",
"-----BEGIN PRIVATE KEY-----",
"MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAOJhGheclmGK03zI",
"EV3QaYBp6rByAA0FpcOwSCVg/4Dafn4KeE1Gf+4IFa670Tkb0RY8NeGr3fS/RBT+",
"1y4vFYlN7qBNVFiN9jJayqi2xjQl8lCAKSKKpW0YOohK8SJOfgocxbsrEDsdNUts",
"r/zC+ncezThh7NUt/DFM0WbXFwR/AgMBAAECgYEA1exixstPhI+2+OTrHFc1S4dL",
"oz+ncqbSlZEBLGl0KWTQQfVM5+FmRR7Yto1/0lLKDBQL6t0J2x3fjWOhHmCaHKZA",
"VAvZ8+OKxwofih3hlO0tGCB8szUJygp2FAmd0rOUqvPQ+PTohZEUXyDaB8MOIbX+",
"qoo7g19+VlbyKqmM8HkCQQDs4GQJwEn7GXKllSMyOfiYnjQM2pwsqO0GivXkH+p3",
"+h5KDp4g3O4EbmbrvZyZB2euVsBjW3pFMu+xPXuOXf91AkEA9KfC7LGLD2OtLmrM",
"iCZAqHlame+uEEDduDmqjTPnNKUWVeRtYKMF5Hltbeo1jMXMSbVZ+fRWKfQ+HAhQ",
"xjFJowJAV6U7PqRoe0FSO1QwXrA2fHnk9nCY4qlqckZObyckAVqJhIteFPjKFNeo",
"u0dAPxsPUOGGc/zwA9Sx/ZmrMuUy1QJBALl7bqawO/Ng6G0mfwZBqgeQaYYHVnnw",
"E6iV353J2eHpvzNDSUFYlyEOhk4soIindSf0m9CK08Be8a+jBkocF+0CQQC+Hi7L",
"kZV1slpW82BxYIhs9Gb0OQgK8SsI4aQPTFGUarQXXAm4eRqBO0kaG+jGX6TtW353",
"EHK784GIxwVXKej/",
"-----END PRIVATE KEY-----")
}

14
testdata/cert.pem vendored
View File

@ -1,14 +0,0 @@
-----BEGIN CERTIFICATE-----
MIICMzCCAZygAwIBAgIRAJCCrDTGEtZfRpxDY1KAoswwDQYJKoZIhvcNAQELBQAw
EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2
MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
gYkCgYEA4mEaF5yWYYrTfMgRXdBpgGnqsHIADQWlw7BIJWD/gNp+fgp4TUZ/7ggV
rrvRORvRFjw14avd9L9EFP7XLi8ViU3uoE1UWI32MlrKqLbGNCXyUIApIoqlbRg6
iErxIk5+ChzFuysQOx01S2yv/ML6dx7NOGHs1S38MUzRZtcXBH8CAwEAAaOBhjCB
gzAOBgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/
BAUwAwEB/zAdBgNVHQ4EFgQUslNI6tYIv909RttHaZVMS/u/VYYwLAYDVR0RBCUw
I4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3DQEB
CwUAA4GBAJ2gRpQHr5Qj7dt26bYVMdN4JGXTsvjbVrJfKI0VfPGJ+SUY/uTVBUeX
+Cwv4DFEPBlNx/lzuUkwmRaExC4/w81LWwxe5KltYsjyJuYowiUbLZ6tzLaQ9Bcx
jxClAVvgj90TGYOwsv6ESOX7GWteN1FlD3+jk7vefjFagaKKFYR9
-----END CERTIFICATE-----

54
testdata/config.yml vendored
View File

@ -1,54 +0,0 @@
upstream:
default:
- tcp+udp:8.8.8.8
- tcp+udp:8.8.4.4
- 1.1.1.1
customDNS:
mapping:
my.duckdns.org: 192.168.178.3
multiple.ips: 192.168.178.3,192.168.178.4,2001:0db8:85a3:08d3:1319:8a2e:0370:7344
conditional:
mapping:
fritz.box: tcp+udp:192.168.178.1
multiple.resolvers: tcp+udp:192.168.178.1,tcp+udp:192.168.178.2
filtering:
queryTypes:
- AAAA
- A
blocking:
blackLists:
ads:
- https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt
- https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
- https://mirror1.malwaredomains.com/files/justdomains
- http://sysctl.org/cameleon/hosts
- https://zeustracker.abuse.ch/blocklist.php?download=domainblocklist
- https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt
special:
- https://hosts-file.net/ad_servers.txt
whiteLists:
ads:
- whitelist.txt
clientGroupsBlock:
default:
- ads
- special
Laptop-D.fritz.box:
- ads
blockTTL: 1m
# without unit -> use minutes
refreshPeriod: 120
clientLookup:
upstream: 192.168.178.1
singleNameOrder:
- 2
- 1
queryLog:
type: csv-client
target: /opt/log
port: 55553,:55554,[::1]:55555
logLevel: debug
dohUserAgent: testBlocky
minTlsServeVersion: 1.3

View File

@ -1,18 +0,0 @@
upstream:
default:
- tcp+udp:8.8.8.8
- tcp+udp:8.8.4.4
- 1.1.1.1
customDNS:
mapping:
my.duckdns.org: 192.168.178.3
multiple.ips: 192.168.178.3,192.168.178.4,2001:0db8:85a3:08d3:1319:8a2e:0370:7344
conditional:
mapping:
fritz.box: tcp+udp:192.168.178.1
multiple.resolvers: tcp+udp:192.168.178.1,tcp+udp:192.168.178.2
filtering:
queryTypes:
- AAAA
- A

View File

@ -1,37 +0,0 @@
blocking:
blackLists:
ads:
- https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt
- https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
- https://mirror1.malwaredomains.com/files/justdomains
- http://sysctl.org/cameleon/hosts
- https://zeustracker.abuse.ch/blocklist.php?download=domainblocklist
- https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt
special:
- https://hosts-file.net/ad_servers.txt
whiteLists:
ads:
- whitelist.txt
clientGroupsBlock:
default:
- ads
- special
Laptop-D.fritz.box:
- ads
blockTTL: 1m
# without unit -> use minutes
refreshPeriod: 120
clientLookup:
upstream: 192.168.178.1
singleNameOrder:
- 2
- 1
queryLog:
type: csv-client
target: /opt/log
port: 55553,:55554,[::1]:55555
logLevel: debug
dohUserAgent: testBlocky
minTlsServeVersion: 1.3

View File

@ -1,2 +0,0 @@
doubleclick.net
doubleclick.net.cn

View File

@ -1 +0,0 @@
heise.de

14
testdata/hosts.txt vendored
View File

@ -1,14 +0,0 @@
# Random comment
127.0.0.1 localhost
127.0.1.1 localhost2 localhost2.local.lan
::1 localhost
# Two empty lines to follow
faaf:faaf:faaf:faaf::1 ipv6host ipv6host.local.lan
192.168.2.1 ipv4host ipv4host.local.lan
10.0.0.1 router0 router1 router2
10.0.0.2 router3 # Another comment
10.0.0.3 # Invalid entry
300.300.300.300 invalid4 # Invalid IPv4
abcd:efgh:ijkl::1 invalid6 # Invalud IPv6

16
testdata/key.pem vendored
View File

@ -1,16 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAOJhGheclmGK03zI
EV3QaYBp6rByAA0FpcOwSCVg/4Dafn4KeE1Gf+4IFa670Tkb0RY8NeGr3fS/RBT+
1y4vFYlN7qBNVFiN9jJayqi2xjQl8lCAKSKKpW0YOohK8SJOfgocxbsrEDsdNUts
r/zC+ncezThh7NUt/DFM0WbXFwR/AgMBAAECgYEA1exixstPhI+2+OTrHFc1S4dL
oz+ncqbSlZEBLGl0KWTQQfVM5+FmRR7Yto1/0lLKDBQL6t0J2x3fjWOhHmCaHKZA
VAvZ8+OKxwofih3hlO0tGCB8szUJygp2FAmd0rOUqvPQ+PTohZEUXyDaB8MOIbX+
qoo7g19+VlbyKqmM8HkCQQDs4GQJwEn7GXKllSMyOfiYnjQM2pwsqO0GivXkH+p3
+h5KDp4g3O4EbmbrvZyZB2euVsBjW3pFMu+xPXuOXf91AkEA9KfC7LGLD2OtLmrM
iCZAqHlame+uEEDduDmqjTPnNKUWVeRtYKMF5Hltbeo1jMXMSbVZ+fRWKfQ+HAhQ
xjFJowJAV6U7PqRoe0FSO1QwXrA2fHnk9nCY4qlqckZObyckAVqJhIteFPjKFNeo
u0dAPxsPUOGGc/zwA9Sx/ZmrMuUy1QJBALl7bqawO/Ng6G0mfwZBqgeQaYYHVnnw
E6iV353J2eHpvzNDSUFYlyEOhk4soIindSf0m9CK08Be8a+jBkocF+0CQQC+Hi7L
kZV1slpW82BxYIhs9Gb0OQgK8SsI4aQPTFGUarQXXAm4eRqBO0kaG+jGX6TtW353
EHK784GIxwVXKej/
-----END PRIVATE KEY-----

View File

@ -1 +0,0 @@
www.bild.de

View File

@ -1 +0,0 @@
youtube.com

14
tools.go Normal file
View File

@ -0,0 +1,14 @@
//go:build tools
// +build tools
// see https://play-with-go.dev/tools-as-dependencies_go115_en/
// and https://www.jvt.me/posts/2022/06/15/go-tools-dependency-management/
package tools
import (
_ "github.com/abice/go-enum"
_ "github.com/dosgo/zigtool/zigcc"
_ "github.com/dosgo/zigtool/zigcpp"
_ "github.com/onsi/ginkgo/v2/ginkgo"
_ "github.com/swaggo/swag/cmd/swag"
)

View File

@ -6,4 +6,6 @@ var (
Version = "undefined"
// BuildTime build time of the binary
BuildTime = "undefined"
// Architecture current CPU architecture
Architecture = "undefined"
)

View File

@ -3,5 +3,6 @@ package web
import _ "embed"
// IndexTmpl html template for the start page
//
//go:embed index.html
var IndexTmpl string