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/ bin
dist/ dist
site
docs
node_modules
.git
.idea .idea
.github .github
testdata/ .vscode
node_modules/ .gitignore
*.md
LICENSE
vendor

View File

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

View File

@ -1,45 +1,35 @@
name: CI Build name: CI Build
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
make:
build: name: Test
name: Build
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
make: [build, test, race, docker-build, goreleaser]
steps: steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Set up Go 1.18 - name: Set up Go
uses: actions/setup-go@v1 uses: actions/setup-go@v3
with: with:
go-version: 1.18 go-version-file: go.mod
id: go id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
- name: Get dependencies - name: Get dependencies
run: | run: go mod download
go get -v -t -d ./...
if [ -f Gopkg.toml ]; then
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
dep ensure
fi
- name: Build - name: make ${{ matrix.make }}
run: make build run: make ${{ matrix.make }}
if: matrix.make != 'goreleaser'
- name: Test
run: make test
- name: Race detection
run: make race
- name: Upload results to codecov - name: Upload results to codecov
run: bash <(curl -s https://codecov.io/bash) -t 48d6a1a8-a66e-4f27-9cc1-a7b91c4209b2 uses: codecov/codecov-action@v3
if: matrix.make == 'test'
- name: Docker images
run: make docker-build
- name: Check GoReleaser configuration - name: Check GoReleaser configuration
uses: goreleaser/goreleaser-action@v2 uses: goreleaser/goreleaser-action@v3
if: matrix.make == 'goreleaser'
with: with:
args: check 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: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # 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). # 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) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v1 uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@ -64,4 +64,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - 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 name: Development docker build
on: on:
push: push:
branches: branches:
- development - development
- fb-* - fb-*
permissions:
security-events: write
actions: read
contents: read
packages: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
docker: check:
if: github.repository_owner == '0xERR0R' name: Check if workflow should run
runs-on: ubuntu-latest 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: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up QEMU - 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 - 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 - name: Login to GitHub Container Registry
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.CR_PAT }} password: ${{ steps.get_token.outputs.token }}
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 if: github.repository_owner == '0xERR0R'
uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract branch name
- name: Populate build variables
id: get_vars
shell: bash shell: bash
run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" run: |
id: extract_branch 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 - name: Build and push
uses: docker/build-push-action@v2 uses: docker/build-push-action@v3
with: with:
context: . context: .
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
push: true push: true
tags: | tags: ${{ steps.get_vars.outputs.tags }}
ghcr.io/0xerr0r/blocky:${{ steps.extract_branch.outputs.branch }} build-args: |
spx01/blocky:${{ steps.extract_branch.outputs.branch }} VERSION=${{ steps.get_vars.outputs.version }}
- name: Scan image BUILD_TIME=${{ steps.get_vars.outputs.build_time }}
uses: anchore/scan-action@v3 cache-from: type=gha
id: scan 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: with:
image: "spx01/blocky:${{ steps.extract_branch.outputs.branch }}" scan-type: 'fs'
fail-build: false ignore-unfixed: true
acs-report-enable: true format: 'sarif'
- name: upload Anchore scan SARIF report output: 'trivy-repo-results.sarif'
uses: github/codeql-action/upload-sarif@v1 severity: 'CRITICAL'
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v2
with: 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: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-python@v2 - uses: actions/setup-python@v4
with: with:
python-version: 3.x python-version: 3.x
- run: pip install mkdocs-material - run: pip install mkdocs-material

View File

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

View File

@ -7,21 +7,20 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository_owner == '0xERR0R'
steps: steps:
- name: Set up Go 1.18
uses: actions/setup-go@v1
with:
go-version: 1.18
id: go
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version-file: go.mod
id: go
- name: Build - name: Build
run: make build run: make build
@ -30,40 +29,59 @@ jobs:
- name: Docker meta - name: Docker meta
id: docker_meta id: docker_meta
uses: crazy-max/ghaction-docker-meta@v1 uses: crazy-max/ghaction-docker-meta@v4
with: with:
images: spx01/blocky,ghcr.io/0xerr0r/blocky images: spx01/blocky,ghcr.io/0xerr0r/blocky
- name: Set up QEMU - 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 - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v2
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.CR_PAT }} password: ${{ secrets.CR_PAT }}
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} 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 - name: Build and push
uses: docker/build-push-action@v2 uses: docker/build-push-action@v3
with: with:
context: . context: .
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker_meta.outputs.tags }} tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }} 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 - name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2 uses: goreleaser/goreleaser-action@v3
with: with:
version: latest version: latest
args: release --rm-dist args: release --rm-dist

3
.gitignore vendored
View File

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

View File

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

View File

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

View File

@ -1,43 +1,79 @@
# build stage # ----------- stage: ca-certs
FROM golang:1.18-alpine AS build-env # get newest certificates in seperate stage for caching
RUN apk add --no-cache \ FROM --platform=$BUILDPLATFORM alpine:3.16 AS ca-certs
git \ RUN apk add --no-cache ca-certificates
make \
gcc \
libc-dev \
zip \
ca-certificates
ENV GO111MODULE=on \ # update certificates and use the apk ones if update fails
CGO_ENABLED=0 RUN --mount=type=cache,target=/etc/ssl/certs \
update-ca-certificates 2>/dev/null || true
WORKDIR /src
# ----------- 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 ./ COPY go.mod go.sum ./
RUN go mod download RUN --mount=type=cache,target=/go/pkg \
go mod download
# add source # add source
ADD . . COPY . .
ARG opts # setup go & zig as CGO compiler
RUN env ${opts} make build 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 # add make & libcap
FROM alpine:3.16 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" \ LABEL org.opencontainers.image.source="https://github.com/0xERR0R/blocky" \
org.opencontainers.image.url="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" org.opencontainers.image.title="DNS proxy as ad-blocker for local network"
COPY --from=build-env /src/bin/blocky /app/blocky USER 100
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
WORKDIR /app WORKDIR /app
ENTRYPOINT ["/sbin/tini", "--"] COPY --from=ca-certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
CMD ["sh", "-c", "/app/blocky --config ${CONFIG_FILE:-/app/config.yml}"] 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 VERSION?=$(shell git describe --always --tags)
.DEFAULT_GOAL := help BUILD_TIME?=$(shell date '+%Y%m%d-%H%M%S')
DOCKER_IMAGE_NAME=spx01/blocky
VERSION := $(shell git describe --always --tags) BINARY_NAME:=blocky
BUILD_TIME=$(shell date '+%Y%m%d-%H%M%S') BIN_OUT_DIR?=bin
DOCKER_IMAGE_NAME="spx01/blocky"
BINARY_NAME=blocky GOARCH?=$(shell go env GOARCH)
BIN_OUT_DIR=bin 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) export PATH=$(shell go env GOPATH)/bin:$(shell echo $$PATH)
all: build test lint ## Build binary (with tests) all: build test lint ## Build binary (with tests)
clean: ## cleans output directory clean: ## cleans output directory
$(shell rm -rf $(BIN_OUT_DIR)/*) rm -rf $(BIN_OUT_DIR)/*
swagger: ## creates swagger documentation as html file 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 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/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 $(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 mkdocs serve
build: ## Build binary 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 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 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 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 lint: ## run golangcli-lint checks
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.46.2 go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.50.1
$(shell go env GOPATH)/bin/golangci-lint run golangci-lint run --timeout 5m
run: build ## Build and run binary run: build ## Build and run binary
./$(BIN_OUT_DIR)/$(BINARY_NAME) ./$(BIN_OUT_DIR)/$(BINARY_NAME)
@ -47,8 +69,15 @@ run: build ## Build and run binary
fmt: ## gofmt and goimports all go files fmt: ## gofmt and goimports all go files
find . -name '*.go' | while read -r file; do gofmt -w -s "$$file"; goimports -w "$$file"; done find . -name '*.go' | while read -r file; do gofmt -w -s "$$file"; goimports -w "$$file"; done
docker-build: ## Build docker image docker-build: ## Build docker image
docker build --network=host --tag ${DOCKER_IMAGE_NAME} . 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 help: ## Shows help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' @grep -E '^[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" "github.com/go-chi/chi/v5"
) )
const (
contentTypeHeader = "content-type"
jsonContentType = "application/json"
)
// BlockingControl interface to control the blocking status // BlockingControl interface to control the blocking status
type BlockingControl interface { type BlockingControl interface {
EnableBlocking() EnableBlocking()
@ -57,7 +62,8 @@ func registerListRefreshEndpoints(router chi.Router, refresher ListRefresher) {
// @Tags lists // @Tags lists
// @Success 200 "Lists were reloaded" // @Success 200 "Lists were reloaded"
// @Router /lists/refresh [post] // @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() l.refresher.RefreshLists()
} }
@ -75,10 +81,12 @@ func registerBlockingEndpoints(router chi.Router, control BlockingControl) {
// @Tags blocking // @Tags blocking
// @Success 200 "Blocking is enabled" // @Success 200 "Blocking is enabled"
// @Router /blocking/enable [get] // @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...") log.Log().Info("enabling blocking...")
s.control.EnableBlocking() s.control.EnableBlocking()
rw.Header().Set(contentTypeHeader, jsonContentType)
} }
// apiBlockingDisable is the http endpoint to disable the blocking status // 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 err error
) )
rw.Header().Set(contentTypeHeader, jsonContentType)
// parse duration from query parameter // parse duration from query parameter
durationParam := req.URL.Query().Get("duration") durationParam := req.URL.Query().Get("duration")
if len(durationParam) > 0 { 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) { func (s *BlockingEndpoint) apiBlockingStatus(rw http.ResponseWriter, _ *http.Request) {
status := s.control.BlockingStatus() status := s.control.BlockingStatus()
rw.Header().Set(contentTypeHeader, jsonContentType)
response, err := json.Marshal(status) response, err := json.Marshal(status)
util.LogOnError("unable to marshal response ", err) util.LogOnError("unable to marshal response ", err)

View File

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

View File

@ -120,8 +120,8 @@ var _ = Describe("Expiration cache", func() {
val, ttl := cache.Get("key1") val, ttl := cache.Get("key1")
Expect(val).Should(Equal("val2")) Expect(val).Should(Equal("val2"))
Expect(ttl.Milliseconds()).Should(And( Expect(ttl.Milliseconds()).Should(And(
BeNumerically(">", 900)), BeNumerically(">", 900),
BeNumerically("<=", 1000)) BeNumerically("<=", 1000)))
}) })
It("should delete the key if function returns nil", func() { 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 ( import (
"fmt" "fmt"
"io/ioutil" "io"
"net/http" "net/http"
"github.com/0xERR0R/blocky/api" "github.com/0xERR0R/blocky/api"
@ -39,7 +39,7 @@ func refreshList(_ *cobra.Command, _ []string) error {
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { 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)) return fmt.Errorf("response NOK, %s %s", resp.Status, string(body))
} }

View File

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

View File

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

View File

@ -1,23 +1,73 @@
package cmd package cmd
import ( import (
"io/ioutil" "io"
"os"
"github.com/0xERR0R/blocky/log" "github.com/0xERR0R/blocky/log"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "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() { When("Version command is called", func() {
log.Log().ExitFunc = nil log.Log().ExitFunc = nil
It("should execute without error", func() { It("should execute without error", func() {
c := NewRootCommand() c := NewRootCommand()
c.SetOutput(ioutil.Discard) c.SetOutput(io.Discard)
c.SetArgs([]string{"help"}) c.SetArgs([]string{"help"})
err := c.Execute() err := c.Execute()
Expect(err).Should(Succeed()) 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 package cmd
import ( import (
"time"
"github.com/0xERR0R/blocky/config" "github.com/0xERR0R/blocky/config"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
) )
var _ = Describe("Serve command", func() { var _ = Describe("Serve command", func() {
@ -21,13 +20,18 @@ var _ = Describe("Serve command", func() {
isConfigMandatory = false isConfigMandatory = false
grClosure := make(chan interface{})
go func() { go func() {
_ = startServer(newServeCommand(), []string{}) defer GinkgoRecover()
err := startServer(newServeCommand(), []string{})
Expect(err).Should(HaveOccurred())
close(grClosure)
}() }()
time.Sleep(100 * time.Millisecond) Eventually(grClosure).Should(BeClosed())
done <- true
}) })
}) })
}) })

View File

@ -21,4 +21,5 @@ func printVersion(_ *cobra.Command, _ []string) {
fmt.Println("blocky") fmt.Println("blocky")
fmt.Printf("Version: %s\n", util.Version) fmt.Printf("Version: %s\n", util.Version)
fmt.Printf("Build time: %s\n", util.BuildTime) 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: ignore:
- "resolver/mocks.go" - "resolver/mocks.go"
- "**/*_enum.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 package config
import ( import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"net" "net"
"os" "os"
"path/filepath" "path/filepath"
@ -12,6 +11,7 @@ import (
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/miekg/dns" "github.com/miekg/dns"
@ -36,6 +36,39 @@ const (
// ) // )
type NetProtocol uint16 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( // QueryLogType type of the query log ENUM(
// console // use logger as fallback // console // use logger as fallback
// none // no logging // none // no logging
@ -46,6 +79,13 @@ type NetProtocol uint16
// ) // )
type QueryLogType int16 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 type QType dns.Type
func (c QType) String() string { func (c QType) String() string {
@ -93,10 +133,11 @@ var netDefaultPort = map[NetProtocol]uint16{
// Upstream is the definition of external DNS server // Upstream is the definition of external DNS server
type Upstream struct { type Upstream struct {
Net NetProtocol Net NetProtocol
Host string Host string
Port uint16 Port uint16
Path string Path string
CommonName string // Common Name to use for certificate verification; optional. "" uses .Host
} }
// IsDefault returns true if u is the default value // 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( var validDomain = regexp.MustCompile(
`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`) `^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`)
// 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) { func ParseUpstream(upstream string) (Upstream, error) {
var path string var path string
var port uint16 var port uint16
commonName, upstream := extractCommonName(upstream)
n, upstream := extractNet(upstream) n, upstream := extractNet(upstream)
path, upstream = extractPath(upstream) path, upstream = extractPath(upstream)
@ -357,13 +400,20 @@ func ParseUpstream(upstream string) (Upstream, error) {
} }
return Upstream{ return Upstream{
Net: n, Net: n,
Host: host, Host: host,
Port: port, Port: port,
Path: path, Path: path,
CommonName: commonName,
}, nil }, nil
} }
func extractCommonName(in string) (string, string) {
upstream, cn, _ := strings.Cut(in, "#")
return cn, upstream
}
func extractPath(in string) (path string, upstream string) { func extractPath(in string) (path string, upstream string) {
slashIdx := strings.Index(in, "/") slashIdx := strings.Index(in, "/")
@ -399,33 +449,37 @@ func extractNet(upstream string) (NetProtocol, string) {
// Config main configuration // Config main configuration
// nolint:maligned // nolint:maligned
type Config struct { type Config struct {
Upstream UpstreamConfig `yaml:"upstream"` Upstream UpstreamConfig `yaml:"upstream"`
UpstreamTimeout Duration `yaml:"upstreamTimeout" default:"2s"` UpstreamTimeout Duration `yaml:"upstreamTimeout" default:"2s"`
CustomDNS CustomDNSConfig `yaml:"customDNS"` ConnectIPVersion IPVersion `yaml:"connectIPVersion"`
Conditional ConditionalUpstreamConfig `yaml:"conditional"` CustomDNS CustomDNSConfig `yaml:"customDNS"`
Blocking BlockingConfig `yaml:"blocking"` Conditional ConditionalUpstreamConfig `yaml:"conditional"`
ClientLookup ClientLookupConfig `yaml:"clientLookup"` Blocking BlockingConfig `yaml:"blocking"`
Caching CachingConfig `yaml:"caching"` ClientLookup ClientLookupConfig `yaml:"clientLookup"`
QueryLog QueryLogConfig `yaml:"queryLog"` Caching CachingConfig `yaml:"caching"`
Prometheus PrometheusConfig `yaml:"prometheus"` QueryLog QueryLogConfig `yaml:"queryLog"`
Redis RedisConfig `yaml:"redis"` Prometheus PrometheusConfig `yaml:"prometheus"`
LogLevel log.Level `yaml:"logLevel" default:"info"` Redis RedisConfig `yaml:"redis"`
LogFormat log.FormatType `yaml:"logFormat" default:"text"` LogLevel log.Level `yaml:"logLevel" default:"info"`
LogPrivacy bool `yaml:"logPrivacy" default:"false"` LogFormat log.FormatType `yaml:"logFormat" default:"text"`
LogTimestamp bool `yaml:"logTimestamp" default:"true"` LogPrivacy bool `yaml:"logPrivacy" default:"false"`
DNSPorts ListenConfig `yaml:"port" default:"[\"53\"]"` LogTimestamp bool `yaml:"logTimestamp" default:"true"`
HTTPPorts ListenConfig `yaml:"httpPort"` DNSPorts ListenConfig `yaml:"port" default:"[\"53\"]"`
HTTPSPorts ListenConfig `yaml:"httpsPort"` HTTPPorts ListenConfig `yaml:"httpPort"`
TLSPorts ListenConfig `yaml:"tlsPort"` HTTPSPorts ListenConfig `yaml:"httpsPort"`
DoHUserAgent string `yaml:"dohUserAgent"` TLSPorts ListenConfig `yaml:"tlsPort"`
MinTLSServeVer string `yaml:"minTlsServeVersion" default:"1.2"` DoHUserAgent string `yaml:"dohUserAgent"`
MinTLSServeVer string `yaml:"minTlsServeVersion" default:"1.2"`
StartVerifyUpstream bool `yaml:"startVerifyUpstream" default:"false"`
// Deprecated // Deprecated
DisableIPv6 bool `yaml:"disableIPv6" default:"false"` DisableIPv6 bool `yaml:"disableIPv6" default:"false"`
CertFile string `yaml:"certFile"` CertFile string `yaml:"certFile"`
KeyFile string `yaml:"keyFile"` KeyFile string `yaml:"keyFile"`
BootstrapDNS BootstrapConfig `yaml:"bootstrapDns"` BootstrapDNS BootstrapConfig `yaml:"bootstrapDns"`
HostsFile HostsFileConfig `yaml:"hostsFile"` HostsFile HostsFileConfig `yaml:"hostsFile"`
FqdnOnly bool `yaml:"fqdnOnly" default:"false"`
Filtering FilteringConfig `yaml:"filtering"` Filtering FilteringConfig `yaml:"filtering"`
Ede EdeConfig `yaml:"ede"`
} }
type BootstrapConfig bootstrapConfig // to avoid infinite recursion. See BootstrapConfig.UnmarshalYAML. type BootstrapConfig bootstrapConfig // to avoid infinite recursion. See BootstrapConfig.UnmarshalYAML.
@ -447,7 +501,8 @@ type UpstreamConfig struct {
// RewriteConfig custom DNS configuration // RewriteConfig custom DNS configuration
type RewriteConfig struct { 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 // CustomDNSConfig custom DNS configuration
@ -476,17 +531,19 @@ type ConditionalUpstreamMapping struct {
// BlockingConfig configuration for query blocking // BlockingConfig configuration for query blocking
type BlockingConfig struct { type BlockingConfig struct {
BlackLists map[string][]string `yaml:"blackLists"` BlackLists map[string][]string `yaml:"blackLists"`
WhiteLists map[string][]string `yaml:"whiteLists"` WhiteLists map[string][]string `yaml:"whiteLists"`
ClientGroupsBlock map[string][]string `yaml:"clientGroupsBlock"` ClientGroupsBlock map[string][]string `yaml:"clientGroupsBlock"`
BlockType string `yaml:"blockType" default:"ZEROIP"` BlockType string `yaml:"blockType" default:"ZEROIP"`
BlockTTL Duration `yaml:"blockTTL" default:"6h"` BlockTTL Duration `yaml:"blockTTL" default:"6h"`
DownloadTimeout Duration `yaml:"downloadTimeout" default:"60s"` DownloadTimeout Duration `yaml:"downloadTimeout" default:"60s"`
DownloadAttempts uint `yaml:"downloadAttempts" default:"3"` DownloadAttempts uint `yaml:"downloadAttempts" default:"3"`
DownloadCooldown Duration `yaml:"downloadCooldown" default:"1s"` DownloadCooldown Duration `yaml:"downloadCooldown" default:"1s"`
RefreshPeriod Duration `yaml:"refreshPeriod" default:"4h"` RefreshPeriod Duration `yaml:"refreshPeriod" default:"4h"`
FailStartOnListError bool `yaml:"failStartOnListError" default:"false"` // Deprecated
ProcessingConcurrency uint `yaml:"processingConcurrency" default:"4"` FailStartOnListError bool `yaml:"failStartOnListError" default:"false"`
ProcessingConcurrency uint `yaml:"processingConcurrency" default:"4"`
StartStrategy StartStrategyType `yaml:"startStrategy" default:"blocking"`
} }
// ClientLookupConfig configuration for the client lookup // ClientLookupConfig configuration for the client lookup
@ -528,20 +585,31 @@ type RedisConfig struct {
} }
type HostsFileConfig struct { type HostsFileConfig struct {
Filepath string `yaml:"filePath"` Filepath string `yaml:"filePath"`
HostsTTL Duration `yaml:"hostsTTL" default:"1h"` HostsTTL Duration `yaml:"hostsTTL" default:"1h"`
RefreshPeriod Duration `yaml:"refreshPeriod" default:"1h"` RefreshPeriod Duration `yaml:"refreshPeriod" default:"1h"`
FilterLoopback bool `yaml:"filterLoopback"`
} }
type FilteringConfig struct { type FilteringConfig struct {
QueryTypes QTypeSet `yaml:"queryTypes"` QueryTypes QTypeSet `yaml:"queryTypes"`
} }
type EdeConfig struct {
Enable bool `yaml:"enable" default:"false"`
}
// nolint:gochecknoglobals // nolint:gochecknoglobals
var config = &Config{} var (
config = &Config{}
cfgLock sync.RWMutex
)
// LoadConfig creates new config from YAML file or a directory containing YAML files // LoadConfig creates new config from YAML file or a directory containing YAML files
func LoadConfig(path string, mandatory bool) (*Config, error) { func LoadConfig(path string, mandatory bool) (*Config, error) {
cfgLock.Lock()
defer cfgLock.Unlock()
cfg := Config{} cfg := Config{}
if err := defaults.Set(&cfg); err != nil { if err := defaults.Set(&cfg); err != nil {
return nil, fmt.Errorf("can't apply default values: %w", err) 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 var data []byte
if fs.IsDir() { //nolint:nestif if fs.IsDir() {
err = filepath.WalkDir(path, func(filePath string, d os.DirEntry, err error) error { data, err = readFromDir(path, data)
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 err != nil { if err != nil {
return nil, fmt.Errorf("can't read config files: %w", err) return nil, fmt.Errorf("can't read config files: %w", err)
} }
} else { } else {
data, err = ioutil.ReadFile(path) data, err = os.ReadFile(path)
if err != nil { if err != nil {
return nil, fmt.Errorf("can't read config file: %w", err) 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 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 { func unmarshalConfig(data []byte, cfg *Config) error {
err := yaml.UnmarshalStrict(data, cfg) err := yaml.UnmarshalStrict(data, cfg)
if err != nil { if err != nil {
@ -620,10 +721,24 @@ func validateConfig(cfg *Config) {
cfg.Filtering.QueryTypes.Insert(dns.Type(dns.TypeAAAA)) 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 // GetConfig returns the current config
func GetConfig() *Config { func GetConfig() *Config {
cfgLock.RLock()
defer cfgLock.RUnlock()
return config return config
} }

View File

@ -11,6 +11,79 @@ import (
"strings" "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 ( const (
// NetProtocolTcpUdp is a NetProtocol of type Tcp+Udp. // NetProtocolTcpUdp is a NetProtocol of type Tcp+Udp.
// TCP and UDP protocols // TCP and UDP protocols
@ -23,6 +96,8 @@ const (
NetProtocolHttps NetProtocolHttps
) )
var ErrInvalidNetProtocol = fmt.Errorf("not a valid NetProtocol, try [%s]", strings.Join(_NetProtocolNames, ", "))
const _NetProtocolName = "tcp+udptcp-tlshttps" const _NetProtocolName = "tcp+udptcp-tlshttps"
var _NetProtocolNames = []string{ var _NetProtocolNames = []string{
@ -63,7 +138,7 @@ func ParseNetProtocol(name string) (NetProtocol, error) {
if x, ok := _NetProtocolValue[name]; ok { if x, ok := _NetProtocolValue[name]; ok {
return x, nil 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. // MarshalText implements the text marshaller method.
@ -103,6 +178,8 @@ const (
QueryLogTypeCsvClient QueryLogTypeCsvClient
) )
var ErrInvalidQueryLogType = fmt.Errorf("not a valid QueryLogType, try [%s]", strings.Join(_QueryLogTypeNames, ", "))
const _QueryLogTypeName = "consolenonemysqlpostgresqlcsvcsv-client" const _QueryLogTypeName = "consolenonemysqlpostgresqlcsvcsv-client"
var _QueryLogTypeNames = []string{ var _QueryLogTypeNames = []string{
@ -152,7 +229,7 @@ func ParseQueryLogType(name string) (QueryLogType, error) {
if x, ok := _QueryLogTypeValue[name]; ok { if x, ok := _QueryLogTypeValue[name]; ok {
return x, nil 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. // MarshalText implements the text marshaller method.
@ -170,3 +247,76 @@ func (x *QueryLogType) UnmarshalText(text []byte) error {
*x = tmp *x = tmp
return nil 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 ( import (
"errors" "errors"
"io/ioutil"
"net" "net"
"os"
"time" "time"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/0xERR0R/blocky/helpertest"
. "github.com/0xERR0R/blocky/log" . "github.com/0xERR0R/blocky/log"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
) )
var _ = Describe("Config", func() { 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() { Describe("Creation of Config", func() {
When("Test config file will be parsed", func() { When("Test config file will be parsed", func() {
It("should return a valid config struct", func() { It("should return a valid config struct", func() {
err := os.Chdir("../testdata") confFile := writeConfigYml(tmpDir)
Expect(err).Should(Succeed()) Expect(confFile.Error).Should(Succeed())
_, err = LoadConfig("config.yml", true) _, err = LoadConfig(confFile.Path, true)
Expect(err).Should(Succeed()) Expect(err).Should(Succeed())
defaultTestFileConfig() defaultTestFileConfig()
@ -29,36 +39,54 @@ var _ = Describe("Config", func() {
}) })
When("Test file does not exist", func() { When("Test file does not exist", func() {
It("should fail", 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())) Expect(err).Should(Not(Succeed()))
}) })
}) })
When("Multiple config files are used", func() { When("Multiple config files are used", func() {
It("should return a valid config struct", 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()) Expect(err).Should(Succeed())
defaultTestFileConfig() 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() { When("Config folder does not exist", func() {
It("should fail", 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())) Expect(err).Should(Not(Succeed()))
}) })
}) })
When("config file is malformed", func() { When("config file is malformed", func() {
It("should return error", func() { It("should return error", func() {
cfgFile := tmpDir.CreateStringFile("config.yml", "malformed_config")
Expect(cfgFile.Error).Should(Succeed())
dir, err := ioutil.TempDir("", "blocky") _, err = LoadConfig(cfgFile.Path, true)
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)
Expect(err).Should(HaveOccurred()) Expect(err).Should(HaveOccurred())
Expect(err.Error()).Should(ContainSubstring("wrong file structure")) 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() { When("config directory does not exist", func() {
It("should return error", func() { It("should return error", func() {
err := os.Chdir("../..") _, err = LoadConfig(tmpDir.JoinPath("config.yml"), true)
Expect(err).Should(Succeed())
_, err = LoadConfig("config.yml", true)
Expect(err).Should(HaveOccurred()) Expect(err).Should(HaveOccurred())
Expect(err.Error()).Should(ContainSubstring("no such file or directory")) Expect(err.Error()).Should(ContainSubstring("no such file or directory"))
}) })
It("should use default config if config is not mandatory", func() { It("should use default config if config is not mandatory", func() {
err := os.Chdir("../..") _, err = LoadConfig(tmpDir.JoinPath("config.yml"), false)
Expect(err).Should(Succeed())
_, err = LoadConfig("config.yml", false)
Expect(err).Should(Succeed()) Expect(err).Should(Succeed())
Expect(config.LogLevel).Should(Equal(LevelInfo)) Expect(config.LogLevel).Should(Equal(LevelInfo))
@ -381,6 +426,10 @@ bootstrapDns:
"tcp-tls:4.4.4.4", "tcp-tls:4.4.4.4",
Upstream{Net: NetProtocolTcpTls, Host: "4.4.4.4", Port: 853}, Upstream{Net: NetProtocolTcpTls, Host: "4.4.4.4", Port: 853},
false), 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", Entry("DoH without port, use default",
"https:4.4.4.4", "https:4.4.4.4",
Upstream{Net: NetProtocolHttps, Host: "4.4.4.4", Port: 443}, Upstream{Net: NetProtocolHttps, Host: "4.4.4.4", Port: 443},
@ -578,6 +627,126 @@ func defaultTestFileConfig() {
Expect(config.DoHUserAgent).Should(Equal("testBlocky")) Expect(config.DoHUserAgent).Should(Equal("testBlocky"))
Expect(config.MinTLSServeVer).Should(Equal("1.3")) Expect(config.MinTLSServeVer).Should(Equal("1.3"))
Expect(config.StartVerifyUpstream).Should(BeFalse())
Expect(GetConfig()).Should(Not(BeNil())) 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 # optional: timeout to query the upstream resolver. Default: 2s
upstreamTimeout: 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 # 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 # example: query "printer.lan" or "my.printer.lan" will return 192.168.178.3
customDNS: 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 # 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 # 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: 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 # optional: replace domain in the query with other domain before resolver lookup in the mapping
rewrite: rewrite:
example.com: fritz.box example.com: fritz.box
@ -97,8 +109,8 @@ blocking:
downloadAttempts: 5 downloadAttempts: 5
# optional: Time between the download attempts. Default: 1s # optional: Time between the download attempts. Default: 1s
downloadCooldown: 10s downloadCooldown: 10s
# optional: if true, application startup will fail if at least one list can't be downloaded / opened. Default: false # optional: if failOnError, application startup will fail if at least one list can't be downloaded / opened. Default: blocking
failStartOnListError: false startStrategy: failOnError
# optional: configuration for caching of DNS responses # optional: configuration for caching of DNS responses
caching: 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. # Max number of domains to be kept in cache for prefetching (soft limit). Useful on systems with limited amount of RAM.
# Default (0): unlimited # Default (0): unlimited
prefetchMaxItemsCount: 0 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 # optional: configuration of client name resolution
clientLookup: clientLookup:
@ -206,6 +221,8 @@ hostsFile:
hostsTTL: 60m hostsTTL: 60m
# optional: Time between hosts file refresh, default: 1h # optional: Time between hosts file refresh, default: 1h
refreshPeriod: 30m 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 # optional: Log level (one from debug, info, warn, error). Default: info
logLevel: info logLevel: info
# optional: Log format (text or json). Default: text # optional: Log format (text or json). Default: text
@ -214,3 +231,8 @@ logFormat: text
logTimestamp: true 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 # 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 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. | | 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 | | 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 | | 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 !!! 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 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. 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 | | Parameter | Type | Mandatory | Default value |
|-----------|----------------------------------|-----------|---------------------------------------------------| |-----------|----------------------------------|-----------|---------------------------------------------------|
| net | enum (tcp+udp, tcp-tls or https) | no | tcp+udp | | net | enum (tcp+udp, tcp-tls or https) | no | tcp+udp |
| host | IP or hostname | yes | | | host | IP or hostname | yes | |
| port | int (1 - 65535) | no | 53 for udp/tcp, 853 for tcp-tls and 443 for https | | 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 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. 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 - 123.123.123.123
``` ```
## Filtering ## Filtering
Under certain circumstances, it may be useful to filter some types of DNS queries. You can define one or more DNS query 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. 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 ## Custom DNS
You can define your own domain name to IP mappings. For example, you can use a user-friendly name for a network printer You can define your own domain name to IP mappings. For example, you can use a user-friendly name for a network printer
@ -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: With parameter `filterUnmappedTypes = true` (default), blocky will filter all queries with unmapped types, for example:
AAAA for "printer.lan" or TXT for "otherdevice.lan". 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 ## 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 `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 !!! example
```yaml ```yaml
conditional: conditional:
fallbackUpstream: false
rewrite: rewrite:
example.com: fritz.box example.com: fritz.box
replace-me.com: with-this.com 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) 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. 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") The query "client.example.com" will be rewritten to "client.fritz.box" and also redirected to the resolver at 192.168.178.1.
will be redirected to the DNS server at 168.168.0.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 ## Client name lookup
@ -436,16 +461,22 @@ You can configure the list download attempts according to your internet connecti
downloadCooldown: 10s 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 You can configure the blocking behavior during application start of blocky.
downloaded or opened. Default value is `false`. 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 !!! example
```yaml ```yaml
blocking: blocking:
failStartOnListError: false startStrategy: failOnError
``` ```
### Concurrency ### 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 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. 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 ```yaml
blocking: 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.prefetchExpires | duration format | no | 2h | Prefetch track time window |
| caching.prefetchThreshold | int | no | 5 | Name queries threshold for prefetch | | 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.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 !!! example
@ -593,17 +624,18 @@ example for Database
logRetentionDays: 7 logRetentionDays: 7
``` ```
### Hosts file ## Hosts file
You can enable resolving of entries, located in local hosts file. You can enable resolving of entries, located in local hosts file.
Configuration parameters: Configuration parameters:
| Parameter | Type | Mandatory | Default value | Description | | Parameter | Type | Mandatory | Default value | Description |
|--------------------------|--------------------------------|-----------|---------------|-----------------------------------------------| |--------------------------|--------------------------------|-----------|---------------|--------------------------------------------------|
| hostsFile.filePath | string | no | | Path to hosts file (e.g. /etc/hosts on Linux) | | 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.hostsTTL | duration (no units is minutes) | no | 1h | TTL |
| hostsFile.refreshPeriod | duration format | no | 1h | Time between hosts file refresh | | 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 !!! example
@ -614,6 +646,23 @@ Configuration parameters:
refreshPeriod: 30m 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) ## 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) 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. 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" --8<-- "docs/includes/abbreviations.md"

116
go.mod
View File

@ -1,90 +1,112 @@
module github.com/0xERR0R/blocky module github.com/0xERR0R/blocky
go 1.18 go 1.19
require ( 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/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/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-chi/cors v1.2.1
github.com/go-redis/redis/v8 v8.11.5 github.com/go-redis/redis/v8 v8.11.5
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
github.com/hashicorp/go-multierror v1.1.1 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/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/mroth/weightedrand v0.4.1
github.com/onsi/gomega v1.19.0 github.com/onsi/ginkgo/v2 v2.5.0
github.com/prometheus/client_golang v1.12.2 github.com/onsi/gomega v1.24.1
github.com/sirupsen/logrus v1.8.1 github.com/prometheus/client_golang v1.14.0
github.com/spf13/cobra v1.4.0 github.com/sirupsen/logrus v1.9.0
github.com/stretchr/testify v1.7.2 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 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 gopkg.in/yaml.v2 v2.4.0
gorm.io/driver/mysql v1.3.4 gorm.io/driver/mysql v1.4.3
gorm.io/driver/sqlite v1.3.2 gorm.io/driver/postgres v1.4.5
gorm.io/gorm v1.23.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 ( require (
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 github.com/go-logr/logr v1.2.3 // indirect
github.com/avast/retry-go/v4 v4.0.5 github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/go-chi/chi/v5 v5.0.7 github.com/lib/pq v1.10.6 // indirect
github.com/hashicorp/golang-lru v0.5.4 golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde // indirect
github.com/onsi/ginkgo/v2 v2.1.4
github.com/swaggo/swag v1.8.2
gorm.io/driver/postgres v1.3.7
) )
require ( require (
github.com/KyleBanks/depth v1.2.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // 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/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // 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/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // 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/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/spec v0.20.4 // indirect github.com/go-openapi/spec v0.20.7 // indirect
github.com/go-openapi/swag v0.19.15 // indirect github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-sql-driver/mysql v1.6.0 // 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/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/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/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/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile 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/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgtype v1.11.0 // indirect github.com/jackc/pgtype v1.12.0 // indirect
github.com/jackc/pgx/v4 v4.16.1 // indirect github.com/jackc/pgx/v4 v4.17.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect github.com/labstack/gommon v0.4.0 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-sqlite3 v1.14.12 // 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/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/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.7.3 // 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/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.3.0 // indirect github.com/stretchr/objx v0.5.0 // indirect
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 // indirect github.com/urfave/cli/v2 v2.23.0 // indirect
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 // indirect
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 // indirect golang.org/x/crypto v0.1.0 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/mod v0.6.0 // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/sys v0.2.0 // indirect
golang.org/x/tools v0.1.10 // indirect golang.org/x/term v0.2.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect golang.org/x/text v0.4.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect golang.org/x/tools v0.2.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v3 v3.0.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/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 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 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 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 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/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/abice/go-enum v0.5.3 h1:Ghq0aWp+tCNZFAb4lFK7UnjzUJQTS1atIMjHkX+Gex4=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 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-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/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-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-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 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 h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= 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.23.1 h1:jR6wZggBxwWygeXcdNyguCOCIjPsZyNUNlAkTx2fu0U=
github.com/alicebob/miniredis/v2 v2.21.0/go.mod h1:XNqvJdQJv5mSuVMc0ynneafpnL/zv52acZ6kqeS0t88= 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 h1:2JGTg6JapxP9/R33ZaagQtAM4EkkSYnIAlOG5EI8gkM=
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef/go.mod h1:JS7hed4L1fj0hXcyEejnW57/7LCetXggd+vwrRnYeII= 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.3.0 h1:cqI48aXx0BExKoM7XPklDpoHAg7/srPPLAfWG5z62jo=
github.com/avast/retry-go/v4 v4.0.5/go.mod h1:HqmLvS2VLdStPCGDFjSuZ9pzlTqVRldCI4w2dO4m1Ms= 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 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.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 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/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/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.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 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/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-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/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.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creasty/defaults v1.6.0 h1:ltuE9cfphUtlrBeomuu8PEyISTXnxqkBIoQfXgv7BSc= 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/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 h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 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.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.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/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/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 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 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= 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.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.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.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.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.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.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.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= 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/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.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= github.com/go-openapi/spec v0.20.7 h1:1Rlu/ZrOCCob0n+JKKJAWhNWMPW8bOZRg8FJaY+0SKI=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= 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.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.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 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= 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 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 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-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 h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 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= 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.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.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.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.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.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/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.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.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.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.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/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 v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 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-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-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-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/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 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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.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 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 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/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/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 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 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.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 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.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.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.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.12.1 h1:rsDFzIpRk7xT4B8FufgpCCeyjdNpKyghZeSefViE5W8= github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys=
github.com/jackc/pgconn v1.12.1/go.mod h1:ZkhRC59Llhrq3oSfrikvwQ5NaxYExr6twkdkMLaKono= 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 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 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= 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.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.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.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.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y=
github.com/jackc/pgproto3/v2 v2.3.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 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 h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 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-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-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 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.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.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w=
github.com/jackc/pgtype v1.11.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= 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-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-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.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.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.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E=
github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ= 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-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 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.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 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 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= 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/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/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.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 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/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.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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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.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.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.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.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-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/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.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.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.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.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 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.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.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-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 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 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 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 h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 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.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
github.com/miekg/dns v1.1.49/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= 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-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/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= 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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 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.5.0 h1:TRtrvv2vdQqzkwrQ1ke6vtXf7IK34RBUJafIy1wMwls=
github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw=
github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= github.com/onsi/gomega v1.24.1 h1:KORJXNNTzJXzu4ScJWssJfJMnJ+2QJqhoQSRwNlze9E=
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM=
github.com/pierrre/gotestcover v0.0.0-20160517101806-924dca7d15f0/go.mod h1:4xpMLz7RBWyB+ElzHu8Llua96TRCB3YwX+l5EP1wmHk=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 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.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.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.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.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.12.2/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-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-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.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.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.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.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= 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.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.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.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.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.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/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.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/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.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 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/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/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 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.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 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.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= 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 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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.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.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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.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.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.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.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/swaggo/swag v1.8.2 h1:D4aBiVS2a65zhyk3WFqOUz7Rz0sOaUcgeErcid5uGL4= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/swaggo/swag v1.8.2/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg= 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 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= 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.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/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.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 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-20220504180219-658193537a64 h1:5mLPGnFdSsevFRFc9q3yYbBkB6tsm4aCwwQV/j1JQAQ=
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 h1:k/gmLsJDWwWqbLCur2yWnJzwQEKRcAHXo6seXGuSwWw= github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 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.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 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-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-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-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-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b h1:QAqMVf3pSa6eeTsuklijukjXBlj7Es2QQplab+/RbQ4= golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 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-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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 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.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.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.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 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= 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-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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/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-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-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-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-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-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-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/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-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-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-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-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-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-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-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-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/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-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-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-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-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-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-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-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-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-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-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-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-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-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-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-20210927094055-39ccf1dd6fa6/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-20211107104306-e0b2ad06fe42/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-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-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/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-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-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.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.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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/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.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.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-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-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/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-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-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/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.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.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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-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-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-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-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= 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.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 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.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-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.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 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/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 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-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-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-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 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/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/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= 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.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-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-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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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.4.3 h1:/JhWJhO2v17d8hjApTltKNADm7K7YI2ogkR7avJUL3k=
gorm.io/driver/mysql v1.3.4/go.mod h1:s4Tq0KmD0yhPGHbZEwg1VPlH0vT/GBHJZorPzhcxBUE= gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
gorm.io/driver/postgres v1.3.7 h1:FKF6sIMDHDEvvMF/XJvbnCl0nu6KSKUaPXevJ4r+VYQ= gorm.io/driver/postgres v1.4.5 h1:mTeXTTtHAgnS9PgmhN2YeUbazYpLhUI1doLnw42XUZc=
gorm.io/driver/postgres v1.3.7/go.mod h1:f02ympjIcgtHEGFMZvdgTxODZ9snAHDb4hXfigBVuNI= gorm.io/driver/postgres v1.4.5/go.mod h1:GKNQYSJ14qvWkvPwXljMGehpKrhlDNsqYRr5HnYGncg=
gorm.io/driver/sqlite v1.3.2 h1:nWTy4cE52K6nnMhv23wLmur9Y3qWbZvOBz+V4PrGAxg= gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
gorm.io/driver/sqlite v1.3.2/go.mod h1:B+8GyC9K7VgzJAcrcXMRPdnMcck+8FgJynEehEPM16U= gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM= gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= 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-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-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/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 ( import (
"bytes" "bytes"
"fmt" "fmt"
"io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
@ -16,7 +15,7 @@ import (
// TempFile creates temp file with passed data // TempFile creates temp file with passed data
func TempFile(data string) *os.File { func TempFile(data string) *os.File {
f, err := ioutil.TempFile("", "prefix") f, err := os.CreateTemp("", "prefix")
if err != nil { if err != nil {
log.Log().Fatal(err) log.Log().Fatal(err)
} }
@ -40,7 +39,8 @@ func TestServer(data string) *httptest.Server {
} }
// DoGetRequest performs a GET request // 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) r, _ := http.NewRequest("GET", url, nil)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
@ -48,7 +48,7 @@ func DoGetRequest(url string, fn func(w http.ResponseWriter, r *http.Request)) (
handler.ServeHTTP(rr, r) handler.ServeHTTP(rr, r)
return rr.Code, rr.Body return rr, rr.Body
} }
// BeDNSRecord returns new dns matcher // 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() { It("Should perform a retry until max retry attempt count is reached and return TransientError", func() {
reader, err := sut.DownloadFile(server.URL) reader, err := sut.DownloadFile(server.URL)
Expect(err).Should(HaveOccurred()) Expect(err).Should(HaveOccurred())
var transientErr *TransientError
Expect(errors.As(err, &transientErr)).To(BeTrue()) err2 := unwrapTransientErr(err)
Expect(transientErr.Unwrap().Error()).Should(ContainSubstring("Timeout"))
Expect(err2.Error()).Should(ContainSubstring("Timeout"))
Expect(reader).Should(BeNil()) Expect(reader).Should(BeNil())
// failed download event was emitted 3 times // failed download event was emitted 3 times
@ -196,15 +197,18 @@ var _ = Describe("Downloader", func() {
When("DNS resolution of passed URL fails", func() { When("DNS resolution of passed URL fails", func() {
BeforeEach(func() { BeforeEach(func() {
sut = NewDownloader( sut = NewDownloader(
WithTimeout(100*time.Millisecond), WithTimeout(500*time.Millisecond),
WithAttempts(3), 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() { 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") reader, err := sut.DownloadFile("http://some.domain.which.does.not.exist")
Expect(err).Should(HaveOccurred()) Expect(err).Should(HaveOccurred())
err2 := unwrapTransientErr(err)
var dnsError *net.DNSError 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()) Expect(reader).Should(BeNil())
// failed download event was emitted 3 times // 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 package lists
//go:generate go-enum -f=$GOFILE --marshal --names //go:generate go run github.com/abice/go-enum -f=$GOFILE --marshal --names
import ( import (
"bufio" "bufio"
"errors" "errors"
@ -95,7 +95,7 @@ func (b *ListCache) Configuration() (result []string) {
// NewListCache creates new list instance // NewListCache creates new list instance
func NewListCache(t ListCacheType, groupToLinks map[string][]string, refreshPeriod time.Duration, 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) groupCaches := make(map[string]stringcache.StringCache)
if processingConcurrency == 0 { if processingConcurrency == 0 {
@ -110,7 +110,13 @@ func NewListCache(t ListCacheType, groupToLinks map[string][]string, refreshPeri
listType: t, listType: t,
processingConcurrency: processingConcurrency, processingConcurrency: processingConcurrency,
} }
initError := b.refresh(true)
var initError error
if async {
initError = nil
} else {
initError = b.refresh(true)
}
if initError == nil { if initError == nil {
go periodicUpdate(b) go periodicUpdate(b)

View File

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

View File

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

View File

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

View File

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

View File

@ -20,6 +20,8 @@ const (
FormatTypeJson FormatTypeJson
) )
var ErrInvalidFormatType = fmt.Errorf("not a valid FormatType, try [%s]", strings.Join(_FormatTypeNames, ", "))
const _FormatTypeName = "textjson" const _FormatTypeName = "textjson"
var _FormatTypeNames = []string{ var _FormatTypeNames = []string{
@ -57,7 +59,7 @@ func ParseFormatType(name string) (FormatType, error) {
if x, ok := _FormatTypeValue[name]; ok { if x, ok := _FormatTypeValue[name]; ok {
return x, nil 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. // MarshalText implements the text marshaller method.
@ -91,6 +93,8 @@ const (
LevelFatal LevelFatal
) )
var ErrInvalidLevel = fmt.Errorf("not a valid Level, try [%s]", strings.Join(_LevelNames, ", "))
const _LevelName = "infotracedebugwarnerrorfatal" const _LevelName = "infotracedebugwarnerrorfatal"
var _LevelNames = []string{ var _LevelNames = []string{
@ -140,7 +144,7 @@ func ParseLevel(name string) (Level, error) {
if x, ok := _LevelValue[name]; ok { if x, ok := _LevelValue[name]; ok {
return x, nil 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. // 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 package model
//go:generate go-enum -f=$GOFILE --marshal --names //go:generate go run github.com/abice/go-enum -f=$GOFILE --marshal --names
import ( import (
"net" "net"
"time" "time"
@ -17,6 +17,8 @@ import (
// CUSTOMDNS // the query was resolved by a custom rule // CUSTOMDNS // the query was resolved by a custom rule
// HOSTSFILE // the query was resolved by looking up the hosts file // HOSTSFILE // the query was resolved by looking up the hosts file
// FILTERED // the query was filtered by query type // 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 type ResponseType int

View File

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

View File

@ -2,6 +2,7 @@ package querylog
import ( import (
"fmt" "fmt"
"reflect"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -70,7 +71,7 @@ func newDatabaseWriter(target gorm.Dialector, logRetentionDays uint64,
} }
// Migrate the schema // 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) 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 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() { func (d *DatabaseWriter) periodicFlush() {
ticker := time.NewTicker(d.dbFlushPeriod) ticker := time.NewTicker(d.dbFlushPeriod)
defer ticker.Stop() defer ticker.Stop()

View File

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

View File

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

View File

@ -6,11 +6,10 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"os" "os"
"path/filepath"
"time" "time"
"github.com/0xERR0R/blocky/helpertest"
"github.com/0xERR0R/blocky/log" "github.com/0xERR0R/blocky/log"
"github.com/0xERR0R/blocky/model" "github.com/0xERR0R/blocky/model"
@ -21,14 +20,16 @@ import (
) )
var _ = Describe("FileWriter", func() { var _ = Describe("FileWriter", func() {
var tmpDir string var (
var err error tmpDir *helpertest.TmpFolder
BeforeEach(func() { err error
tmpDir, err = ioutil.TempDir("", "fileWriter") writer *FileWriter
Expect(err).Should(Succeed()) )
})
AfterEach(func() { JustBeforeEach(func() {
_ = os.RemoveAll(tmpDir) tmpDir = helpertest.NewTmpFolder("fileWriter")
Expect(tmpDir.Error).Should(Succeed())
DeferCleanup(tmpDir.Clean)
}) })
Describe("CSV writer", func() { Describe("CSV writer", func() {
@ -40,9 +41,10 @@ var _ = Describe("FileWriter", func() {
}) })
When("New log entry was created", func() { When("New log entry was created", func() {
It("should be logged in one file", 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()) Expect(err).Should(Succeed())
writer, _ := NewCSVWriter(tmpDir, false, 0)
res, err := util.NewMsgWithAnswer("example.com", 123, dns.Type(dns.TypeA), "123.124.122.122") res, err := util.NewMsgWithAnswer("example.com", 123, dns.Type(dns.TypeA), "123.124.122.122")
Expect(err).Should(Succeed()) 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")))) Eventually(func(g Gomega) int {
Expect(csvLines).Should(HaveLen(2)) 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() { 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()) Expect(err).Should(Succeed())
writer, _ := NewCSVWriter(tmpDir, true, 0)
res, err := util.NewMsgWithAnswer("example.com", 123, dns.Type(dns.TypeA), "123.124.122.122") res, err := util.NewMsgWithAnswer("example.com", 123, dns.Type(dns.TypeA), "123.124.122.122")
Expect(err).Should(Succeed()) 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")))) Eventually(func(g Gomega) int {
Expect(csvLines).Should(HaveLen(1)) 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")))) Eventually(func(g Gomega) int {
Expect(csvLines).Should(HaveLen(1)) return len(readCsv(tmpDir.JoinPath(
fmt.Sprintf("%s_client2.log", time.Now().Format("2006-01-02")))))
}).Should(Equal(1))
}) })
}) })
When("Cleanup is called", func() { When("Cleanup is called", func() {
It("should delete old files", func() { It("should delete old files", func() {
tmpDir, err = ioutil.TempDir("", "queryLoggingResolver") writer, err = NewCSVWriter(tmpDir.Path, false, 1)
Expect(err).Should(Succeed()) Expect(err).Should(Succeed())
writer, _ := NewCSVWriter(tmpDir, false, 1)
res, err := util.NewMsgWithAnswer("example.com", 123, dns.Type(dns.TypeA), "123.124.122.122") res, err := util.NewMsgWithAnswer("example.com", 123, dns.Type(dns.TypeA), "123.124.122.122")
Expect(err).Should(Succeed()) Expect(err).Should(Succeed())
@ -173,19 +182,27 @@ var _ = Describe("FileWriter", func() {
Reason: "Resolved", Reason: "Resolved",
RType: model.ResponseTypeRESOLVED, RType: model.ResponseTypeRESOLVED,
}, },
Start: time.Now().AddDate(0, 0, -2), Start: time.Now().AddDate(0, 0, -3),
DurationMs: 20, DurationMs: 20,
}) })
}) })
fmt.Println(tmpDir.Path)
files, err := ioutil.ReadDir(tmpDir) Eventually(func(g Gomega) int {
Expect(err).Should(Succeed()) filesCount, err := tmpDir.CountFiles()
Expect(files).Should(HaveLen(2)) g.Expect(err).Should(Succeed())
writer.CleanUp()
files, err = ioutil.ReadDir(tmpDir) return filesCount
Expect(err).Should(Succeed()) }, "20s", "1s").Should(Equal(2))
Expect(files).Should(HaveLen(1))
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) refreshPeriod := time.Duration(cfg.RefreshPeriod)
downloader := createDownloader(cfg, bootstrap) downloader := createDownloader(cfg, bootstrap)
blacklistMatcher, blErr := lists.NewListCache(lists.ListCacheTypeBlacklist, cfg.BlackLists, 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, whitelistMatcher, wlErr := lists.NewListCache(lists.ListCacheTypeWhitelist, cfg.WhiteLists,
refreshPeriod, downloader, cfg.ProcessingConcurrency) refreshPeriod, downloader, cfg.ProcessingConcurrency,
(cfg.StartStrategy == config.StartStrategyTypeFast))
whitelistOnlyGroups := determineWhitelistOnlyGroups(&cfg) whitelistOnlyGroups := determineWhitelistOnlyGroups(&cfg)
err = multierror.Append(err, blErr, wlErr).ErrorOrNil() err = multierror.Append(err, blErr, wlErr).ErrorOrNil()
if err != nil && cfg.FailStartOnListError { if err != nil && cfg.StartStrategy == config.StartStrategyTypeFailOnError {
return nil, err return nil, err
} }
@ -465,6 +467,9 @@ func (r *BlockingResolver) isGroupDisabled(group string) bool {
// returns groups which should be checked for client's request // returns groups which should be checked for client's request
func (r *BlockingResolver) groupsToCheckForClient(request *model.Request) []string { func (r *BlockingResolver) groupsToCheckForClient(request *model.Request) []string {
r.status.lock.RLock()
defer r.status.lock.RUnlock()
var groups []string var groups []string
// try client names // try client names
for _, cName := range request.ClientNames { for _, cName := range request.ClientNames {
@ -613,6 +618,9 @@ func (r *BlockingResolver) queryForFQIdentifierIPs(identifier string) (result []
} }
func (r *BlockingResolver) initFQDNIPCache() { func (r *BlockingResolver) initFQDNIPCache() {
r.status.lock.Lock()
defer r.status.lock.Unlock()
identifiers := make([]string, 0) identifiers := make([]string, 0)
for identifier := range r.clientGroupsBlock { for identifier := range r.clientGroupsBlock {

View File

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

View File

@ -26,7 +26,8 @@ var (
// Bootstrap allows resolving hostnames using the configured bootstrap DNS. // Bootstrap allows resolving hostnames using the configured bootstrap DNS.
type Bootstrap struct { type Bootstrap struct {
log *logrus.Entry log *logrus.Entry
startVerifyUpstream bool
resolver Resolver resolver Resolver
upstream Resolver // the upstream that's part of the above 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 // 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 // issue since they stay allocated until the process terminates
b = &Bootstrap{ b = &Bootstrap{
log: log, log: log,
upstreamIPs: ips, upstreamIPs: ips,
systemResolver: net.DefaultResolver, // allow replacing it during tests systemResolver: net.DefaultResolver, // allow replacing it during tests
startVerifyUpstream: cfg.StartVerifyUpstream,
} }
if upstream.IsDefault() { 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) { func (b *Bootstrap) resolveUpstream(r Resolver, host string) ([]net.IP, error) {
// Use system resolver if no bootstrap is configured // Use system resolver if no bootstrap is configured
if b.resolver == nil { if b.resolver == nil {
filteredQTypes := config.GetConfig().Filtering.QueryTypes cfg := config.GetConfig()
network := "ip"
if filteredQTypes.Contains(dns.Type(dns.TypeAAAA)) {
network = "ip4"
} else if filteredQTypes.Contains(dns.Type(dns.TypeA)) {
network = "ip6"
}
ctx := context.Background() ctx := context.Background()
timeout := config.GetConfig().UpstreamTimeout timeout := cfg.UpstreamTimeout
if timeout != 0 { if timeout != 0 {
var cancel context.CancelFunc var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(context.Background(), time.Duration(timeout)) ctx, cancel = context.WithTimeout(ctx, time.Duration(timeout))
defer cancel() defer cancel()
} }
return b.systemResolver.LookupIP(ctx, network, host) return b.systemResolver.LookupIP(ctx, cfg.ConnectIPVersion.Net(), host)
} }
if r == b.upstream { if r == b.upstream {
@ -145,14 +139,16 @@ func (b *Bootstrap) NewHTTPTransport() *http.Transport {
return nil, err return nil, err
} }
filteredQTypes := config.GetConfig().Filtering.QueryTypes connectIPVersion := config.GetConfig().ConnectIPVersion
var qTypes []dns.Type var qTypes []dns.Type
switch { 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)} 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)} qTypes = []dns.Type{dns.Type(dns.TypeAAAA)}
default: default:
qTypes = v4v6QTypes qTypes = v4v6QTypes

View File

@ -112,7 +112,7 @@ func (r *CachingResolver) onExpired(cacheKey string) (val interface{}, ttl time.
if response.Res.Rcode == dns.RcodeSuccess { if response.Res.Rcode == dns.RcodeSuccess {
evt.Bus().Publish(evt.CachingDomainPrefetched, domainName) 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 { } else {
util.LogOnError(fmt.Sprintf("can't prefetch '%s' ", domainName), err) 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 { if response.Res.Rcode == dns.RcodeSuccess {
// put value into cache // 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 { } else if response.Res.Rcode == dns.RcodeNameError {
if r.cacheTimeNegative > 0 { if r.cacheTimeNegative > 0 {
// put return code if NXDOMAIN // 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 { for _, a := range answer {
// if TTL < mitTTL -> adjust the value, set minTTL // if TTL < mitTTL -> adjust the value, set minTTL
if r.minCacheTimeSec > 0 { if r.minCacheTimeSec > 0 {
@ -264,10 +273,10 @@ func (r *CachingResolver) adjustTTLs(answer []dns.RR) (maxTTL uint32) {
} }
} }
if maxTTL < a.Header().Ttl { if max < a.Header().Ttl {
maxTTL = 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() { Describe("Negative cache (caching if upstream resolver returns NXDOMAIN)", func() {
When("Upstream resolver returns NXDOMAIN with caching", func() { Context("Caching if upstream resolver returns NXDOMAIN", func() {
BeforeEach(func() { When("Upstream resolver returns NXDOMAIN with caching", func() {
mockAnswer.Rcode = dns.RcodeNameError 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))
}) })
By("second request", func() { It("response should be cached", func() {
Eventually(func(g Gomega) { By("first request", func() {
resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA))) resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA)))
g.Expect(err).Should(Succeed()) Expect(err).Should(Succeed())
g.Expect(resp.RType).Should(Equal(ResponseTypeCACHED)) Expect(resp.RType).Should(Equal(ResponseTypeRESOLVED))
g.Expect(resp.Reason).Should(Equal("CACHED NEGATIVE")) Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError))
g.Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError)) Expect(m.Calls).Should(HaveLen(1))
// still one call to resolver })
g.Expect(m.Calls).Should(HaveLen(1))
}, "500ms").Should(Succeed()) 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() { Context("Caching if upstream resolver returns empty result", func() {
BeforeEach(func() { When("Upstream resolver returns empty result with caching", func() {
mockAnswer.Rcode = dns.RcodeNameError BeforeEach(func() {
sutConfig = config.CachingConfig{ mockAnswer.Rcode = dns.RcodeSuccess
CacheTimeNegative: config.Duration(time.Minute * -1), mockAnswer.Answer = make([]dns.RR, 0)
}
})
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() { It("response should be cached", func() {
Eventually(func(g Gomega) { By("first request", func() {
resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA))) resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA)))
g.Expect(err).Should(Succeed()) Expect(err).Should(Succeed())
g.Expect(resp.RType).Should(Equal(ResponseTypeRESOLVED)) Expect(resp.RType).Should(Equal(ResponseTypeRESOLVED))
g.Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError)) Expect(resp.Res.Rcode).Should(Equal(dns.RcodeSuccess))
g.Expect(m.Calls).Should(HaveLen(2)) Expect(m.Calls).Should(HaveLen(1))
}, "500ms").Should(Succeed()) })
})
})
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() { When("MX query will be performed", func() {
BeforeEach(func() { BeforeEach(func() {
mockAnswer, _ = util.NewMsgWithAnswer("google.de.", 180, dns.Type(dns.TypeMX), "10 alt1.aspmx.l.google.com.") 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.Rcode).Should(Equal(dns.RcodeSuccess))
Expect(resp.Res.Answer).Should(HaveLen(2)) Expect(resp.Res.Answer).Should(HaveLen(2))
Expect(resp.Res.Answer).Should(ContainElements( 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.123"),
BeDNSRecord("multiple.ips.", dns.TypeA, TTL, "192.168.143.125")) BeDNSRecord("multiple.ips.", dns.TypeA, TTL, "192.168.143.125")))
// will not delegate to next resolver // will not delegate to next resolver
m.AssertNotCalled(GinkgoT(), "Resolve", mock.Anything) 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(HaveLen(2))
Expect(resp.Res.Answer).Should(ContainElements( 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.", 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.", 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 // will not delegate to next resolver
m.AssertNotCalled(GinkgoT(), "Resolve", mock.Anything) 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" "github.com/sirupsen/logrus"
) )
//nolint:gochecknoglobals
var (
_, loopback4, _ = net.ParseCIDR("127.0.0.0/8")
loopback6 = net.ParseIP("::1")
)
const ( const (
hostsFileResolverLogger = "hosts_file_resolver" hostsFileResolverLogger = "hosts_file_resolver"
) )
type HostsFileResolver struct { type HostsFileResolver struct {
NextResolver NextResolver
HostsFilePath string HostsFilePath string
hosts []host hosts []host
ttl uint32 ttl uint32
refreshPeriod time.Duration refreshPeriod time.Duration
filterLoopback bool
} }
func (r *HostsFileResolver) handleReverseDNS(request *model.Request) *model.Response { 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 file path: %s", r.HostsFilePath))
result = append(result, fmt.Sprintf("hosts TTL: %d", r.ttl)) 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("hosts refresh period: %s", r.refreshPeriod.String()))
result = append(result, fmt.Sprintf("filter loopback addresses: %t", r.filterLoopback))
} else { } else {
result = []string{"deactivated"} result = []string{"deactivated"}
} }
@ -128,9 +136,10 @@ func (r *HostsFileResolver) Configuration() (result []string) {
func NewHostsFileResolver(cfg config.HostsFileConfig) ChainedResolver { func NewHostsFileResolver(cfg config.HostsFileConfig) ChainedResolver {
r := HostsFileResolver{ r := HostsFileResolver{
HostsFilePath: cfg.Filepath, HostsFilePath: cfg.Filepath,
ttl: uint32(time.Duration(cfg.HostsTTL).Seconds()), ttl: uint32(time.Duration(cfg.HostsTTL).Seconds()),
refreshPeriod: time.Duration(cfg.RefreshPeriod), refreshPeriod: time.Duration(cfg.RefreshPeriod),
filterLoopback: cfg.FilterLoopback,
} }
if err := r.parseHostsFile(); err != nil { if err := r.parseHostsFile(); err != nil {
@ -150,6 +159,7 @@ type host struct {
Aliases []string Aliases []string
} }
// nolint:funlen
func (r *HostsFileResolver) parseHostsFile() error { func (r *HostsFileResolver) parseHostsFile() error {
const minColumnCount = 2 const minColumnCount = 2
@ -197,6 +207,11 @@ func (r *HostsFileResolver) parseHostsFile() error {
h.IP = net.ParseIP(fields[0]) h.IP = net.ParseIP(fields[0])
h.Hostname = fields[1] h.Hostname = fields[1]
// Check if loopback
if r.filterLoopback && (loopback4.Contains(h.IP) || loopback6.Equal(h.IP)) {
continue
}
if len(fields) > minColumnCount { if len(fields) > minColumnCount {
for i := 2; i < len(fields); i++ { for i := 2; i < len(fields); i++ {
h.Aliases = append(h.Aliases, fields[i]) h.Aliases = append(h.Aliases, fields[i])

View File

@ -16,19 +16,29 @@ import (
var _ = Describe("HostsFileResolver", func() { var _ = Describe("HostsFileResolver", func() {
var ( var (
sut *HostsFileResolver sut *HostsFileResolver
m *MockResolver m *MockResolver
err error err error
resp *Response resp *Response
tmpDir *TmpFolder
tmpFile *TmpFile
) )
TTL := uint32(time.Now().Second()) TTL := uint32(time.Now().Second())
BeforeEach(func() { BeforeEach(func() {
tmpDir = NewTmpFolder("HostsFileResolver")
Expect(tmpDir.Error).Should(Succeed())
DeferCleanup(tmpDir.Clean)
tmpFile = writeHostFile(tmpDir)
Expect(tmpFile.Error).Should(Succeed())
cfg := config.HostsFileConfig{ cfg := config.HostsFileConfig{
Filepath: "../testdata/hosts.txt", Filepath: tmpFile.Path,
HostsTTL: config.Duration(time.Duration(TTL) * time.Second), HostsTTL: config.Duration(time.Duration(TTL) * time.Second),
RefreshPeriod: config.Duration(30 * time.Minute), RefreshPeriod: config.Duration(30 * time.Minute),
FilterLoopback: true,
} }
sut = NewHostsFileResolver(cfg).(*HostsFileResolver) sut = NewHostsFileResolver(cfg).(*HostsFileResolver)
m = &MockResolver{} m = &MockResolver{}
@ -79,8 +89,8 @@ var _ = Describe("HostsFileResolver", func() {
When("Hosts file can be located", func() { When("Hosts file can be located", func() {
It("should parse it successfully", func() { It("should parse it successfully", func() {
Expect(sut).Should(Not(BeNil())) Expect(sut).ShouldNot(BeNil())
Expect(sut.hosts).Should(HaveLen(7)) Expect(sut.hosts).Should(HaveLen(4))
}) })
}) })
@ -165,7 +175,7 @@ var _ = Describe("HostsFileResolver", func() {
When("hosts file is provided", func() { When("hosts file is provided", func() {
It("should return configuration", func() { It("should return configuration", func() {
c := sut.Configuration() 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 package resolver
import ( import (
"io/ioutil" "io"
"net" "net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -101,7 +101,7 @@ func TestBootstrap(response *dns.Msg) *Bootstrap {
func TestDOHUpstream(fn func(request *dns.Msg) (response *dns.Msg), func TestDOHUpstream(fn func(request *dns.Msg) (response *dns.Msg),
reqFn ...func(w http.ResponseWriter)) config.Upstream { reqFn ...func(w http.ResponseWriter)) config.Upstream {
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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) util.FatalOnError("can't read request: ", err)

View File

@ -10,6 +10,7 @@ import (
"github.com/0xERR0R/blocky/config" "github.com/0xERR0R/blocky/config"
"github.com/0xERR0R/blocky/model" "github.com/0xERR0R/blocky/model"
"github.com/0xERR0R/blocky/util" "github.com/0xERR0R/blocky/util"
"github.com/miekg/dns"
"github.com/mroth/weightedrand" "github.com/mroth/weightedrand"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -36,23 +37,56 @@ type requestResponse struct {
err error 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 // NewParallelBestResolver creates new resolver instance
func NewParallelBestResolver(upstreamResolvers map[string][]config.Upstream, bootstrap *Bootstrap) (Resolver, error) { 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 { 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) r, err := NewUpstreamResolver(u, bootstrap)
if err != nil { 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, 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 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() { Describe("Resolving result from fastest upstream resolver", func() {
var ( var (
sut Resolver sut Resolver
@ -310,6 +371,8 @@ var _ = Describe("ParallelBestResolver", Label("parallelBestResolver"), func() {
sut Resolver sut Resolver
) )
BeforeEach(func() { BeforeEach(func() {
config.GetConfig().StartVerifyUpstream = false
sut, _ = NewParallelBestResolver(map[string][]config.Upstream{upstreamDefaultCfgName: { sut, _ = NewParallelBestResolver(map[string][]config.Upstream{upstreamDefaultCfgName: {
{Host: "host1"}, {Host: "host1"},
{Host: "host2"}, {Host: "host2"},

View File

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

View File

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

View File

@ -11,6 +11,11 @@ import (
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
) )
const (
sampleOriginal = "test.original."
sampleRewritten = "test.rewritten."
)
var _ = Describe("RewriterResolver", func() { var _ = Describe("RewriterResolver", func() {
var ( var (
sut ChainedResolver sut ChainedResolver
@ -53,6 +58,11 @@ var _ = Describe("RewriterResolver", func() {
When("has rewrite", func() { When("has rewrite", func() {
var request *model.Request var request *model.Request
var expectNilAnswer bool
BeforeEach(func() {
expectNilAnswer = false
})
AfterEach(func() { AfterEach(func() {
request = newRequest(fqdnOriginal, dns.Type(dns.TypeA)) request = newRequest(fqdnOriginal, dns.Type(dns.TypeA))
@ -80,13 +90,17 @@ var _ = Describe("RewriterResolver", func() {
Expect(err).Should(Succeed()) Expect(err).Should(Succeed())
if resp != mNextResponse { if resp != mNextResponse {
Expect(resp.Res.Question[0].Name).Should(Equal(fqdnOriginal)) 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() { It("should modify names", func() {
fqdnOriginal = "test.original." fqdnOriginal = sampleOriginal
fqdnRewritten = "test.rewritten." fqdnRewritten = sampleRewritten
}) })
It("should modify subdomains", func() { It("should modify subdomains", func() {
@ -105,8 +119,9 @@ var _ = Describe("RewriterResolver", func() {
}) })
It("should call next resolver", func() { It("should call next resolver", func() {
fqdnOriginal = "test.original." fqdnOriginal = sampleOriginal
fqdnRewritten = "test.rewritten." fqdnRewritten = sampleRewritten
expectNilAnswer = true
// Make inner call the NoOpResolver // Make inner call the NoOpResolver
mInner.ResolveFn = func(req *model.Request) (*model.Response, error) { mInner.ResolveFn = func(req *model.Request) (*model.Response, error) {
@ -126,6 +141,54 @@ var _ = Describe("RewriterResolver", func() {
return mNextResponse, nil 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() { 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" "crypto/tls"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io"
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
@ -53,15 +53,21 @@ type dnsUpstreamClient struct {
type httpUpstreamClient struct { type httpUpstreamClient struct {
client *http.Client client *http.Client
host string
} }
func createUpstreamClient(cfg config.Upstream) upstreamClient { func createUpstreamClient(cfg config.Upstream) upstreamClient {
timeout := time.Duration(config.GetConfig().UpstreamTimeout) timeout := time.Duration(config.GetConfig().UpstreamTimeout)
tlsConfig := tls.Config{ tlsConfig := tls.Config{
ServerName: cfg.Host, ServerName: cfg.Host,
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
} }
if cfg.CommonName != "" {
tlsConfig.ServerName = cfg.CommonName
}
switch cfg.Net { switch cfg.Net {
case config.NetProtocolHttps: case config.NetProtocolHttps:
return &httpUpstreamClient{ return &httpUpstreamClient{
@ -73,6 +79,7 @@ func createUpstreamClient(cfg config.Upstream) upstreamClient {
}, },
Timeout: timeout, Timeout: timeout,
}, },
host: cfg.Host,
} }
case config.NetProtocolTcpTls: 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 { 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, 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("User-Agent", config.GetConfig().DoHUserAgent)
req.Header.Set("Content-Type", dnsContentType) req.Header.Set("Content-Type", dnsContentType)
req.Host = r.host
httpResponse, err := r.client.Do(req) httpResponse, err := r.client.Do(req)
if err != nil { if err != nil {
@ -147,7 +156,7 @@ func (r *httpUpstreamClient) callExternal(msg *dns.Msg,
dnsContentType, contentType) dnsContentType, contentType)
} }
body, err := ioutil.ReadAll(httpResponse.Body) body, err := io.ReadAll(httpResponse.Body)
if err != nil { if err != nil {
return nil, 0, fmt.Errorf("can't read response body: %w", err) 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 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))) 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() { It("should return error", func() {
_, err := sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeA))) _, err := sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeA)))
Expect(err).Should(HaveOccurred()) 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 ( import (
"bytes" "bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand" "crypto/rand"
"crypto/rsa"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"math"
"math/big" "math/big"
mrand "math/rand" mrand "math/rand"
"net" "net"
@ -36,7 +38,6 @@ const (
maxUDPBufferSize = 65535 maxUDPBufferSize = 65535
caExpiryYears = 10 caExpiryYears = 10
certExpiryYears = 5 certExpiryYears = 5
certRSAsize = 4096
) )
// Server controls the endpoints for DNS and HTTP // Server controls the endpoints for DNS and HTTP
@ -295,7 +296,7 @@ func createUDPServer(address string) (*dns.Server, error) {
func createSelfSignedCert() (tls.Certificate, error) { func createSelfSignedCert() (tls.Certificate, error) {
// Create CA // Create CA
ca := &x509.Certificate{ 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(), NotBefore: time.Now(),
NotAfter: time.Now().AddDate(caExpiryYears, 0, 0), NotAfter: time.Now().AddDate(caExpiryYears, 0, 0),
IsCA: true, IsCA: true,
@ -304,7 +305,7 @@ func createSelfSignedCert() (tls.Certificate, error) {
BasicConstraintsValid: true, BasicConstraintsValid: true,
} }
caPrivKey, err := rsa.GenerateKey(rand.Reader, certRSAsize) caPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil { if err != nil {
return tls.Certificate{}, err return tls.Certificate{}, err
} }
@ -323,16 +324,22 @@ func createSelfSignedCert() (tls.Certificate, error) {
} }
caPrivKeyPEM := new(bytes.Buffer) caPrivKeyPEM := new(bytes.Buffer)
b, err := x509.MarshalECPrivateKey(caPrivKey)
if err != nil {
return tls.Certificate{}, err
}
if err = pem.Encode(caPrivKeyPEM, &pem.Block{ if err = pem.Encode(caPrivKeyPEM, &pem.Block{
Type: "RSA PRIVATE KEY", Type: "EC PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey), Bytes: b,
}); err != nil { }); err != nil {
return tls.Certificate{}, err return tls.Certificate{}, err
} }
// Create certificate // Create certificate
cert := &x509.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{"*"}, DNSNames: []string{"*"},
NotBefore: time.Now(), NotBefore: time.Now(),
NotAfter: time.Now().AddDate(certExpiryYears, 0, 0), NotAfter: time.Now().AddDate(certExpiryYears, 0, 0),
@ -341,7 +348,7 @@ func createSelfSignedCert() (tls.Certificate, error) {
KeyUsage: x509.KeyUsageDigitalSignature, KeyUsage: x509.KeyUsageDigitalSignature,
} }
certPrivKey, err := rsa.GenerateKey(rand.Reader, certRSAsize) certPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil { if err != nil {
return tls.Certificate{}, err return tls.Certificate{}, err
} }
@ -360,9 +367,15 @@ func createSelfSignedCert() (tls.Certificate, error) {
} }
certPrivKeyPEM := new(bytes.Buffer) certPrivKeyPEM := new(bytes.Buffer)
b, err = x509.MarshalECPrivateKey(certPrivKey)
if err != nil {
return tls.Certificate{}, err
}
if err = pem.Encode(certPrivKeyPEM, &pem.Block{ if err = pem.Encode(certPrivKeyPEM, &pem.Block{
Type: "RSA PRIVATE KEY", Type: "EC PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey), Bytes: b,
}); err != nil { }); err != nil {
return tls.Certificate{}, err return tls.Certificate{}, err
} }
@ -397,7 +410,9 @@ func createQueryResolver(
r = resolver.Chain( r = resolver.Chain(
resolver.NewFilteringResolver(cfg.Filtering), resolver.NewFilteringResolver(cfg.Filtering),
resolver.NewFqdnOnlyResolver(*cfg),
clientNamesResolver, clientNamesResolver,
resolver.NewEdeResolver(cfg.Ede),
resolver.NewQueryLoggingResolver(cfg.QueryLog), resolver.NewQueryLoggingResolver(cfg.QueryLog),
resolver.NewMetricsResolver(cfg.Prometheus), resolver.NewMetricsResolver(cfg.Prometheus),
resolver.NewRewriterResolver(cfg.CustomDNS.RewriteConfig, resolver.NewCustomDNSResolver(cfg.CustomDNS)), resolver.NewRewriterResolver(cfg.CustomDNS.RewriteConfig, resolver.NewCustomDNSResolver(cfg.CustomDNS)),
@ -405,6 +420,7 @@ func createQueryResolver(
blockingResolver, blockingResolver,
resolver.NewCachingResolver(cfg.Caching, redisClient), resolver.NewCachingResolver(cfg.Caching, redisClient),
resolver.NewRewriterResolver(cfg.Conditional.RewriteConfig, conditionalUpstreamResolver), resolver.NewRewriterResolver(cfg.Conditional.RewriteConfig, conditionalUpstreamResolver),
resolver.NewSpecialUseDomainNamesResolver(),
parallelResolver, parallelResolver,
) )
@ -467,6 +483,12 @@ func toMB(b uint64) uint64 {
return b / bytesInKB / bytesInKB return b / bytesInKB / bytesInKB
} }
const (
readHeaderTimeout = 20 * time.Second
readTimeout = 20 * time.Second
writeTimeout = 20 * time.Second
)
// Start starts the server // Start starts the server
func (s *Server) Start(errCh chan<- error) { func (s *Server) Start(errCh chan<- error) {
logger().Info("Starting server") logger().Info("Starting server")
@ -488,7 +510,14 @@ func (s *Server) Start(errCh chan<- error) {
go func() { go func() {
logger().Infof("http server is up and running on addr/port %s", address) 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) 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) logger().Infof("https server is up and running on addr/port %s", address)
server := http.Server{ server := http.Server{
Handler: s.httpsMux, Handler: s.httpsMux,
ReadTimeout: readTimeout,
ReadHeaderTimeout: readHeaderTimeout,
WriteTimeout: writeTimeout,
//nolint:gosec //nolint:gosec
TLSConfig: &tls.Config{ TLSConfig: &tls.Config{
MinVersion: minTLSVersion(), MinVersion: minTLSVersion(),

View File

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

View File

@ -4,7 +4,7 @@ import (
"bytes" "bytes"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"io/ioutil" "io"
"net" "net"
"net/http" "net/http"
"strings" "strings"
@ -61,8 +61,15 @@ var _ = BeforeSuite(func() {
DeferCleanup(fritzboxMockUpstream.Close) DeferCleanup(fritzboxMockUpstream.Close)
clientMockUpstream := resolver.NewMockUDPUpstreamServer().WithAnswerFn(func(request *dns.Msg) (response *dns.Msg) { 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( 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()) Expect(err).Should(Succeed())
@ -75,6 +82,28 @@ var _ = BeforeSuite(func() {
upstreamFritzbox = fritzboxMockUpstream.Start() upstreamFritzbox = fritzboxMockUpstream.Start()
upstreamGoogle = googleMockUpstream.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 // create server
sut, err = NewServer(&config.Config{ sut, err = NewServer(&config.Config{
CustomDNS: config.CustomDNSConfig{ CustomDNS: config.CustomDNSConfig{
@ -97,13 +126,13 @@ var _ = BeforeSuite(func() {
Blocking: config.BlockingConfig{ Blocking: config.BlockingConfig{
BlackLists: map[string][]string{ BlackLists: map[string][]string{
"ads": { "ads": {
"../testdata/doubleclick.net.txt", doubleclickFile.Path,
"../testdata/www.bild.de.txt", bildFile.Path,
"../testdata/heise.de.txt"}, heiseFile.Path},
"youtube": {"../testdata/youtube.com.txt"}}, "youtube": {youtubeFile.Path}},
WhiteLists: map[string][]string{ WhiteLists: map[string][]string{
"ads": {"../testdata/heise.de.txt"}, "ads": {heiseFile.Path},
"whitelist": {"../testdata/heise.de.txt"}, "whitelist": {heiseFile.Path},
}, },
ClientGroupsBlock: map[string][]string{ ClientGroupsBlock: map[string][]string{
"default": {"ads"}, "default": {"ads"},
@ -123,8 +152,8 @@ var _ = BeforeSuite(func() {
DNSPorts: config.ListenConfig{"55555"}, DNSPorts: config.ListenConfig{"55555"},
TLSPorts: config.ListenConfig{"8853"}, TLSPorts: config.ListenConfig{"8853"},
CertFile: "../testdata/cert.pem", CertFile: certPem.Path,
KeyFile: "../testdata/key.pem", KeyFile: keyPem.Path,
HTTPPorts: config.ListenConfig{"4000"}, HTTPPorts: config.ListenConfig{"4000"},
HTTPSPorts: config.ListenConfig{"4443"}, HTTPSPorts: config.ListenConfig{"4443"},
Prometheus: config.PrometheusConfig{ Prometheus: config.PrometheusConfig{
@ -290,18 +319,19 @@ var _ = Describe("Running DNS server", func() {
Describe("Prometheus endpoint", func() { Describe("Prometheus endpoint", func() {
When("Prometheus URL is called", func() { When("Prometheus URL is called", func() {
It("should return prometheus data", 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(err).Should(Succeed())
Expect(r.StatusCode).Should(Equal(http.StatusOK)) Expect(resp).Should(HaveHTTPStatus(http.StatusOK))
}) })
}) })
}) })
Describe("Root endpoint", func() { Describe("Root endpoint", func() {
When("Root URL is called", func() { When("Root URL is called", func() {
It("should return root page", 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(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()) Expect(err).Should(Succeed())
defer resp.Body.Close() 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 var result api.QueryResult
err = json.NewDecoder(resp.Body).Decode(&result) err = json.NewDecoder(resp.Body).Decode(&result)
@ -384,7 +415,9 @@ var _ = Describe("Running DNS server", func() {
defer resp.Body.Close() defer resp.Body.Close()
Expect(resp).Should(HaveHTTPStatus(http.StatusOK)) 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()) Expect(err).Should(Succeed())
msg := new(dns.Msg) msg := new(dns.Msg)
@ -446,7 +479,8 @@ var _ = Describe("Running DNS server", func() {
Expect(err).Should(Succeed()) Expect(err).Should(Succeed())
defer resp.Body.Close() defer resp.Body.Close()
Expect(resp).Should(HaveHTTPStatus(http.StatusOK)) 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()) Expect(err).Should(Succeed())
msg = new(dns.Msg) msg = new(dns.Msg)
@ -465,7 +499,8 @@ var _ = Describe("Running DNS server", func() {
Expect(err).Should(Succeed()) Expect(err).Should(Succeed())
defer resp.Body.Close() defer resp.Body.Close()
Expect(resp).Should(HaveHTTPStatus(http.StatusOK)) 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()) Expect(err).Should(Succeed())
msg = new(dns.Msg) msg = new(dns.Msg)
@ -522,7 +557,7 @@ var _ = Describe("Running DNS server", func() {
Expect(cErr).Should(Succeed()) Expect(cErr).Should(Succeed())
cfg.Upstream.ExternalResolvers = map[string][]config.Upstream{ 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" cfg.Redis.Address = "test-fail"
}) })
@ -652,7 +687,7 @@ var _ = Describe("Running DNS server", func() {
Expect(cErr).Should(Succeed()) Expect(cErr).Should(Succeed())
cfg.Upstream.ExternalResolvers = map[string][]config.Upstream{ 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() { 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 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" Version = "undefined"
// BuildTime build time of the binary // BuildTime build time of the binary
BuildTime = "undefined" BuildTime = "undefined"
// Architecture current CPU architecture
Architecture = "undefined"
) )

View File

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