diff --git a/.dockerignore b/.dockerignore index 6868ecff..26d18a8d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,13 @@ -bin/ -dist/ +bin +dist +site +docs +node_modules +.git .idea .github -testdata/ -node_modules/ \ No newline at end of file +.vscode +.gitignore +*.md +LICENSE +vendor \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 75bc54a8..224aa601 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,3 +7,8 @@ updates: open-pull-requests-limit: 10 assignees: - 0xERR0R + +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: daily diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index a03110c9..839a2469 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -1,45 +1,35 @@ name: CI Build on: [push, pull_request] jobs: - - build: - name: Build + make: + name: Test runs-on: ubuntu-latest + strategy: + matrix: + make: [build, test, race, docker-build, goreleaser] steps: + - name: Check out code into the Go module directory + uses: actions/checkout@v3 - - name: Set up Go 1.18 - uses: actions/setup-go@v1 + - name: Set up Go + uses: actions/setup-go@v3 with: - go-version: 1.18 + go-version-file: go.mod id: go - - name: Check out code into the Go module directory - uses: actions/checkout@v1 - - name: Get dependencies - run: | - go get -v -t -d ./... - if [ -f Gopkg.toml ]; then - curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh - dep ensure - fi + run: go mod download - - name: Build - run: make build - - - name: Test - run: make test - - - name: Race detection - run: make race + - name: make ${{ matrix.make }} + run: make ${{ matrix.make }} + if: matrix.make != 'goreleaser' - name: Upload results to codecov - run: bash <(curl -s https://codecov.io/bash) -t 48d6a1a8-a66e-4f27-9cc1-a7b91c4209b2 - - - name: Docker images - run: make docker-build + uses: codecov/codecov-action@v3 + if: matrix.make == 'test' - name: Check GoReleaser configuration - uses: goreleaser/goreleaser-action@v2 + uses: goreleaser/goreleaser-action@v3 + if: matrix.make == 'goreleaser' with: args: check diff --git a/.github/workflows/close_stale.yml b/.github/workflows/close_stale.yml new file mode 100644 index 00000000..e89afb88 --- /dev/null +++ b/.github/workflows/close_stale.yml @@ -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 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 67242225..a34dd2b9 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,11 +35,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -50,7 +50,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -64,4 +64,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/development-docker.yml b/.github/workflows/development-docker.yml index 5ab177a3..955d0d6c 100644 --- a/.github/workflows/development-docker.yml +++ b/.github/workflows/development-docker.yml @@ -1,55 +1,207 @@ name: Development docker build + on: push: branches: - development - fb-* +permissions: + security-events: write + actions: read + contents: read + packages: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: - docker: - if: github.repository_owner == '0xERR0R' + check: + name: Check if workflow should run runs-on: ubuntu-latest + outputs: + enabled: ${{ steps.check.outputs.enabled }} + steps: + - name: Enabled Check + id: check + shell: bash + run: | + ENABLED=${{ secrets.DEVELOPMENT_DOCKER }} + + if [[ "${{ github.repository_owner }}" == "0xERR0R" ]]; then + ENABLED="true" + fi + + if [[ "${ENABLED,,}" != "true" ]]; then + echo "enabled=0" >> $GITHUB_OUTPUT + + echo "Workflow is disabled" + + echo "### Workflow is disabled" >> $GITHUB_STEP_SUMMARY + echo "To enable this workflow by creating a secret 'DEVELOPMENT_DOCKER' with the value 'true'" >> $GITHUB_STEP_SUMMARY + else + echo "enabled=1" >> $GITHUB_OUTPUT + + echo "Workflow is enabled" + fi + + docker: + name: Build Docker image + runs-on: ubuntu-latest + needs: check + if: ${{ needs.check.outputs.enabled == 1 }} + outputs: + repository: ${{ steps.get_vars.outputs.repository }} + branch: ${{ steps.get_vars.outputs.branch }} steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 + - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 + with: + platforms: arm,arm64 + - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 + + - name: Get registry token + id: get_token + shell: bash + run: | + if [ "${{ secrets.CR_PAT }}" ]; then + echo "token=${{ secrets.CR_PAT }}" >> $GITHUB_OUTPUT + else + echo "token=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_OUTPUT + fi + - name: Login to GitHub Container Registry - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.repository_owner }} - password: ${{ secrets.CR_PAT }} + password: ${{ steps.get_token.outputs.token }} + - name: Login to DockerHub - uses: docker/login-action@v1 + if: github.repository_owner == '0xERR0R' + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Extract branch name + + - name: Populate build variables + id: get_vars shell: bash - run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" - id: extract_branch + run: | + REPOSITORY=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]') + echo "repository=${REPOSITORY}" >> $GITHUB_OUTPUT + echo "REPOSITORY: ${REPOSITORY}" + + BRANCH=${GITHUB_REF#refs/heads/} + echo "branch=${BRANCH}" >> $GITHUB_OUTPUT + echo "Branch: ${BRANCH}" + + VERSION=$(git describe --always --tags) + echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "VERSION: ${VERSION}" + + BUILD_TIME=$(date '+%Y%m%d-%H%M%S') + echo "build_time=${BUILD_TIME}" >> $GITHUB_OUTPUT + echo "BUILD_TIME: ${BUILD_TIME}" + + TAGS="ghcr.io/${REPOSITORY}:${BRANCH}" + if [[ "${{ github.repository_owner }}" == "0xERR0R" ]]; then + TAGS="${TAGS} , spx01/blocky:${BRANCH}" + fi + echo "tags=${TAGS}" >> $GITHUB_OUTPUT + echo "TAGS: ${TAGS}" + - name: Build and push - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: context: . platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 push: true - tags: | - ghcr.io/0xerr0r/blocky:${{ steps.extract_branch.outputs.branch }} - spx01/blocky:${{ steps.extract_branch.outputs.branch }} - - name: Scan image - uses: anchore/scan-action@v3 - id: scan + tags: ${{ steps.get_vars.outputs.tags }} + build-args: | + VERSION=${{ steps.get_vars.outputs.version }} + BUILD_TIME=${{ steps.get_vars.outputs.build_time }} + cache-from: type=gha + cache-to: type=gha,mode=max + + repo-scan: + name: Repo vulnerability scan + runs-on: ubuntu-latest + needs: check + if: needs.check.outputs.enabled == 1 + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Run Trivy vulnerability scanner in repo mode + uses: aquasecurity/trivy-action@master with: - image: "spx01/blocky:${{ steps.extract_branch.outputs.branch }}" - fail-build: false - acs-report-enable: true - - name: upload Anchore scan SARIF report - uses: github/codeql-action/upload-sarif@v1 + scan-type: 'fs' + ignore-unfixed: true + format: 'sarif' + output: 'trivy-repo-results.sarif' + severity: 'CRITICAL' + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v2 with: - sarif_file: ${{ steps.scan.outputs.sarif }} + sarif_file: 'trivy-repo-results.sarif' + + image-scan: + name: Image vulnerability scan + runs-on: ubuntu-latest + needs: docker + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Run Trivy vulnerability scanner on Docker image + uses: aquasecurity/trivy-action@master + with: + image-ref: 'ghcr.io/${{ needs.docker.outputs.repository }}:${{ needs.docker.outputs.branch }}' + format: 'sarif' + output: 'trivy-image-results.sarif' + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: 'trivy-image-results.sarif' + + image-test: + name: Test docker images + runs-on: ubuntu-latest + needs: docker + steps: + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + with: + platforms: arm,arm64 + + - name: Test images + shell: bash + run: | + echo '::group::Version for linux/amd64' + docker run --rm ghcr.io/${{ needs.docker.outputs.repository }}:${{ needs.docker.outputs.branch }} version + echo '::endgroup::' + + echo '::group::Version for linux/arm/v6' + docker run --platform linux/arm/v6 --rm ghcr.io/${{ needs.docker.outputs.repository }}:${{ needs.docker.outputs.branch }} version + echo '::endgroup::' + + echo '::group::Version for linux/arm/v7' + docker run --platform linux/arm/v7 --rm ghcr.io/${{ needs.docker.outputs.repository }}:${{ needs.docker.outputs.branch }} version + echo '::endgroup::' + + echo '::group::Version for linux/arm64' + docker run --platform linux/arm64 --rm ghcr.io/${{ needs.docker.outputs.repository }}:${{ needs.docker.outputs.branch }} version + echo '::endgroup::' \ No newline at end of file diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e1c5b0c7..63565d8f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -8,8 +8,8 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 with: python-version: 3.x - run: pip install mkdocs-material diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 1f6a67c6..a67e7304 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -9,12 +9,12 @@ jobs: name: lint runs-on: ubuntu-latest steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 with: - go-version: '1.18' - - uses: actions/checkout@v2 + go-version-file: go.mod + - uses: actions/checkout@v3 + - name: golangci-lint - uses: golangci/golangci-lint-action@v3 - with: - version: v1.46.2 - args: --timeout 5m0s + run: make lint \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fb559b9d..019542a6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,21 +7,20 @@ on: jobs: build: - runs-on: ubuntu-latest - + if: github.repository_owner == '0xERR0R' steps: - - name: Set up Go 1.18 - uses: actions/setup-go@v1 - with: - go-version: 1.18 - id: go - - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version-file: go.mod + id: go + - name: Build run: make build @@ -30,40 +29,59 @@ jobs: - name: Docker meta id: docker_meta - uses: crazy-max/ghaction-docker-meta@v1 + uses: crazy-max/ghaction-docker-meta@v4 with: images: spx01/blocky,ghcr.io/0xerr0r/blocky - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 + with: + platforms: arm,arm64 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 - name: Login to GitHub Container Registry - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.CR_PAT }} - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} + - name: Populate build variables + id: get_vars + shell: bash + run: | + VERSION=$(git describe --always --tags) + echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "VERSION: ${VERSION}" + + BUILD_TIME=$(date '+%Y%m%d-%H%M%S') + echo "build_time=${BUILD_TIME}" >> $GITHUB_OUTPUT + echo "BUILD_TIME: ${BUILD_TIME}" + - name: Build and push - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: context: . platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.docker_meta.outputs.tags }} labels: ${{ steps.docker_meta.outputs.labels }} + build-args: | + VERSION=${{ steps.get_vars.outputs.version }} + BUILD_TIME=${{ steps.get_vars.outputs.build_time }} + cache-from: type=gha + cache-to: type=gha,mode=max - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v2 + uses: goreleaser/goreleaser-action@v3 with: version: latest args: release --rm-dist diff --git a/.gitignore b/.gitignore index 1bcd1cfa..dc819864 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea/ +.vscode/ *.iml /*.pem bin/ @@ -13,3 +14,5 @@ todo.txt !docs/config.yml node_modules package-lock.json +.vscode/ +vendor/ \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index 6a7eef42..d7d2a570 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -3,7 +3,6 @@ linters: - asciicheck - bidichk - bodyclose - - deadcode - depguard - dogsled - dupl @@ -30,7 +29,6 @@ linters: - gosimple - govet - grouper - - ifshort - importas - ineffassign - lll @@ -41,25 +39,24 @@ linters: - nilerr - nilnil - nlreturn - - nolintlint + - nosprintfhostport - prealloc - predeclared - revive - sqlclosecheck - staticcheck - - structcheck - stylecheck - tenv - typecheck - unconvert - unparam - unused - - varcheck - wastedassign - whitespace - wsl disable: - noctx + - contextcheck - scopelint disable-all: false diff --git a/.goreleaser.yml b/.goreleaser.yml index 4ff57fb8..bb49054e 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -45,6 +45,7 @@ snapshot: checksum: name_template: "{{ .ProjectName }}_checksums.txt" changelog: + use: github sort: asc filters: exclude: diff --git a/Dockerfile b/Dockerfile index 25a0194b..a6a2c7ba 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,43 +1,79 @@ -# build stage -FROM golang:1.18-alpine AS build-env -RUN apk add --no-cache \ - git \ - make \ - gcc \ - libc-dev \ - zip \ - ca-certificates +# ----------- stage: ca-certs +# get newest certificates in seperate stage for caching +FROM --platform=$BUILDPLATFORM alpine:3.16 AS ca-certs +RUN apk add --no-cache ca-certificates -ENV GO111MODULE=on \ - CGO_ENABLED=0 - -WORKDIR /src +# update certificates and use the apk ones if update fails +RUN --mount=type=cache,target=/etc/ssl/certs \ + update-ca-certificates 2>/dev/null || true +# ----------- stage: zig-env +# zig compiler is used for CGO cross compilation +# even though CGO is disabled it is used in the os and net package +FROM --platform=$BUILDPLATFORM ghcr.io/euantorano/zig:master AS zig-env + +# ----------- stage: build +FROM --platform=$BUILDPLATFORM golang:1-alpine AS build + +# required arguments +ARG VERSION +ARG BUILD_TIME + +# auto provided by Docker +# https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope +ARG TARGETOS +ARG TARGETARCH +ARG TARGETVARIANT + +# set working directory +WORKDIR /go/src + +# download packages COPY go.mod go.sum ./ -RUN go mod download +RUN --mount=type=cache,target=/go/pkg \ + go mod download # add source -ADD . . +COPY . . -ARG opts -RUN env ${opts} make build +# setup go & zig as CGO compiler +COPY --from=zig-env /usr/local/bin/zig /usr/local/bin/zig +ENV PATH="/usr/local/bin/zig:${PATH}" \ + CC="zigcc" \ + CXX="zigcpp" \ + CGO_ENABLED=0 \ + GOOS="linux" \ + GOARCH=$TARGETARCH \ + GO_SKIP_GENERATE=1\ + GO_BUILD_FLAGS="-tags static -v " \ + BIN_USER=100\ + BIN_AUTOCAB=1 \ + BIN_OUT_DIR="/bin" -# final stage -FROM alpine:3.16 +# add make & libcap +RUN apk add --no-cache make libcap + +# build binary +RUN --mount=type=bind,target=. \ + --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg \ + make build GOARM=${TARGETVARIANT##*v} + +# ----------- stage: final +FROM scratch LABEL org.opencontainers.image.source="https://github.com/0xERR0R/blocky" \ org.opencontainers.image.url="https://github.com/0xERR0R/blocky" \ org.opencontainers.image.title="DNS proxy as ad-blocker for local network" -COPY --from=build-env /src/bin/blocky /app/blocky -RUN apk add --no-cache ca-certificates bind-tools tini tzdata libcap && \ - adduser -S -D -H -h /app -s /sbin/nologin blocky && \ - setcap 'cap_net_bind_service=+ep' /app/blocky - -HEALTHCHECK --interval=1m --timeout=3s CMD dig @127.0.0.1 -p 53 healthcheck.blocky +tcp +short || exit 1 - -USER blocky +USER 100 WORKDIR /app -ENTRYPOINT ["/sbin/tini", "--"] -CMD ["sh", "-c", "/app/blocky --config ${CONFIG_FILE:-/app/config.yml}"] +COPY --from=ca-certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=build /bin/blocky /app/blocky + +ENV BLOCKY_CONFIG_FILE=/app/config.yml + +ENTRYPOINT ["/app/blocky"] + +HEALTHCHECK --interval=1m --timeout=3s CMD ["/app/blocky", "healthcheck"] diff --git a/Makefile b/Makefile index c5fce451..419e4034 100644 --- a/Makefile +++ b/Makefile @@ -1,25 +1,36 @@ -#!/usr/bin/env bash +.PHONY: all clean build swagger test lint run fmt docker-build help +.DEFAULT_GOAL:=help -.PHONY: all clean build swagger test lint run help -.DEFAULT_GOAL := help +VERSION?=$(shell git describe --always --tags) +BUILD_TIME?=$(shell date '+%Y%m%d-%H%M%S') +DOCKER_IMAGE_NAME=spx01/blocky -VERSION := $(shell git describe --always --tags) -BUILD_TIME=$(shell date '+%Y%m%d-%H%M%S') -DOCKER_IMAGE_NAME="spx01/blocky" -BINARY_NAME=blocky -BIN_OUT_DIR=bin +BINARY_NAME:=blocky +BIN_OUT_DIR?=bin + +GOARCH?=$(shell go env GOARCH) +GOARM?=$(shell go env GOARM) + +GO_BUILD_FLAGS?=-v +GO_BUILD_LD_FLAGS:=\ + -w \ + -s \ + -X github.com/0xERR0R/blocky/util.Version=${VERSION} \ + -X github.com/0xERR0R/blocky/util.BuildTime=${BUILD_TIME} \ + -X github.com/0xERR0R/blocky/util.Architecture=${GOARCH}${GOARM} + +GO_BUILD_OUTPUT:=$(BIN_OUT_DIR)/$(BINARY_NAME)$(BINARY_SUFFIX) export PATH=$(shell go env GOPATH)/bin:$(shell echo $$PATH) all: build test lint ## Build binary (with tests) clean: ## cleans output directory - $(shell rm -rf $(BIN_OUT_DIR)/*) + rm -rf $(BIN_OUT_DIR)/* swagger: ## creates swagger documentation as html file - go install github.com/swaggo/swag/cmd/swag@v1.6.9 npm install bootprint bootprint-openapi html-inline - $(shell go env GOPATH)/bin/swag init -g api/api.go + go run github.com/swaggo/swag/cmd/swag init -g api/api.go $(shell) node_modules/bootprint/bin/bootprint.js openapi docs/swagger.json /tmp/swagger/ $(shell) node_modules/html-inline/bin/cmd.js /tmp/swagger/index.html > docs/swagger.html @@ -27,19 +38,30 @@ serve_docs: ## serves online docs mkdocs serve build: ## Build binary - go install github.com/abice/go-enum@v0.4.0 +ifdef GO_SKIP_GENERATE + $(info skipping go generate) +else go generate ./... - go build -v -ldflags="-w -s -X github.com/0xERR0R/blocky/util.Version=${VERSION} -X github.com/0xERR0R/blocky/util.BuildTime=${BUILD_TIME}" -o $(BIN_OUT_DIR)/$(BINARY_NAME)$(BINARY_SUFFIX) +endif + go build $(GO_BUILD_FLAGS) -ldflags="$(GO_BUILD_LD_FLAGS)" -o $(GO_BUILD_OUTPUT) +ifdef BIN_USER + $(info setting owner of $(GO_BUILD_OUTPUT) to $(BIN_USER)) + chown $(BIN_USER) $(GO_BUILD_OUTPUT) +endif +ifdef BIN_AUTOCAB + $(info setting cap_net_bind_service to $(GO_BUILD_OUTPUT)) + setcap 'cap_net_bind_service=+ep' $(GO_BUILD_OUTPUT) +endif test: ## run tests - go test -v -coverprofile=coverage.txt -covermode=atomic -cover ./... + go run github.com/onsi/ginkgo/v2/ginkgo -v --coverprofile=coverage.txt --covermode=atomic -cover ./... race: ## run tests with race detector - go test -race -short ./... + go run github.com/onsi/ginkgo/v2/ginkgo --race ./... -lint: build ## run golangcli-lint checks - go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.46.2 - $(shell go env GOPATH)/bin/golangci-lint run +lint: ## run golangcli-lint checks + go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.50.1 + golangci-lint run --timeout 5m run: build ## Build and run binary ./$(BIN_OUT_DIR)/$(BINARY_NAME) @@ -47,8 +69,15 @@ run: build ## Build and run binary fmt: ## gofmt and goimports all go files find . -name '*.go' | while read -r file; do gofmt -w -s "$$file"; goimports -w "$$file"; done -docker-build: ## Build docker image - docker build --network=host --tag ${DOCKER_IMAGE_NAME} . +docker-build: ## Build docker image + go generate ./... + docker buildx build \ + --build-arg VERSION=${VERSION} \ + --build-arg BUILD_TIME=${BUILD_TIME} \ + --network=host \ + -o type=docker \ + -t ${DOCKER_IMAGE_NAME} \ + . help: ## Shows help @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/api/api_endpoints.go b/api/api_endpoints.go index 975b1bf1..9ad78c30 100644 --- a/api/api_endpoints.go +++ b/api/api_endpoints.go @@ -12,6 +12,11 @@ import ( "github.com/go-chi/chi/v5" ) +const ( + contentTypeHeader = "content-type" + jsonContentType = "application/json" +) + // BlockingControl interface to control the blocking status type BlockingControl interface { EnableBlocking() @@ -57,7 +62,8 @@ func registerListRefreshEndpoints(router chi.Router, refresher ListRefresher) { // @Tags lists // @Success 200 "Lists were reloaded" // @Router /lists/refresh [post] -func (l *ListRefreshEndpoint) apiListRefresh(_ http.ResponseWriter, _ *http.Request) { +func (l *ListRefreshEndpoint) apiListRefresh(rw http.ResponseWriter, _ *http.Request) { + rw.Header().Set(contentTypeHeader, jsonContentType) l.refresher.RefreshLists() } @@ -75,10 +81,12 @@ func registerBlockingEndpoints(router chi.Router, control BlockingControl) { // @Tags blocking // @Success 200 "Blocking is enabled" // @Router /blocking/enable [get] -func (s *BlockingEndpoint) apiBlockingEnable(_ http.ResponseWriter, _ *http.Request) { +func (s *BlockingEndpoint) apiBlockingEnable(rw http.ResponseWriter, _ *http.Request) { log.Log().Info("enabling blocking...") s.control.EnableBlocking() + + rw.Header().Set(contentTypeHeader, jsonContentType) } // apiBlockingDisable is the http endpoint to disable the blocking status @@ -98,6 +106,8 @@ func (s *BlockingEndpoint) apiBlockingDisable(rw http.ResponseWriter, req *http. err error ) + rw.Header().Set(contentTypeHeader, jsonContentType) + // parse duration from query parameter durationParam := req.URL.Query().Get("duration") if len(durationParam) > 0 { @@ -132,6 +142,8 @@ func (s *BlockingEndpoint) apiBlockingDisable(rw http.ResponseWriter, req *http. func (s *BlockingEndpoint) apiBlockingStatus(rw http.ResponseWriter, _ *http.Request) { status := s.control.BlockingStatus() + rw.Header().Set(contentTypeHeader, jsonContentType) + response, err := json.Marshal(status) util.LogOnError("unable to marshal response ", err) diff --git a/api/api_endpoints_test.go b/api/api_endpoints_test.go index 2aae0f41..17564300 100644 --- a/api/api_endpoints_test.go +++ b/api/api_endpoints_test.go @@ -51,9 +51,9 @@ var _ = Describe("API tests", func() { r := &ListRefreshMock{} sut := &ListRefreshEndpoint{refresher: r} It("should trigger the list refresh", func() { - httpCode, _ := DoGetRequest("/api/lists/refresh", sut.apiListRefresh) - Expect(httpCode).Should(Equal(http.StatusOK)) - Expect(r.refreshTriggered).Should(BeTrue()) + resp, _ := DoGetRequest("/api/lists/refresh", sut.apiListRefresh) + Expect(resp).Should(HaveHTTPStatus(http.StatusOK)) + Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/json")) }) }) @@ -74,18 +74,18 @@ var _ = Describe("API tests", func() { It("should disable blocking resolver", func() { By("Calling Rest API to deactivate", func() { - httpCode, _ := DoGetRequest("/api/blocking/disable", sut.apiBlockingDisable) - Expect(httpCode).Should(Equal(http.StatusOK)) - Expect(bc.enabled).Should(BeFalse()) + resp, _ := DoGetRequest("/api/blocking/disable", sut.apiBlockingDisable) + Expect(resp).Should(HaveHTTPStatus(http.StatusOK)) + Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/json")) }) }) }) When("Disable blocking is called with a wrong parameter", func() { It("Should return http bad request as return code", func() { - httpCode, _ := DoGetRequest("/api/blocking/disable?duration=xyz", sut.apiBlockingDisable) - - Expect(httpCode).Should(Equal(http.StatusBadRequest)) + resp, _ := DoGetRequest("/api/blocking/disable?duration=xyz", sut.apiBlockingDisable) + Expect(resp).Should(HaveHTTPStatus(http.StatusBadRequest)) + Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/json")) }) }) @@ -96,8 +96,9 @@ var _ = Describe("API tests", func() { }) By("Calling Rest API to deactivate blocking for 0.5 sec", func() { - httpCode, _ := DoGetRequest("/api/blocking/disable?duration=500ms", sut.apiBlockingDisable) - Expect(httpCode).Should(Equal(http.StatusOK)) + resp, _ := DoGetRequest("/api/blocking/disable?duration=500ms", sut.apiBlockingDisable) + Expect(resp).Should(HaveHTTPStatus(http.StatusOK)) + Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/json")) }) By("ensure that the blocking is disabled", func() { @@ -110,13 +111,15 @@ var _ = Describe("API tests", func() { When("Blocking status is called", func() { It("should return correct status", func() { By("enable blocking via API", func() { - httpCode, _ := DoGetRequest("/api/blocking/enable", sut.apiBlockingEnable) - Expect(httpCode).Should(Equal(http.StatusOK)) + resp, _ := DoGetRequest("/api/blocking/enable", sut.apiBlockingEnable) + Expect(resp).Should(HaveHTTPStatus(http.StatusOK)) + Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/json")) }) By("Query blocking status via API should return 'enabled'", func() { - httpCode, body := DoGetRequest("/api/blocking/status", sut.apiBlockingStatus) - Expect(httpCode).Should(Equal(http.StatusOK)) + resp, body := DoGetRequest("/api/blocking/status", sut.apiBlockingStatus) + Expect(resp).Should(HaveHTTPStatus(http.StatusOK)) + Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/json")) var result BlockingStatus err := json.NewDecoder(body).Decode(&result) Expect(err).Should(Succeed()) @@ -125,13 +128,15 @@ var _ = Describe("API tests", func() { }) By("disable blocking via API", func() { - httpCode, _ := DoGetRequest("/api/blocking/disable?duration=500ms", sut.apiBlockingDisable) - Expect(httpCode).Should(Equal(http.StatusOK)) + resp, _ := DoGetRequest("/api/blocking/disable?duration=500ms", sut.apiBlockingDisable) + Expect(resp).Should(HaveHTTPStatus(http.StatusOK)) + Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/json")) }) By("Query blocking status via API again should return 'disabled'", func() { - httpCode, body := DoGetRequest("/api/blocking/status", sut.apiBlockingStatus) - Expect(httpCode).Should(Equal(http.StatusOK)) + resp, body := DoGetRequest("/api/blocking/status", sut.apiBlockingStatus) + Expect(resp).Should(HaveHTTPStatus(http.StatusOK)) + Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/json")) var result BlockingStatus err := json.NewDecoder(body).Decode(&result) diff --git a/cache/expirationcache/expiration_cache_test.go b/cache/expirationcache/expiration_cache_test.go index 30de44e6..f55e362c 100644 --- a/cache/expirationcache/expiration_cache_test.go +++ b/cache/expirationcache/expiration_cache_test.go @@ -120,8 +120,8 @@ var _ = Describe("Expiration cache", func() { val, ttl := cache.Get("key1") Expect(val).Should(Equal("val2")) Expect(ttl.Milliseconds()).Should(And( - BeNumerically(">", 900)), - BeNumerically("<=", 1000)) + BeNumerically(">", 900), + BeNumerically("<=", 1000))) }) It("should delete the key if function returns nil", func() { diff --git a/cmd/healthcheck.go b/cmd/healthcheck.go new file mode 100644 index 00000000..37358e08 --- /dev/null +++ b/cmd/healthcheck.go @@ -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 +} diff --git a/cmd/healthcheck_test.go b/cmd/healthcheck_test.go new file mode 100644 index 00000000..37f3f739 --- /dev/null +++ b/cmd/healthcheck_test.go @@ -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 +} diff --git a/cmd/lists.go b/cmd/lists.go index 4b99b03b..004f2083 100644 --- a/cmd/lists.go +++ b/cmd/lists.go @@ -2,7 +2,7 @@ package cmd import ( "fmt" - "io/ioutil" + "io" "net/http" "github.com/0xERR0R/blocky/api" @@ -39,7 +39,7 @@ func refreshList(_ *cobra.Command, _ []string) error { defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - body, _ := ioutil.ReadAll(resp.Body) + body, _ := io.ReadAll(resp.Body) return fmt.Errorf("response NOK, %s %s", resp.Status, string(body)) } diff --git a/cmd/query.go b/cmd/query.go index 9dcb3a40..751e707d 100644 --- a/cmd/query.go +++ b/cmd/query.go @@ -4,7 +4,7 @@ import ( "bytes" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "github.com/0xERR0R/blocky/api" @@ -53,7 +53,7 @@ func query(cmd *cobra.Command, args []string) error { defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - body, _ := ioutil.ReadAll(resp.Body) + body, _ := io.ReadAll(resp.Body) return fmt.Errorf("response NOK, %s %s", resp.Status, string(body)) } diff --git a/cmd/root.go b/cmd/root.go index dbe9adbe..3dd31efa 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,7 +2,9 @@ package cmd import ( "fmt" + "net" "os" + "strconv" "strings" "github.com/0xERR0R/blocky/config" @@ -20,9 +22,11 @@ var ( ) const ( - defaultPort = 4000 - defaultHost = "localhost" - defaultConfigPath = "./config.yml" + defaultPort = 4000 + defaultHost = "localhost" + defaultConfigPath = "./config.yml" + configFileEnvVar = "BLOCKY_CONFIG_FILE" + configFileEnvVarOld = "CONFIG_FILE" ) // NewRootCommand creates a new root cli command instance @@ -49,13 +53,14 @@ Complete documentation is available at https://github.com/0xERR0R/blocky`, NewVersionCommand(), newServeCommand(), newBlockingCommand(), - NewListsCommand()) + NewListsCommand(), + NewHealthcheckCommand()) return c } func apiURL(path string) string { - return fmt.Sprintf("http://%s:%d%s", apiHost, apiPort, path) + return fmt.Sprintf("http://%s%s", net.JoinHostPort(apiHost, strconv.Itoa(int(apiPort))), path) } //nolint:gochecknoinits @@ -64,6 +69,18 @@ func init() { } func initConfig() { + if configPath == defaultConfigPath { + val, present := os.LookupEnv(configFileEnvVar) + if present { + configPath = val + } else { + val, present = os.LookupEnv(configFileEnvVarOld) + if present { + configPath = val + } + } + } + cfg, err := config.LoadConfig(configPath, false) if err != nil { util.FatalOnError("unable to load configuration: ", err) diff --git a/cmd/root_test.go b/cmd/root_test.go index f0b39b4f..d2a7d42f 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -1,23 +1,73 @@ package cmd import ( - "io/ioutil" + "io" + "os" "github.com/0xERR0R/blocky/log" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + . "github.com/0xERR0R/blocky/helpertest" ) -var _ = Describe("Version command", func() { +var _ = Describe("root command", func() { When("Version command is called", func() { log.Log().ExitFunc = nil It("should execute without error", func() { c := NewRootCommand() - c.SetOutput(ioutil.Discard) + c.SetOutput(io.Discard) c.SetArgs([]string{"help"}) err := c.Execute() Expect(err).Should(Succeed()) }) }) + When("Config provided", func() { + var ( + tmpDir *TmpFolder + tmpFile *TmpFile + ) + + BeforeEach(func() { + configPath = defaultConfigPath + + tmpDir = NewTmpFolder("RootCommand") + Expect(tmpDir.Error).Should(Succeed()) + DeferCleanup(tmpDir.Clean) + + tmpFile = tmpDir.CreateStringFile("config", + "upstream:", + " default:", + " - 1.1.1.1", + "blocking:", + " blackLists:", + " ads:", + " - https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt", + " clientGroupsBlock:", + " default:", + " - ads", + "port: 5333", + ) + Expect(tmpFile.Error).Should(Succeed()) + }) + + It("should accept old env var", func() { + os.Setenv(configFileEnvVarOld, tmpFile.Path) + DeferCleanup(func() { os.Unsetenv(configFileEnvVarOld) }) + + initConfig() + + Expect(configPath).Should(Equal(tmpFile.Path)) + }) + + It("should accept new env var", func() { + os.Setenv(configFileEnvVar, tmpFile.Path) + DeferCleanup(func() { os.Unsetenv(configFileEnvVar) }) + + initConfig() + + Expect(configPath).Should(Equal(tmpFile.Path)) + }) + }) }) diff --git a/cmd/serve_test.go b/cmd/serve_test.go index e314c211..374192b2 100644 --- a/cmd/serve_test.go +++ b/cmd/serve_test.go @@ -1,11 +1,10 @@ package cmd import ( - "time" - "github.com/0xERR0R/blocky/config" . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" ) var _ = Describe("Serve command", func() { @@ -21,13 +20,18 @@ var _ = Describe("Serve command", func() { isConfigMandatory = false + grClosure := make(chan interface{}) + go func() { - _ = startServer(newServeCommand(), []string{}) + defer GinkgoRecover() + + err := startServer(newServeCommand(), []string{}) + Expect(err).Should(HaveOccurred()) + + close(grClosure) }() - time.Sleep(100 * time.Millisecond) - - done <- true + Eventually(grClosure).Should(BeClosed()) }) }) }) diff --git a/cmd/version.go b/cmd/version.go index efc82ce6..cb35416e 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -21,4 +21,5 @@ func printVersion(_ *cobra.Command, _ []string) { fmt.Println("blocky") fmt.Printf("Version: %s\n", util.Version) fmt.Printf("Build time: %s\n", util.BuildTime) + fmt.Printf("Architecture: %s\n", util.Architecture) } diff --git a/codecov.yml b/codecov.yml index 729cff40..d4a9cd77 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,3 +1,16 @@ +coverage: + status: + project: + default: + target: auto + threshold: 80% + patch: + default: + # basic + target: auto + threshold: 0% + base: auto + only_pulls: true ignore: - "resolver/mocks.go" - "**/*_enum.go" diff --git a/config/config.go b/config/config.go index 478a809d..cf76b78f 100644 --- a/config/config.go +++ b/config/config.go @@ -1,10 +1,9 @@ -//go:generate go-enum -f=$GOFILE --marshal --names +//go:generate go run github.com/abice/go-enum -f=$GOFILE --marshal --names package config import ( "errors" "fmt" - "io/ioutil" "net" "os" "path/filepath" @@ -12,6 +11,7 @@ import ( "sort" "strconv" "strings" + "sync" "time" "github.com/miekg/dns" @@ -36,6 +36,39 @@ const ( // ) type NetProtocol uint16 +// IPVersion represents IP protocol version(s). ENUM( +// dual // IPv4 and IPv6 +// v4 // IPv4 only +// v6 // IPv6 only +// ) +type IPVersion uint8 + +func (ipv IPVersion) Net() string { + switch ipv { + case IPVersionDual: + return "ip" + case IPVersionV4: + return "ip4" + case IPVersionV6: + return "ip6" + } + + panic(fmt.Errorf("bad value: %s", ipv)) +} + +func (ipv IPVersion) QTypes() []dns.Type { + switch ipv { + case IPVersionDual: + return []dns.Type{dns.Type(dns.TypeA), dns.Type(dns.TypeAAAA)} + case IPVersionV4: + return []dns.Type{dns.Type(dns.TypeA)} + case IPVersionV6: + return []dns.Type{dns.Type(dns.TypeAAAA)} + } + + panic(fmt.Errorf("bad value: %s", ipv)) +} + // QueryLogType type of the query log ENUM( // console // use logger as fallback // none // no logging @@ -46,6 +79,13 @@ type NetProtocol uint16 // ) type QueryLogType int16 +// StartStrategyType upstart strategy ENUM( +// blocking // synchronously download blocking lists on startup +// failOnError // synchronously download blocking lists on startup and shutdown on error +// fast // asyncronously download blocking lists on startup +// ) +type StartStrategyType uint16 + type QType dns.Type func (c QType) String() string { @@ -93,10 +133,11 @@ var netDefaultPort = map[NetProtocol]uint16{ // Upstream is the definition of external DNS server type Upstream struct { - Net NetProtocol - Host string - Port uint16 - Path string + Net NetProtocol + Host string + Port uint16 + Path string + CommonName string // Common Name to use for certificate verification; optional. "" uses .Host } // IsDefault returns true if u is the default value @@ -315,12 +356,14 @@ func (s *QTypeSet) UnmarshalYAML(unmarshal func(interface{}) error) error { var validDomain = regexp.MustCompile( `^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`) -// ParseUpstream creates new Upstream from passed string in format [net]:host[:port][/path] +// ParseUpstream creates new Upstream from passed string in format [net]:host[:port][/path][#commonname] func ParseUpstream(upstream string) (Upstream, error) { var path string var port uint16 + commonName, upstream := extractCommonName(upstream) + n, upstream := extractNet(upstream) path, upstream = extractPath(upstream) @@ -357,13 +400,20 @@ func ParseUpstream(upstream string) (Upstream, error) { } return Upstream{ - Net: n, - Host: host, - Port: port, - Path: path, + Net: n, + Host: host, + Port: port, + Path: path, + CommonName: commonName, }, nil } +func extractCommonName(in string) (string, string) { + upstream, cn, _ := strings.Cut(in, "#") + + return cn, upstream +} + func extractPath(in string) (path string, upstream string) { slashIdx := strings.Index(in, "/") @@ -399,33 +449,37 @@ func extractNet(upstream string) (NetProtocol, string) { // Config main configuration // nolint:maligned type Config struct { - Upstream UpstreamConfig `yaml:"upstream"` - UpstreamTimeout Duration `yaml:"upstreamTimeout" default:"2s"` - CustomDNS CustomDNSConfig `yaml:"customDNS"` - Conditional ConditionalUpstreamConfig `yaml:"conditional"` - Blocking BlockingConfig `yaml:"blocking"` - ClientLookup ClientLookupConfig `yaml:"clientLookup"` - Caching CachingConfig `yaml:"caching"` - QueryLog QueryLogConfig `yaml:"queryLog"` - Prometheus PrometheusConfig `yaml:"prometheus"` - Redis RedisConfig `yaml:"redis"` - LogLevel log.Level `yaml:"logLevel" default:"info"` - LogFormat log.FormatType `yaml:"logFormat" default:"text"` - LogPrivacy bool `yaml:"logPrivacy" default:"false"` - LogTimestamp bool `yaml:"logTimestamp" default:"true"` - DNSPorts ListenConfig `yaml:"port" default:"[\"53\"]"` - HTTPPorts ListenConfig `yaml:"httpPort"` - HTTPSPorts ListenConfig `yaml:"httpsPort"` - TLSPorts ListenConfig `yaml:"tlsPort"` - DoHUserAgent string `yaml:"dohUserAgent"` - MinTLSServeVer string `yaml:"minTlsServeVersion" default:"1.2"` + Upstream UpstreamConfig `yaml:"upstream"` + UpstreamTimeout Duration `yaml:"upstreamTimeout" default:"2s"` + ConnectIPVersion IPVersion `yaml:"connectIPVersion"` + CustomDNS CustomDNSConfig `yaml:"customDNS"` + Conditional ConditionalUpstreamConfig `yaml:"conditional"` + Blocking BlockingConfig `yaml:"blocking"` + ClientLookup ClientLookupConfig `yaml:"clientLookup"` + Caching CachingConfig `yaml:"caching"` + QueryLog QueryLogConfig `yaml:"queryLog"` + Prometheus PrometheusConfig `yaml:"prometheus"` + Redis RedisConfig `yaml:"redis"` + LogLevel log.Level `yaml:"logLevel" default:"info"` + LogFormat log.FormatType `yaml:"logFormat" default:"text"` + LogPrivacy bool `yaml:"logPrivacy" default:"false"` + LogTimestamp bool `yaml:"logTimestamp" default:"true"` + DNSPorts ListenConfig `yaml:"port" default:"[\"53\"]"` + HTTPPorts ListenConfig `yaml:"httpPort"` + HTTPSPorts ListenConfig `yaml:"httpsPort"` + TLSPorts ListenConfig `yaml:"tlsPort"` + DoHUserAgent string `yaml:"dohUserAgent"` + MinTLSServeVer string `yaml:"minTlsServeVersion" default:"1.2"` + StartVerifyUpstream bool `yaml:"startVerifyUpstream" default:"false"` // Deprecated DisableIPv6 bool `yaml:"disableIPv6" default:"false"` CertFile string `yaml:"certFile"` KeyFile string `yaml:"keyFile"` BootstrapDNS BootstrapConfig `yaml:"bootstrapDns"` HostsFile HostsFileConfig `yaml:"hostsFile"` + FqdnOnly bool `yaml:"fqdnOnly" default:"false"` Filtering FilteringConfig `yaml:"filtering"` + Ede EdeConfig `yaml:"ede"` } type BootstrapConfig bootstrapConfig // to avoid infinite recursion. See BootstrapConfig.UnmarshalYAML. @@ -447,7 +501,8 @@ type UpstreamConfig struct { // RewriteConfig custom DNS configuration type RewriteConfig struct { - Rewrite map[string]string `yaml:"rewrite"` + Rewrite map[string]string `yaml:"rewrite"` + FallbackUpstream bool `yaml:"fallbackUpstream" default:"false"` } // CustomDNSConfig custom DNS configuration @@ -476,17 +531,19 @@ type ConditionalUpstreamMapping struct { // BlockingConfig configuration for query blocking type BlockingConfig struct { - BlackLists map[string][]string `yaml:"blackLists"` - WhiteLists map[string][]string `yaml:"whiteLists"` - ClientGroupsBlock map[string][]string `yaml:"clientGroupsBlock"` - BlockType string `yaml:"blockType" default:"ZEROIP"` - BlockTTL Duration `yaml:"blockTTL" default:"6h"` - DownloadTimeout Duration `yaml:"downloadTimeout" default:"60s"` - DownloadAttempts uint `yaml:"downloadAttempts" default:"3"` - DownloadCooldown Duration `yaml:"downloadCooldown" default:"1s"` - RefreshPeriod Duration `yaml:"refreshPeriod" default:"4h"` - FailStartOnListError bool `yaml:"failStartOnListError" default:"false"` - ProcessingConcurrency uint `yaml:"processingConcurrency" default:"4"` + BlackLists map[string][]string `yaml:"blackLists"` + WhiteLists map[string][]string `yaml:"whiteLists"` + ClientGroupsBlock map[string][]string `yaml:"clientGroupsBlock"` + BlockType string `yaml:"blockType" default:"ZEROIP"` + BlockTTL Duration `yaml:"blockTTL" default:"6h"` + DownloadTimeout Duration `yaml:"downloadTimeout" default:"60s"` + DownloadAttempts uint `yaml:"downloadAttempts" default:"3"` + DownloadCooldown Duration `yaml:"downloadCooldown" default:"1s"` + RefreshPeriod Duration `yaml:"refreshPeriod" default:"4h"` + // Deprecated + FailStartOnListError bool `yaml:"failStartOnListError" default:"false"` + ProcessingConcurrency uint `yaml:"processingConcurrency" default:"4"` + StartStrategy StartStrategyType `yaml:"startStrategy" default:"blocking"` } // ClientLookupConfig configuration for the client lookup @@ -528,20 +585,31 @@ type RedisConfig struct { } type HostsFileConfig struct { - Filepath string `yaml:"filePath"` - HostsTTL Duration `yaml:"hostsTTL" default:"1h"` - RefreshPeriod Duration `yaml:"refreshPeriod" default:"1h"` + Filepath string `yaml:"filePath"` + HostsTTL Duration `yaml:"hostsTTL" default:"1h"` + RefreshPeriod Duration `yaml:"refreshPeriod" default:"1h"` + FilterLoopback bool `yaml:"filterLoopback"` } type FilteringConfig struct { QueryTypes QTypeSet `yaml:"queryTypes"` } +type EdeConfig struct { + Enable bool `yaml:"enable" default:"false"` +} + // nolint:gochecknoglobals -var config = &Config{} +var ( + config = &Config{} + cfgLock sync.RWMutex +) // LoadConfig creates new config from YAML file or a directory containing YAML files func LoadConfig(path string, mandatory bool) (*Config, error) { + cfgLock.Lock() + defer cfgLock.Unlock() + cfg := Config{} if err := defaults.Set(&cfg); err != nil { return nil, fmt.Errorf("can't apply default values: %w", err) @@ -562,32 +630,14 @@ func LoadConfig(path string, mandatory bool) (*Config, error) { var data []byte - if fs.IsDir() { //nolint:nestif - err = filepath.WalkDir(path, func(filePath string, d os.DirEntry, err error) error { - if err != nil { - return err - } - - if path == filePath { - return nil - } - - fileData, err := os.ReadFile(filePath) - if err != nil { - return err - } - - data = append(data, []byte("\n")...) - data = append(data, fileData...) - - return nil - }) + if fs.IsDir() { + data, err = readFromDir(path, data) if err != nil { return nil, fmt.Errorf("can't read config files: %w", err) } } else { - data, err = ioutil.ReadFile(path) + data, err = os.ReadFile(path) if err != nil { return nil, fmt.Errorf("can't read config file: %w", err) } @@ -603,6 +653,57 @@ func LoadConfig(path string, mandatory bool) (*Config, error) { return &cfg, nil } +func readFromDir(path string, data []byte) ([]byte, error) { + err := filepath.WalkDir(path, func(filePath string, d os.DirEntry, err error) error { + if err != nil { + return err + } + + if path == filePath { + return nil + } + + // Ignore non YAML files + if !strings.HasSuffix(filePath, ".yml") && !strings.HasSuffix(filePath, ".yaml") { + return nil + } + + isRegular, err := isRegularFile(filePath) + if err != nil { + return err + } + + // Ignore non regular files (directories, sockets, etc.) + if !isRegular { + return nil + } + + fileData, err := os.ReadFile(filePath) + if err != nil { + return err + } + + data = append(data, []byte("\n")...) + data = append(data, fileData...) + + return nil + }) + + return data, err +} + +// isRegularFile follows symlinks, so the result is `true` for a symlink to a regular file. +func isRegularFile(path string) (bool, error) { + stat, err := os.Stat(path) + if err != nil { + return false, err + } + + isRegular := stat.Mode()&os.ModeType == 0 + + return isRegular, nil +} + func unmarshalConfig(data []byte, cfg *Config) error { err := yaml.UnmarshalStrict(data, cfg) if err != nil { @@ -620,10 +721,24 @@ func validateConfig(cfg *Config) { cfg.Filtering.QueryTypes.Insert(dns.Type(dns.TypeAAAA)) } + + if cfg.Blocking.FailStartOnListError { + log.Log().Warnf("'blocking.failStartOnListError' is deprecated. Please use 'blocking.startStrategy'" + + " with 'failOnError' instead.") + + if cfg.Blocking.StartStrategy == StartStrategyTypeBlocking { + cfg.Blocking.StartStrategy = StartStrategyTypeFailOnError + } else if cfg.Blocking.StartStrategy == StartStrategyTypeFast { + log.Log().Warnf("'blocking.startStrategy' with 'fast' will ignore 'blocking.failStartOnListError'.") + } + } } // GetConfig returns the current config func GetConfig() *Config { + cfgLock.RLock() + defer cfgLock.RUnlock() + return config } diff --git a/config/config_enum.go b/config/config_enum.go index 4e04da99..dbdfef14 100644 --- a/config/config_enum.go +++ b/config/config_enum.go @@ -11,6 +11,79 @@ import ( "strings" ) +const ( + // IPVersionDual is a IPVersion of type Dual. + // IPv4 and IPv6 + IPVersionDual IPVersion = iota + // IPVersionV4 is a IPVersion of type V4. + // IPv4 only + IPVersionV4 + // IPVersionV6 is a IPVersion of type V6. + // IPv6 only + IPVersionV6 +) + +var ErrInvalidIPVersion = fmt.Errorf("not a valid IPVersion, try [%s]", strings.Join(_IPVersionNames, ", ")) + +const _IPVersionName = "dualv4v6" + +var _IPVersionNames = []string{ + _IPVersionName[0:4], + _IPVersionName[4:6], + _IPVersionName[6:8], +} + +// IPVersionNames returns a list of possible string values of IPVersion. +func IPVersionNames() []string { + tmp := make([]string, len(_IPVersionNames)) + copy(tmp, _IPVersionNames) + return tmp +} + +var _IPVersionMap = map[IPVersion]string{ + IPVersionDual: _IPVersionName[0:4], + IPVersionV4: _IPVersionName[4:6], + IPVersionV6: _IPVersionName[6:8], +} + +// String implements the Stringer interface. +func (x IPVersion) String() string { + if str, ok := _IPVersionMap[x]; ok { + return str + } + return fmt.Sprintf("IPVersion(%d)", x) +} + +var _IPVersionValue = map[string]IPVersion{ + _IPVersionName[0:4]: IPVersionDual, + _IPVersionName[4:6]: IPVersionV4, + _IPVersionName[6:8]: IPVersionV6, +} + +// ParseIPVersion attempts to convert a string to a IPVersion. +func ParseIPVersion(name string) (IPVersion, error) { + if x, ok := _IPVersionValue[name]; ok { + return x, nil + } + return IPVersion(0), fmt.Errorf("%s is %w", name, ErrInvalidIPVersion) +} + +// MarshalText implements the text marshaller method. +func (x IPVersion) MarshalText() ([]byte, error) { + return []byte(x.String()), nil +} + +// UnmarshalText implements the text unmarshaller method. +func (x *IPVersion) UnmarshalText(text []byte) error { + name := string(text) + tmp, err := ParseIPVersion(name) + if err != nil { + return err + } + *x = tmp + return nil +} + const ( // NetProtocolTcpUdp is a NetProtocol of type Tcp+Udp. // TCP and UDP protocols @@ -23,6 +96,8 @@ const ( NetProtocolHttps ) +var ErrInvalidNetProtocol = fmt.Errorf("not a valid NetProtocol, try [%s]", strings.Join(_NetProtocolNames, ", ")) + const _NetProtocolName = "tcp+udptcp-tlshttps" var _NetProtocolNames = []string{ @@ -63,7 +138,7 @@ func ParseNetProtocol(name string) (NetProtocol, error) { if x, ok := _NetProtocolValue[name]; ok { return x, nil } - return NetProtocol(0), fmt.Errorf("%s is not a valid NetProtocol, try [%s]", name, strings.Join(_NetProtocolNames, ", ")) + return NetProtocol(0), fmt.Errorf("%s is %w", name, ErrInvalidNetProtocol) } // MarshalText implements the text marshaller method. @@ -103,6 +178,8 @@ const ( QueryLogTypeCsvClient ) +var ErrInvalidQueryLogType = fmt.Errorf("not a valid QueryLogType, try [%s]", strings.Join(_QueryLogTypeNames, ", ")) + const _QueryLogTypeName = "consolenonemysqlpostgresqlcsvcsv-client" var _QueryLogTypeNames = []string{ @@ -152,7 +229,7 @@ func ParseQueryLogType(name string) (QueryLogType, error) { if x, ok := _QueryLogTypeValue[name]; ok { return x, nil } - return QueryLogType(0), fmt.Errorf("%s is not a valid QueryLogType, try [%s]", name, strings.Join(_QueryLogTypeNames, ", ")) + return QueryLogType(0), fmt.Errorf("%s is %w", name, ErrInvalidQueryLogType) } // MarshalText implements the text marshaller method. @@ -170,3 +247,76 @@ func (x *QueryLogType) UnmarshalText(text []byte) error { *x = tmp return nil } + +const ( + // StartStrategyTypeBlocking is a StartStrategyType of type Blocking. + // synchronously download blocking lists on startup + StartStrategyTypeBlocking StartStrategyType = iota + // StartStrategyTypeFailOnError is a StartStrategyType of type FailOnError. + // synchronously download blocking lists on startup and shutdown on error + StartStrategyTypeFailOnError + // StartStrategyTypeFast is a StartStrategyType of type Fast. + // asyncronously download blocking lists on startup + StartStrategyTypeFast +) + +var ErrInvalidStartStrategyType = fmt.Errorf("not a valid StartStrategyType, try [%s]", strings.Join(_StartStrategyTypeNames, ", ")) + +const _StartStrategyTypeName = "blockingfailOnErrorfast" + +var _StartStrategyTypeNames = []string{ + _StartStrategyTypeName[0:8], + _StartStrategyTypeName[8:19], + _StartStrategyTypeName[19:23], +} + +// StartStrategyTypeNames returns a list of possible string values of StartStrategyType. +func StartStrategyTypeNames() []string { + tmp := make([]string, len(_StartStrategyTypeNames)) + copy(tmp, _StartStrategyTypeNames) + return tmp +} + +var _StartStrategyTypeMap = map[StartStrategyType]string{ + StartStrategyTypeBlocking: _StartStrategyTypeName[0:8], + StartStrategyTypeFailOnError: _StartStrategyTypeName[8:19], + StartStrategyTypeFast: _StartStrategyTypeName[19:23], +} + +// String implements the Stringer interface. +func (x StartStrategyType) String() string { + if str, ok := _StartStrategyTypeMap[x]; ok { + return str + } + return fmt.Sprintf("StartStrategyType(%d)", x) +} + +var _StartStrategyTypeValue = map[string]StartStrategyType{ + _StartStrategyTypeName[0:8]: StartStrategyTypeBlocking, + _StartStrategyTypeName[8:19]: StartStrategyTypeFailOnError, + _StartStrategyTypeName[19:23]: StartStrategyTypeFast, +} + +// ParseStartStrategyType attempts to convert a string to a StartStrategyType. +func ParseStartStrategyType(name string) (StartStrategyType, error) { + if x, ok := _StartStrategyTypeValue[name]; ok { + return x, nil + } + return StartStrategyType(0), fmt.Errorf("%s is %w", name, ErrInvalidStartStrategyType) +} + +// MarshalText implements the text marshaller method. +func (x StartStrategyType) MarshalText() ([]byte, error) { + return []byte(x.String()), nil +} + +// UnmarshalText implements the text unmarshaller method. +func (x *StartStrategyType) UnmarshalText(text []byte) error { + name := string(text) + tmp, err := ParseStartStrategyType(name) + if err != nil { + return err + } + *x = tmp + return nil +} diff --git a/config/config_test.go b/config/config_test.go index ee6de681..e3d8d68d 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -2,26 +2,36 @@ package config import ( "errors" - "io/ioutil" "net" - "os" "time" "github.com/miekg/dns" + "github.com/0xERR0R/blocky/helpertest" . "github.com/0xERR0R/blocky/log" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("Config", func() { + var ( + tmpDir *helpertest.TmpFolder + err error + ) + + BeforeEach(func() { + tmpDir = helpertest.NewTmpFolder("config") + Expect(tmpDir.Error).Should(Succeed()) + DeferCleanup(tmpDir.Clean) + }) + Describe("Creation of Config", func() { When("Test config file will be parsed", func() { It("should return a valid config struct", func() { - err := os.Chdir("../testdata") - Expect(err).Should(Succeed()) + confFile := writeConfigYml(tmpDir) + Expect(confFile.Error).Should(Succeed()) - _, err = LoadConfig("config.yml", true) + _, err = LoadConfig(confFile.Path, true) Expect(err).Should(Succeed()) defaultTestFileConfig() @@ -29,36 +39,54 @@ var _ = Describe("Config", func() { }) When("Test file does not exist", func() { It("should fail", func() { - _, err := LoadConfig("../testdata/config-does-not-exist.yaml", true) + _, err := LoadConfig(tmpDir.JoinPath("config-does-not-exist.yaml"), true) Expect(err).Should(Not(Succeed())) }) }) When("Multiple config files are used", func() { It("should return a valid config struct", func() { - _, err := LoadConfig("../testdata/config/", true) + err = writeConfigDir(tmpDir) + Expect(err).Should(Succeed()) + + _, err := LoadConfig(tmpDir.Path, true) Expect(err).Should(Succeed()) defaultTestFileConfig() }) + + It("should ignore non YAML files", func() { + err = writeConfigDir(tmpDir) + Expect(err).Should(Succeed()) + + tmpDir.CreateStringFile("ignore-me.txt", "THIS SHOULD BE IGNORED!") + + _, err := LoadConfig(tmpDir.Path, true) + Expect(err).Should(Succeed()) + }) + + It("should ignore non regular files", func() { + err = writeConfigDir(tmpDir) + Expect(err).Should(Succeed()) + + tmpDir.CreateSubFolder("subfolder") + tmpDir.CreateSubFolder("subfolder.yml") + + _, err := LoadConfig(tmpDir.Path, true) + Expect(err).Should(Succeed()) + }) }) When("Config folder does not exist", func() { It("should fail", func() { - _, err := LoadConfig("../testdata/does-not-exist-config/", true) + _, err := LoadConfig(tmpDir.JoinPath("does-not-exist-config/"), true) Expect(err).Should(Not(Succeed())) }) }) When("config file is malformed", func() { It("should return error", func() { + cfgFile := tmpDir.CreateStringFile("config.yml", "malformed_config") + Expect(cfgFile.Error).Should(Succeed()) - dir, err := ioutil.TempDir("", "blocky") - defer os.Remove(dir) - Expect(err).Should(Succeed()) - err = os.Chdir(dir) - Expect(err).Should(Succeed()) - err = ioutil.WriteFile("config.yml", []byte("malformed_config"), 0600) - Expect(err).Should(Succeed()) - - _, err = LoadConfig("config.yml", true) + _, err = LoadConfig(cfgFile.Path, true) Expect(err).Should(HaveOccurred()) Expect(err.Error()).Should(ContainSubstring("wrong file structure")) }) @@ -172,22 +200,39 @@ bootstrapDns: }) }) + When("Deprecated parameter 'failStartOnListError' is set", func() { + var ( + c Config + ) + BeforeEach(func() { + c = Config{ + Blocking: BlockingConfig{ + FailStartOnListError: true, + StartStrategy: StartStrategyTypeBlocking, + }, + } + }) + It("should change StartStrategy blocking to failOnError", func() { + validateConfig(&c) + Expect(c.Blocking.StartStrategy).Should(Equal(StartStrategyTypeFailOnError)) + }) + It("shouldn't change StartStrategy if set to fast", func() { + c.Blocking.StartStrategy = StartStrategyTypeFast + validateConfig(&c) + Expect(c.Blocking.StartStrategy).Should(Equal(StartStrategyTypeFast)) + }) + }) + When("config directory does not exist", func() { It("should return error", func() { - err := os.Chdir("../..") - Expect(err).Should(Succeed()) - - _, err = LoadConfig("config.yml", true) + _, err = LoadConfig(tmpDir.JoinPath("config.yml"), true) Expect(err).Should(HaveOccurred()) Expect(err.Error()).Should(ContainSubstring("no such file or directory")) }) It("should use default config if config is not mandatory", func() { - err := os.Chdir("../..") - Expect(err).Should(Succeed()) - - _, err = LoadConfig("config.yml", false) + _, err = LoadConfig(tmpDir.JoinPath("config.yml"), false) Expect(err).Should(Succeed()) Expect(config.LogLevel).Should(Equal(LevelInfo)) @@ -381,6 +426,10 @@ bootstrapDns: "tcp-tls:4.4.4.4", Upstream{Net: NetProtocolTcpTls, Host: "4.4.4.4", Port: 853}, false), + Entry("tcp-tls with common name", + "tcp-tls:1.1.1.2#security.cloudflare-dns.com", + Upstream{Net: NetProtocolTcpTls, Host: "1.1.1.2", Port: 853, CommonName: "security.cloudflare-dns.com"}, + false), Entry("DoH without port, use default", "https:4.4.4.4", Upstream{Net: NetProtocolHttps, Host: "4.4.4.4", Port: 443}, @@ -578,6 +627,126 @@ func defaultTestFileConfig() { Expect(config.DoHUserAgent).Should(Equal("testBlocky")) Expect(config.MinTLSServeVer).Should(Equal("1.3")) + Expect(config.StartVerifyUpstream).Should(BeFalse()) Expect(GetConfig()).Should(Not(BeNil())) } + +func writeConfigYml(tmpDir *helpertest.TmpFolder) *helpertest.TmpFile { + return tmpDir.CreateStringFile("config.yml", + "upstream:", + " default:", + " - tcp+udp:8.8.8.8", + " - tcp+udp:8.8.4.4", + " - 1.1.1.1", + "customDNS:", + " mapping:", + " my.duckdns.org: 192.168.178.3", + " multiple.ips: 192.168.178.3,192.168.178.4,2001:0db8:85a3:08d3:1319:8a2e:0370:7344", + "conditional:", + " mapping:", + " fritz.box: tcp+udp:192.168.178.1", + " multiple.resolvers: tcp+udp:192.168.178.1,tcp+udp:192.168.178.2", + "filtering:", + " queryTypes:", + " - AAAA", + " - A", + "blocking:", + " blackLists:", + " ads:", + " - https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt", + " - https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts", + " - https://mirror1.malwaredomains.com/files/justdomains", + " - http://sysctl.org/cameleon/hosts", + " - https://zeustracker.abuse.ch/blocklist.php?download=domainblocklist", + " - https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt", + " special:", + " - https://hosts-file.net/ad_servers.txt", + " whiteLists:", + " ads:", + " - whitelist.txt", + " clientGroupsBlock:", + " default:", + " - ads", + " - special", + " Laptop-D.fritz.box:", + " - ads", + " blockTTL: 1m", + " refreshPeriod: 120", + "clientLookup:", + " upstream: 192.168.178.1", + " singleNameOrder:", + " - 2", + " - 1", + "queryLog:", + " type: csv-client", + " target: /opt/log", + "port: 55553,:55554,[::1]:55555", + "logLevel: debug", + "dohUserAgent: testBlocky", + "minTlsServeVersion: 1.3", + "startVerifyUpstream: false") +} + +func writeConfigDir(tmpDir *helpertest.TmpFolder) error { + f1 := tmpDir.CreateStringFile("config1.yaml", + "upstream:", + " default:", + " - tcp+udp:8.8.8.8", + " - tcp+udp:8.8.4.4", + " - 1.1.1.1", + "customDNS:", + " mapping:", + " my.duckdns.org: 192.168.178.3", + " multiple.ips: 192.168.178.3,192.168.178.4,2001:0db8:85a3:08d3:1319:8a2e:0370:7344", + "conditional:", + " mapping:", + " fritz.box: tcp+udp:192.168.178.1", + " multiple.resolvers: tcp+udp:192.168.178.1,tcp+udp:192.168.178.2", + "filtering:", + " queryTypes:", + " - AAAA", + " - A") + if f1.Error != nil { + return f1.Error + } + + f2 := tmpDir.CreateStringFile("config2.yaml", + "blocking:", + " blackLists:", + " ads:", + " - https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt", + " - https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts", + " - https://mirror1.malwaredomains.com/files/justdomains", + " - http://sysctl.org/cameleon/hosts", + " - https://zeustracker.abuse.ch/blocklist.php?download=domainblocklist", + " - https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt", + " special:", + " - https://hosts-file.net/ad_servers.txt", + " whiteLists:", + " ads:", + " - whitelist.txt", + " clientGroupsBlock:", + " default:", + " - ads", + " - special", + " Laptop-D.fritz.box:", + " - ads", + " blockTTL: 1m", + " refreshPeriod: 120", + "clientLookup:", + " upstream: 192.168.178.1", + " singleNameOrder:", + " - 2", + " - 1", + "queryLog:", + " type: csv-client", + " target: /opt/log", + "port: 55553,:55554,[::1]:55555", + "logLevel: debug", + "dohUserAgent: testBlocky", + "minTlsServeVersion: 1.3", + "startVerifyUpstream: false") + + return f2.Error +} diff --git a/docs/blocky-query-grafana-postgres.json b/docs/blocky-query-grafana-postgres.json new file mode 100644 index 00000000..4dad81c9 --- /dev/null +++ b/docs/blocky-query-grafana-postgres.json @@ -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 +} \ No newline at end of file diff --git a/docs/config.yml b/docs/config.yml index 61e84102..5118f65f 100644 --- a/docs/config.yml +++ b/docs/config.yml @@ -19,6 +19,14 @@ upstream: # optional: timeout to query the upstream resolver. Default: 2s upstreamTimeout: 2s +# optional: If true, blocky will fail to start unless at least one upstream server per group is reachable. Default: false +startVerifyUpstream: true + +# optional: Determines how blocky will create outgoing connections. This impacts both upstreams, and lists. +# accepted: dual, v4, v6 +# default: dual +connectIPVersion: dual + # optional: custom IP address(es) for domain name (with all sub-domains). Multiple addresses must be separated by a comma # example: query "printer.lan" or "my.printer.lan" will return 192.168.178.3 customDNS: @@ -35,6 +43,10 @@ customDNS: # optional: definition, which DNS resolver(s) should be used for queries to the domain (with all sub-domains). Multiple resolvers must be separated by a comma # Example: Query client.fritz.box will ask DNS server 192.168.178.1. This is necessary for local network, to resolve clients by host name conditional: + # optional: if false (default), return empty result if after rewrite, the mapped resolver returned an empty answer. If true, the original query will be sent to the upstream resolver + # Example: The query "blog.example.com" will be rewritten to "blog.fritz.box" and also redirected to the resolver at 192.168.178.1. If not found and if `fallbackUpstream` was set to `true`, the original query "blog.example.com" will be sent upstream. + # Usage: One usecase when having split DNS for internal and external (internet facing) users, but not all subdomains are listed in the internal domain. + fallbackUpstream: false # optional: replace domain in the query with other domain before resolver lookup in the mapping rewrite: example.com: fritz.box @@ -97,8 +109,8 @@ blocking: downloadAttempts: 5 # optional: Time between the download attempts. Default: 1s downloadCooldown: 10s - # optional: if true, application startup will fail if at least one list can't be downloaded / opened. Default: false - failStartOnListError: false + # optional: if failOnError, application startup will fail if at least one list can't be downloaded / opened. Default: blocking + startStrategy: failOnError # optional: configuration for caching of DNS responses caching: @@ -128,6 +140,9 @@ caching: # Max number of domains to be kept in cache for prefetching (soft limit). Useful on systems with limited amount of RAM. # Default (0): unlimited prefetchMaxItemsCount: 0 + # Time how long negative results (NXDOMAIN response or empty result) are cached. A value of -1 will disable caching for negative results. + # Default: 30m + cacheTimeNegative: 30m # optional: configuration of client name resolution clientLookup: @@ -206,6 +221,8 @@ hostsFile: hostsTTL: 60m # optional: Time between hosts file refresh, default: 1h refreshPeriod: 30m + # optional: Whether loopback hosts addresses (127.0.0.0/8 and ::1) should be filtered or not, default: false + filterLoopback: true # optional: Log level (one from debug, info, warn, error). Default: info logLevel: info # optional: Log format (text or json). Default: text @@ -214,3 +231,8 @@ logFormat: text logTimestamp: true # optional: obfuscate log output (replace all alphanumeric characters with *) for user sensitive data like request domains or responses to increase privacy. Default: false logPrivacy: false + +# optional: add EDE error codes to dns response +ede: + # enabled if true, Default: false + enable: true \ No newline at end of file diff --git a/docs/configuration.md b/docs/configuration.md index 3f3219cf..a29bef23 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -25,6 +25,8 @@ configuration properties as [JSON](config.yml). | logPrivacy | bool | no | false | Obfuscate log output (replace all alphanumeric characters with *) for user sensitive data like request domains or responses to increase privacy. | | dohUserAgent | string | no | | HTTP User Agent for DoH upstreams | | minTlsServeVersion | string | no | 1.2 | Minimum TLS version that the DoT and DoH server use to serve those encrypted DNS requests | +| startVerifyUpstream | bool | no | false | If true, blocky will fail to start unless at least one upstream server per group is reachable. | +| connectIPVersion | bool | no | dual | IP version to use for outgoing connections (dual, v4, v6) | !!! example @@ -50,13 +52,16 @@ following network protocols (net part of the resolver URL): returns the answer from the fastest one. This improves your network speed and increases your privacy - your DNS traffic will be distributed over multiple providers. -Each resolver must be defined as a string in following format: `[net:]host:[port][/path]`. +Each resolver must be defined as a string in following format: `[net:]host:[port][/path][#commonName]`. | Parameter | Type | Mandatory | Default value | |-----------|----------------------------------|-----------|---------------------------------------------------| -| net | enum (tcp+udp, tcp-tls or https) | no | tcp+udp | -| host | IP or hostname | yes | | -| port | int (1 - 65535) | no | 53 for udp/tcp, 853 for tcp-tls and 443 for https | +| net | enum (tcp+udp, tcp-tls or https) | no | tcp+udp | +| host | IP or hostname | yes | | +| port | int (1 - 65535) | no | 53 for udp/tcp, 853 for tcp-tls and 443 for https | +| commonName | string | no | the host value | + +The commonName parameter overrides the expected certificate common name value used for verification. Blocky needs at least the configuration of the **default** group. This group will be used as a fallback, if no client specific resolver configuration is available. @@ -134,7 +139,6 @@ Works only on Linux/\*nix OS due to golang limitations under Windows. - 123.123.123.123 ``` - ## Filtering Under certain circumstances, it may be useful to filter some types of DNS queries. You can define one or more DNS query @@ -150,6 +154,18 @@ types, all queries with these types will be dropped (empty answer will be return This configuration will drop all 'AAAA' (IPv6) queries. +## FQDN only + +In domain environments, it may be usefull to only response to FQDN requests. If this option is enabled blocky respond immidiatly +with NXDOMAIN if the request is not a valid FQDN. The request is therfore not further processed by other options like custom or conditional. +Please be aware that by enabling it your hostname resolution will break unless every hostname is part of a domain. + +!!! example + + ```yaml + fqdnOnly: true + ``` + ## Custom DNS You can define your own domain name to IP mappings. For example, you can use a user-friendly name for a network printer @@ -186,7 +202,7 @@ The query "printer.home" will be rewritten to "printer.lan" and return 192.168.1 With parameter `filterUnmappedTypes = true` (default), blocky will filter all queries with unmapped types, for example: AAAA for "printer.lan" or TXT for "otherdevice.lan". -With `filterUnmappedTypes = true` a query AAAA "printer.lan" will be forwarded to the upstream DNS server. +With `filterUnmappedTypes = false` a query AAAA "printer.lan" will be forwarded to the upstream DNS server. ## Conditional DNS resolution @@ -196,10 +212,15 @@ hostname belongs to which IP address, all DNS queries for the local network shou The optional parameter `rewrite` behaves the same as with custom DNS. +The optional parameter fallbackUpstream, if false (default), return empty result if after rewrite, the mapped resolver returned an empty answer. If true, the original query will be sent to the upstream resolver. + +### Usage: One usecase when having split DNS for internal and external (internet facing) users, but not all subdomains are listed in the internal domain + !!! example ```yaml conditional: + fallbackUpstream: false rewrite: example.com: fritz.box replace-me.com: with-this.com @@ -217,9 +238,13 @@ The optional parameter `rewrite` behaves the same as with custom DNS. You can use `.` as wildcard for all non full qualified domains (domains without dot) In this example, a DNS query "client.fritz.box" will be redirected to the router's DNS server at 192.168.178.1 and client.lan.net to 192.170.1.2 and 192.170.1.3. -The query "client.example.com" will be rewritten to "client.fritz.box" and also redirected to the resolver at 192.168.178.1. All unqualified hostnames (e.g. "test") -will be redirected to the DNS server at 168.168.0.1 +The query "client.example.com" will be rewritten to "client.fritz.box" and also redirected to the resolver at 192.168.178.1. +If not found and if `fallbackUpstream` was set to `true`, the original query "blog.example.com" will be sent upstream. + +All unqualified hostnames (e.g. "test") will be redirected to the DNS server at 168.168.0.1. + +One usecase for `fallbackUpstream` is when having split DNS for internal and external (internet facing) users, but not all subdomains are listed in the internal domain. ## Client name lookup @@ -436,16 +461,22 @@ You can configure the list download attempts according to your internet connecti downloadCooldown: 10s ``` -### Fail on start +### Start strategy -You can ensure with parameter `failStartOnListError = true` that the application will fail if at least one list can't be -downloaded or opened. Default value is `false`. +You can configure the blocking behavior during application start of blocky. +If no starategy is selected blocking will be used. + +| startStrategy | Description | +|---------------|-------------------------------------------------------------------------------------------------------| +| blocking | all blocking lists will be loaded before DNS resoulution starts | +| failOnError | like blocking but blocky shutsdown if an download fails | +| fast | DNS resolution starts immediately without blocking which will be enabled after list load is completed | !!! example ```yaml blocking: - failStartOnListError: false + startStrategy: failOnError ``` ### Concurrency @@ -454,7 +485,7 @@ Blocky downloads and processes links in a single group concurrently. With parame how many links can be processed in the same time. Higher value can reduce the overall list refresh time, but more parallel download and processing jobs need more RAM. Please consider to reduce this value on systems with limited memory. Default value is 4. - !!! example +!!! example ```yaml blocking: @@ -483,7 +514,7 @@ With following parameters you can tune the caching behavior: | caching.prefetchExpires | duration format | no | 2h | Prefetch track time window | | caching.prefetchThreshold | int | no | 5 | Name queries threshold for prefetch | | caching.prefetchMaxItemsCount | int | no | 0 (unlimited) | Max number of domains to be kept in cache for prefetching (soft limit). Default (0): unlimited. Useful on systems with limited amount of RAM. | -| caching.cacheTimeNegative | duration format | no | 30m | Time how long negative results are cached. A value of -1 will disable caching for negative results. | +| caching.cacheTimeNegative | duration format | no | 30m | Time how long negative results (NXDOMAIN response or empty result) are cached. A value of -1 will disable caching for negative results. | !!! example @@ -593,17 +624,18 @@ example for Database logRetentionDays: 7 ``` -### Hosts file +## Hosts file You can enable resolving of entries, located in local hosts file. Configuration parameters: -| Parameter | Type | Mandatory | Default value | Description | -|--------------------------|--------------------------------|-----------|---------------|-----------------------------------------------| -| hostsFile.filePath | string | no | | Path to hosts file (e.g. /etc/hosts on Linux) | -| hostsFile.hostsTTL | duration (no units is minutes) | no | 1h | TTL | -| hostsFile.refreshPeriod | duration format | no | 1h | Time between hosts file refresh | +| Parameter | Type | Mandatory | Default value | Description | +|--------------------------|--------------------------------|-----------|---------------|--------------------------------------------------| +| hostsFile.filePath | string | no | | Path to hosts file (e.g. /etc/hosts on Linux) | +| hostsFile.hostsTTL | duration (no units is minutes) | no | 1h | TTL | +| hostsFile.refreshPeriod | duration format | no | 1h | Time between hosts file refresh | +| hostsFile.filterLoopback | bool | no | false | Filter loopback addresses (127.0.0.0/8 and ::1) | !!! example @@ -614,6 +646,23 @@ Configuration parameters: refreshPeriod: 30m ``` +### Deliver EDE codes as EDNS0 option + +DNS responses can be extended with EDE codes according to [RFC8914](https://datatracker.ietf.org/doc/rfc8914/). + +Configuration parameters: + +| Parameter | Type | Mandatory | Default value | Description | +|--------------------------|--------------------------------|-----------|---------------|----------------------------------------------------| +| ede.enable | bool | no | false | If true, DNS responses are deliverd with EDE codes | + +!!! example + + ```yaml + ede: + enable: true + ``` + ## SSL certificate configuration (DoH / TLS listener) See [Wiki - Configuration of HTTPS](https://github.com/0xERR0R/blocky/wiki/Configuration-of-HTTPS-for-DoH-and-Rest-API) diff --git a/docs/prometheus_grafana.md b/docs/prometheus_grafana.md index 17ab5644..290889fd 100644 --- a/docs/prometheus_grafana.md +++ b/docs/prometheus_grafana.md @@ -52,4 +52,8 @@ or [at grafana.com](https://grafana.com/grafana/dashboards/14980) Please define the MySQL source in Grafana, which points to the database with blocky's log entries. +## Postgres + +The JSON for a Grafana dashboard equivalent to the MySQL/MariaDB version is located [here](https://github.com/0xERR0R/blocky/blob/master/docs/blocky-query-grafana-postgres.json) + --8<-- "docs/includes/abbreviations.md" diff --git a/go.mod b/go.mod index 81a78dec..41da6cff 100644 --- a/go.mod +++ b/go.mod @@ -1,90 +1,112 @@ module github.com/0xERR0R/blocky -go 1.18 +go 1.19 require ( - github.com/alicebob/miniredis/v2 v2.21.0 + github.com/abice/go-enum v0.5.3 + github.com/alicebob/miniredis/v2 v2.23.1 github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef + github.com/avast/retry-go/v4 v4.3.0 github.com/creasty/defaults v1.6.0 + github.com/go-chi/chi/v5 v5.0.7 github.com/go-chi/cors v1.2.1 github.com/go-redis/redis/v8 v8.11.5 github.com/google/uuid v1.3.0 github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b github.com/hashicorp/go-multierror v1.1.1 - github.com/mattn/go-colorable v0.1.12 // indirect + github.com/hashicorp/golang-lru v0.5.4 + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect - github.com/miekg/dns v1.1.49 + github.com/miekg/dns v1.1.50 github.com/mroth/weightedrand v0.4.1 - github.com/onsi/gomega v1.19.0 - github.com/prometheus/client_golang v1.12.2 - github.com/sirupsen/logrus v1.8.1 - github.com/spf13/cobra v1.4.0 - github.com/stretchr/testify v1.7.2 + github.com/onsi/ginkgo/v2 v2.5.0 + github.com/onsi/gomega v1.24.1 + github.com/prometheus/client_golang v1.14.0 + github.com/sirupsen/logrus v1.9.0 + github.com/spf13/cobra v1.6.1 + github.com/stretchr/testify v1.8.1 + github.com/swaggo/swag v1.8.7 github.com/x-cray/logrus-prefixed-formatter v0.5.2 - golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 + golang.org/x/net v0.2.0 gopkg.in/yaml.v2 v2.4.0 - gorm.io/driver/mysql v1.3.4 - gorm.io/driver/sqlite v1.3.2 - gorm.io/gorm v1.23.5 + gorm.io/driver/mysql v1.4.3 + gorm.io/driver/postgres v1.4.5 + gorm.io/driver/sqlite v1.4.3 + gorm.io/gorm v1.24.1-0.20221019064659-5dd2bb482755 ) +require github.com/dosgo/zigtool v0.0.0-20210923085854-9c6fc1d62198 + require ( - github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 - github.com/avast/retry-go/v4 v4.0.5 - github.com/go-chi/chi/v5 v5.0.7 - github.com/hashicorp/golang-lru v0.5.4 - github.com/onsi/ginkgo/v2 v2.1.4 - github.com/swaggo/swag v1.8.2 - gorm.io/driver/postgres v1.3.7 + github.com/go-logr/logr v1.2.3 // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect + github.com/lib/pq v1.10.6 // indirect + golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde // indirect ) require ( github.com/KyleBanks/depth v1.2.1 // indirect - github.com/PuerkitoBio/purell v1.1.1 // indirect - github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver v1.5.0 // indirect + github.com/Masterminds/sprig v2.22.0+incompatible // indirect github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/ghodss/yaml v1.0.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/jsonreference v0.19.6 // indirect - github.com/go-openapi/spec v0.20.4 // indirect - github.com/go-openapi/swag v0.19.15 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/spec v0.20.7 // indirect + github.com/go-openapi/swag v0.22.3 // indirect github.com/go-sql-driver/mysql v1.6.0 // indirect + github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect + github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/google/go-cmp v0.5.9 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/huandu/xstrings v1.3.3 // indirect + github.com/imdario/mergo v0.3.13 // indirect + github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect - github.com/jackc/pgconn v1.12.1 // indirect + github.com/jackc/pgconn v1.13.0 // indirect github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgproto3/v2 v2.3.0 // indirect + github.com/jackc/pgproto3/v2 v2.3.1 // indirect github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect - github.com/jackc/pgtype v1.11.0 // indirect - github.com/jackc/pgx/v4 v4.16.1 // indirect + github.com/jackc/pgtype v1.12.0 // indirect + github.com/jackc/pgx/v4 v4.17.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/mailru/easyjson v0.7.6 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect - github.com/mattn/go-sqlite3 v1.14.12 // indirect + github.com/labstack/gommon v0.4.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-sqlite3 v1.14.15 // indirect + github.com/mattn/goveralls v0.0.11 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_model v0.2.0 // indirect - github.com/prometheus/common v0.32.1 // indirect - github.com/prometheus/procfs v0.7.3 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.37.0 // indirect + github.com/prometheus/procfs v0.8.0 // indirect + github.com/ramr/go-reaper v0.2.1 + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/stretchr/objx v0.3.0 // indirect - github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 // indirect - golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b // indirect - golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect - golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 // indirect - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect - golang.org/x/text v0.3.7 // indirect - golang.org/x/tools v0.1.10 // indirect - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect - google.golang.org/protobuf v1.27.1 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + github.com/stretchr/objx v0.5.0 // indirect + github.com/urfave/cli/v2 v2.23.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 // indirect + golang.org/x/crypto v0.1.0 // indirect + golang.org/x/mod v0.6.0 // indirect + golang.org/x/sys v0.2.0 // indirect + golang.org/x/term v0.2.0 // indirect + golang.org/x/text v0.4.0 // indirect + golang.org/x/tools v0.2.0 // indirect + google.golang.org/protobuf v1.28.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 85a13954..a43b798b 100644 --- a/go.sum +++ b/go.sum @@ -35,30 +35,34 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= +github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/abice/go-enum v0.5.3 h1:Ghq0aWp+tCNZFAb4lFK7UnjzUJQTS1atIMjHkX+Gex4= +github.com/abice/go-enum v0.5.3/go.mod h1:jf915DI7NxXZRwk8qDgZJKq2raAtwcPBXJRh9WVgtU0= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= -github.com/alicebob/miniredis/v2 v2.21.0 h1:CdmwIlKUWFBDS+4464GtQiQ0R1vpzOgu4Vnd74rBL7M= -github.com/alicebob/miniredis/v2 v2.21.0/go.mod h1:XNqvJdQJv5mSuVMc0ynneafpnL/zv52acZ6kqeS0t88= +github.com/alicebob/miniredis/v2 v2.23.1 h1:jR6wZggBxwWygeXcdNyguCOCIjPsZyNUNlAkTx2fu0U= +github.com/alicebob/miniredis/v2 v2.23.1/go.mod h1:84TWKZlxYkfgMucPBf5SOQBYJceZeQRFIaQgNMiCX6Q= github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef h1:2JGTg6JapxP9/R33ZaagQtAM4EkkSYnIAlOG5EI8gkM= github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef/go.mod h1:JS7hed4L1fj0hXcyEejnW57/7LCetXggd+vwrRnYeII= -github.com/avast/retry-go/v4 v4.0.5 h1:C0Fm9MjPCmgLW6Jb1zBTVRx0ycr+VUaaUZO5wpqYjqg= -github.com/avast/retry-go/v4 v4.0.5/go.mod h1:HqmLvS2VLdStPCGDFjSuZ9pzlTqVRldCI4w2dO4m1Ms= +github.com/avast/retry-go/v4 v4.3.0 h1:cqI48aXx0BExKoM7XPklDpoHAg7/srPPLAfWG5z62jo= +github.com/avast/retry-go/v4 v4.3.0/go.mod h1:bqOlT4nxk4phk9buiQFaghzjpqdchOSwPgjdfdQBtdg= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bradleyjkemp/cupaloy v2.3.0+incompatible h1:UafIjBvWQmS9i/xRg+CamMrnLTKNzo+bdmT/oH34c2Y= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= @@ -72,7 +76,8 @@ github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creasty/defaults v1.6.0 h1:ltuE9cfphUtlrBeomuu8PEyISTXnxqkBIoQfXgv7BSc= @@ -82,11 +87,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dosgo/zigtool v0.0.0-20210923085854-9c6fc1d62198 h1:3b37D/Oxs95GmDsGKNx21aBYWF270emHjqUExsAL01g= +github.com/dosgo/zigtool v0.0.0-20210923085854-9c6fc1d62198/go.mod h1:NUrh34aXXgbs4C2HkTmRmkzsKhtrFPRitYkbZMDDONo= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= @@ -97,24 +107,31 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= -github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= -github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= -github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/spec v0.20.7 h1:1Rlu/ZrOCCob0n+JKKJAWhNWMPW8bOZRg8FJaY+0SKI= +github.com/go-openapi/spec v0.20.7/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -129,6 +146,8 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -156,8 +175,9 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -168,6 +188,8 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -184,9 +206,14 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= @@ -197,8 +224,8 @@ github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsU github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= -github.com/jackc/pgconn v1.12.1 h1:rsDFzIpRk7xT4B8FufgpCCeyjdNpKyghZeSefViE5W8= -github.com/jackc/pgconn v1.12.1/go.mod h1:ZkhRC59Llhrq3oSfrikvwQ5NaxYExr6twkdkMLaKono= +github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys= +github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= @@ -214,26 +241,26 @@ github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvW github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.3.0 h1:brH0pCGBDkBW07HWlN/oSBXrmo3WB0UvZd1pIuDcL8Y= -github.com/jackc/pgproto3/v2 v2.3.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y= +github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= -github.com/jackc/pgtype v1.11.0 h1:u4uiGPz/1hryuXzyaBhSk6dnIyyG2683olG2OV+UUgs= -github.com/jackc/pgtype v1.11.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w= +github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= -github.com/jackc/pgx/v4 v4.16.1 h1:JzTglcal01DrghUqt+PmzWsZx/Yh7SC/CTQmSBMTd0Y= -github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ= +github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E= +github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= @@ -256,43 +283,50 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxv github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= +github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= +github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0= -github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/goveralls v0.0.11 h1:eJXea6R6IFlL1QMKNMzDvvHv/hwGrnvyig4N+0+XiMM= +github.com/mattn/goveralls v0.0.11/go.mod h1:gU8SyhNswsJKchEV93xRQxX6X3Ei4PJdQk/6ZHvrvRk= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/miekg/dns v1.1.49 h1:qe0mQU3Z/XpFeE+AEBo2rqaS1IPBJ3anmqZ4XiZJVG8= -github.com/miekg/dns v1.1.49/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= +github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -305,11 +339,10 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo/v2 v2.1.4 h1:GNapqRSid3zijZ9H77KrgVG4/8KqiyRsxcSxe+7ApXY= -github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= -github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= -github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/pierrre/gotestcover v0.0.0-20160517101806-924dca7d15f0/go.mod h1:4xpMLz7RBWyB+ElzHu8Llua96TRCB3YwX+l5EP1wmHk= +github.com/onsi/ginkgo/v2 v2.5.0 h1:TRtrvv2vdQqzkwrQ1ke6vtXf7IK34RBUJafIy1wMwls= +github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw= +github.com/onsi/gomega v1.24.1 h1:KORJXNNTzJXzu4ScJWssJfJMnJ+2QJqhoQSRwNlze9E= +github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -320,29 +353,36 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34= -github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= +github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s= +github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= +github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/ramr/go-reaper v0.2.1 h1:zww+wlQOvTjBZuk1920R/e0GFEb6O7+B0WQLV6dM924= +github.com/ramr/go-reaper v0.2.1/go.mod h1:AVypdzrcCXjSc/JYnlXl8TsB+z84WyFzxWE8Jh0MOJc= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= @@ -352,36 +392,44 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= -github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= -github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= -github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/swaggo/swag v1.8.2 h1:D4aBiVS2a65zhyk3WFqOUz7Rz0sOaUcgeErcid5uGL4= -github.com/swaggo/swag v1.8.2/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/swaggo/swag v1.8.7 h1:2K9ivTD3teEO+2fXV6zrZKDqk5IuU2aJtBDo8U7omWU= +github.com/swaggo/swag v1.8.7/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk= +github.com/urfave/cli/v2 v2.23.0 h1:pkly7gKIeYv3olPAeNajNpLjeJrmTPYCoZWaV+2VfvE= +github.com/urfave/cli/v2 v2.23.0/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 h1:k/gmLsJDWwWqbLCur2yWnJzwQEKRcAHXo6seXGuSwWw= -github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA= +github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 h1:5mLPGnFdSsevFRFc9q3yYbBkB6tsm4aCwwQV/j1JQAQ= +github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -410,9 +458,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b h1:QAqMVf3pSa6eeTsuklijukjXBlj7Es2QQplab+/RbQ4= -golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -444,8 +492,8 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -477,18 +525,20 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -498,8 +548,9 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde h1:ejfdSekXMDxDLbRrJMwUk6KnSLZ2McaUCVcIKM+N6jc= +golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -540,25 +591,25 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211107104306-e0b2ad06fe42/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 h1:OH54vjqzRWmbJ62fjuhxy7AxFFgoHN0/DPc/UrL8cAs= -golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -566,8 +617,9 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -616,16 +668,15 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= -golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= -golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= @@ -702,15 +753,14 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= @@ -723,17 +773,20 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/mysql v1.3.4 h1:/KoBMgsUHC3bExsekDcmNYaBnfH2WNeFuXqqrqMc98Q= -gorm.io/driver/mysql v1.3.4/go.mod h1:s4Tq0KmD0yhPGHbZEwg1VPlH0vT/GBHJZorPzhcxBUE= -gorm.io/driver/postgres v1.3.7 h1:FKF6sIMDHDEvvMF/XJvbnCl0nu6KSKUaPXevJ4r+VYQ= -gorm.io/driver/postgres v1.3.7/go.mod h1:f02ympjIcgtHEGFMZvdgTxODZ9snAHDb4hXfigBVuNI= -gorm.io/driver/sqlite v1.3.2 h1:nWTy4cE52K6nnMhv23wLmur9Y3qWbZvOBz+V4PrGAxg= -gorm.io/driver/sqlite v1.3.2/go.mod h1:B+8GyC9K7VgzJAcrcXMRPdnMcck+8FgJynEehEPM16U= -gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= -gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM= -gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/driver/mysql v1.4.3 h1:/JhWJhO2v17d8hjApTltKNADm7K7YI2ogkR7avJUL3k= +gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c= +gorm.io/driver/postgres v1.4.5 h1:mTeXTTtHAgnS9PgmhN2YeUbazYpLhUI1doLnw42XUZc= +gorm.io/driver/postgres v1.4.5/go.mod h1:GKNQYSJ14qvWkvPwXljMGehpKrhlDNsqYRr5HnYGncg= +gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= +gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= +gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= +gorm.io/gorm v1.24.1-0.20221019064659-5dd2bb482755 h1:7AdrbfcvKnzejfqP5g37fdSZOXH/JvaPIzBIHTOqXKk= +gorm.io/gorm v1.24.1-0.20221019064659-5dd2bb482755/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/helpertest/helper.go b/helpertest/helper.go index 38817926..626ac148 100644 --- a/helpertest/helper.go +++ b/helpertest/helper.go @@ -3,7 +3,6 @@ package helpertest import ( "bytes" "fmt" - "io/ioutil" "net/http" "net/http/httptest" "os" @@ -16,7 +15,7 @@ import ( // TempFile creates temp file with passed data func TempFile(data string) *os.File { - f, err := ioutil.TempFile("", "prefix") + f, err := os.CreateTemp("", "prefix") if err != nil { log.Log().Fatal(err) } @@ -40,7 +39,8 @@ func TestServer(data string) *httptest.Server { } // DoGetRequest performs a GET request -func DoGetRequest(url string, fn func(w http.ResponseWriter, r *http.Request)) (code int, body *bytes.Buffer) { +func DoGetRequest(url string, + fn func(w http.ResponseWriter, r *http.Request)) (*httptest.ResponseRecorder, *bytes.Buffer) { r, _ := http.NewRequest("GET", url, nil) rr := httptest.NewRecorder() @@ -48,7 +48,7 @@ func DoGetRequest(url string, fn func(w http.ResponseWriter, r *http.Request)) ( handler.ServeHTTP(rr, r) - return rr.Code, rr.Body + return rr, rr.Body } // BeDNSRecord returns new dns matcher diff --git a/helpertest/tmpdata.go b/helpertest/tmpdata.go new file mode 100644 index 00000000..3826d8cb --- /dev/null +++ b/helpertest/tmpdata.go @@ -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 +} diff --git a/lists/downloader_test.go b/lists/downloader_test.go index 4bcacd77..86d69fac 100644 --- a/lists/downloader_test.go +++ b/lists/downloader_test.go @@ -183,9 +183,10 @@ var _ = Describe("Downloader", func() { It("Should perform a retry until max retry attempt count is reached and return TransientError", func() { reader, err := sut.DownloadFile(server.URL) Expect(err).Should(HaveOccurred()) - var transientErr *TransientError - Expect(errors.As(err, &transientErr)).To(BeTrue()) - Expect(transientErr.Unwrap().Error()).Should(ContainSubstring("Timeout")) + + err2 := unwrapTransientErr(err) + + Expect(err2.Error()).Should(ContainSubstring("Timeout")) Expect(reader).Should(BeNil()) // failed download event was emitted 3 times @@ -196,15 +197,18 @@ var _ = Describe("Downloader", func() { When("DNS resolution of passed URL fails", func() { BeforeEach(func() { sut = NewDownloader( - WithTimeout(100*time.Millisecond), + WithTimeout(500*time.Millisecond), WithAttempts(3), - WithCooldown(time.Millisecond)) + WithCooldown(200*time.Millisecond)) }) It("Should perform a retry until max retry attempt count is reached and return DNSError", func() { reader, err := sut.DownloadFile("http://some.domain.which.does.not.exist") Expect(err).Should(HaveOccurred()) + + err2 := unwrapTransientErr(err) + var dnsError *net.DNSError - Expect(errors.As(err, &dnsError)).To(BeTrue(), "received error %w", err) + Expect(errors.As(err2, &dnsError)).To(BeTrue(), "received error %w", err) Expect(reader).Should(BeNil()) // failed download event was emitted 3 times @@ -215,3 +219,12 @@ var _ = Describe("Downloader", func() { }) }) }) + +func unwrapTransientErr(origErr error) error { + var transientErr *TransientError + if errors.As(origErr, &transientErr) { + return transientErr.Unwrap() + } + + return origErr +} diff --git a/lists/list_cache.go b/lists/list_cache.go index f84631f8..e6035fa2 100644 --- a/lists/list_cache.go +++ b/lists/list_cache.go @@ -1,6 +1,6 @@ package lists -//go:generate go-enum -f=$GOFILE --marshal --names +//go:generate go run github.com/abice/go-enum -f=$GOFILE --marshal --names import ( "bufio" "errors" @@ -95,7 +95,7 @@ func (b *ListCache) Configuration() (result []string) { // NewListCache creates new list instance func NewListCache(t ListCacheType, groupToLinks map[string][]string, refreshPeriod time.Duration, - downloader FileDownloader, processingConcurrency uint) (*ListCache, error) { + downloader FileDownloader, processingConcurrency uint, async bool) (*ListCache, error) { groupCaches := make(map[string]stringcache.StringCache) if processingConcurrency == 0 { @@ -110,7 +110,13 @@ func NewListCache(t ListCacheType, groupToLinks map[string][]string, refreshPeri listType: t, processingConcurrency: processingConcurrency, } - initError := b.refresh(true) + + var initError error + if async { + initError = nil + } else { + initError = b.refresh(true) + } if initError == nil { go periodicUpdate(b) diff --git a/lists/list_cache_benchmark_test.go b/lists/list_cache_benchmark_test.go index cc374fdd..a70fd0bc 100644 --- a/lists/list_cache_benchmark_test.go +++ b/lists/list_cache_benchmark_test.go @@ -12,7 +12,7 @@ func BenchmarkRefresh(b *testing.B) { "gr1": {file1, file2, file3}, } - cache, _ := NewListCache(ListCacheTypeBlacklist, lists, -1, NewDownloader(), 5) + cache, _ := NewListCache(ListCacheTypeBlacklist, lists, -1, NewDownloader(), 5, false) b.ReportAllocs() diff --git a/lists/list_cache_enum.go b/lists/list_cache_enum.go index a78a70c3..c1fdf38e 100644 --- a/lists/list_cache_enum.go +++ b/lists/list_cache_enum.go @@ -20,6 +20,8 @@ const ( ListCacheTypeWhitelist ) +var ErrInvalidListCacheType = fmt.Errorf("not a valid ListCacheType, try [%s]", strings.Join(_ListCacheTypeNames, ", ")) + const _ListCacheTypeName = "blacklistwhitelist" var _ListCacheTypeNames = []string{ @@ -57,7 +59,7 @@ func ParseListCacheType(name string) (ListCacheType, error) { if x, ok := _ListCacheTypeValue[name]; ok { return x, nil } - return ListCacheType(0), fmt.Errorf("%s is not a valid ListCacheType, try [%s]", name, strings.Join(_ListCacheTypeNames, ", ")) + return ListCacheType(0), fmt.Errorf("%s is %w", name, ErrInvalidListCacheType) } // MarshalText implements the text marshaller method. diff --git a/lists/list_cache_test.go b/lists/list_cache_test.go index 88a0de74..3f496420 100644 --- a/lists/list_cache_test.go +++ b/lists/list_cache_test.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "log" "math/rand" "net/http/httptest" @@ -22,36 +21,39 @@ import ( var _ = Describe("ListCache", func() { var ( - emptyFile, file1, file2, file3 *os.File + tmpDir *TmpFolder + emptyFile, file1, file2, file3 *TmpFile server1, server2, server3 *httptest.Server ) BeforeEach(func() { - emptyFile = TempFile("#empty file\n\n") - server1 = TestServer("blocked1.com\nblocked1a.com\n192.168.178.55") - server2 = TestServer("blocked2.com") - server3 = TestServer("blocked3.com\nblocked1a.com") + tmpDir = NewTmpFolder("ListCache") + Expect(tmpDir.Error).Should(Succeed()) + DeferCleanup(tmpDir.Clean) - file1 = TempFile("blocked1.com\nblocked1a.com") - file2 = TempFile("blocked2.com") - file3 = TempFile("blocked3.com\nblocked1a.com") - }) - AfterEach(func() { - _ = os.Remove(emptyFile.Name()) - _ = os.Remove(file1.Name()) - _ = os.Remove(file2.Name()) - _ = os.Remove(file3.Name()) - server1.Close() - server2.Close() - server3.Close() + server1 = TestServer("blocked1.com\nblocked1a.com\n192.168.178.55") + DeferCleanup(server1.Close) + server2 = TestServer("blocked2.com") + DeferCleanup(server2.Close) + server3 = TestServer("blocked3.com\nblocked1a.com") + DeferCleanup(server3.Close) + + emptyFile = tmpDir.CreateStringFile("empty", "#empty file") + Expect(emptyFile.Error).Should(Succeed()) + file1 = tmpDir.CreateStringFile("file1", "blocked1.com", "blocked1a.com") + Expect(file1.Error).Should(Succeed()) + file2 = tmpDir.CreateStringFile("file2", "blocked2.com") + Expect(file2.Error).Should(Succeed()) + file3 = tmpDir.CreateStringFile("file3", "blocked3.com", "blocked1a.com") + Expect(file3.Error).Should(Succeed()) }) Describe("List cache and matching", func() { When("Query with empty", func() { It("should not panic", func() { lists := map[string][]string{ - "gr0": {emptyFile.Name()}, + "gr0": {emptyFile.Path}, } - sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency) + sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency, false) Expect(err).Should(Succeed()) found, group := sut.Match("", []string{"gr0"}) @@ -63,9 +65,9 @@ var _ = Describe("ListCache", func() { When("List is empty", func() { It("should not match anything", func() { lists := map[string][]string{ - "gr1": {emptyFile.Name()}, + "gr1": {emptyFile.Path}, } - sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency) + sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency, false) Expect(err).Should(Succeed()) found, group := sut.Match("google.com", []string{"gr1"}) @@ -79,12 +81,15 @@ var _ = Describe("ListCache", func() { // should produce a transient error on second and third attempt data := make(chan func() (io.ReadCloser, error), 3) mockDownloader := &MockDownloader{data: data} + // nolint:unparam data <- func() (io.ReadCloser, error) { return io.NopCloser(strings.NewReader("blocked1.com")), nil } + // nolint:unparam data <- func() (io.ReadCloser, error) { return nil, &TransientError{inner: errors.New("boom")} } + // nolint:unparam data <- func() (io.ReadCloser, error) { return nil, &TransientError{inner: errors.New("boom")} } @@ -97,6 +102,7 @@ var _ = Describe("ListCache", func() { 4*time.Hour, mockDownloader, defaultProcessingConcurrency, + false, ) Expect(err).Should(Succeed()) @@ -131,6 +137,7 @@ var _ = Describe("ListCache", func() { // should produce a 404 err on second attempt data := make(chan func() (io.ReadCloser, error), 2) mockDownloader := &MockDownloader{data: data} + //nolint:unparam data <- func() (io.ReadCloser, error) { return io.NopCloser(strings.NewReader("blocked1.com")), nil } @@ -141,7 +148,8 @@ var _ = Describe("ListCache", func() { "gr1": {"http://dummy"}, } - sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, mockDownloader, defaultProcessingConcurrency) + sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, mockDownloader, + defaultProcessingConcurrency, false) Expect(err).Should(Succeed()) By("Lists loaded without err", func() { @@ -171,7 +179,7 @@ var _ = Describe("ListCache", func() { "gr2": {server3.URL}, } - sut, _ := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency) + sut, _ := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency, false) found, group := sut.Match("blocked1.com", []string{"gr1", "gr2"}) Expect(found).Should(BeTrue()) @@ -193,7 +201,7 @@ var _ = Describe("ListCache", func() { "gr2": {server3.URL, "someotherfile"}, } - sut, _ := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency) + sut, _ := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency, false) found, group := sut.Match("blocked1.com", []string{"gr1", "gr2"}) Expect(found).Should(BeTrue()) @@ -220,7 +228,7 @@ var _ = Describe("ListCache", func() { resultCnt = cnt }) - sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency) + sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency, false) Expect(err).Should(Succeed()) found, group := sut.Match("blocked1.com", []string{}) @@ -232,11 +240,11 @@ var _ = Describe("ListCache", func() { When("multiple groups are passed", func() { It("should match", func() { lists := map[string][]string{ - "gr1": {file1.Name(), file2.Name()}, - "gr2": {"file://" + file3.Name()}, + "gr1": {file1.Path, file2.Path}, + "gr2": {"file://" + file3.Path}, } - sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency) + sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency, false) Expect(err).Should(Succeed()) Expect(sut.groupCaches["gr1"].ElementCount()).Should(Equal(3)) @@ -264,7 +272,8 @@ var _ = Describe("ListCache", func() { "gr1": {file1, file2, file3}, } - sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency) + sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), + defaultProcessingConcurrency, false) Expect(err).Should(Succeed()) Expect(sut.groupCaches["gr1"].ElementCount()).Should(Equal(38000)) @@ -276,7 +285,8 @@ var _ = Describe("ListCache", func() { "gr1": {"inlinedomain1.com\n#some comment\ninlinedomain2.com"}, } - sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency) + sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), + defaultProcessingConcurrency, false) Expect(err).Should(Succeed()) Expect(sut.groupCaches["gr1"].ElementCount()).Should(Equal(2)) @@ -296,7 +306,8 @@ var _ = Describe("ListCache", func() { "gr1": {"inlinedomain1.com\n" + strings.Repeat("longString", 100000)}, } - sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency) + sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), + defaultProcessingConcurrency, false) Expect(err).Should(Succeed()) found, group := sut.Match("inlinedomain1.com", []string{"gr1"}) @@ -310,7 +321,8 @@ var _ = Describe("ListCache", func() { "gr1": {"/^apple\\.(de|com)$/\n"}, } - sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), defaultProcessingConcurrency) + sut, err := NewListCache(ListCacheTypeBlacklist, lists, 0, NewDownloader(), + defaultProcessingConcurrency, false) Expect(err).Should(Succeed()) found, group := sut.Match("apple.com", []string{"gr1"}) @@ -331,7 +343,8 @@ var _ = Describe("ListCache", func() { "gr2": {"inline\ndefinition\n"}, } - sut, err := NewListCache(ListCacheTypeBlacklist, lists, time.Hour, NewDownloader(), defaultProcessingConcurrency) + sut, err := NewListCache(ListCacheTypeBlacklist, lists, time.Hour, NewDownloader(), + defaultProcessingConcurrency, false) Expect(err).Should(Succeed()) c := sut.Configuration() @@ -342,10 +355,11 @@ var _ = Describe("ListCache", func() { When("refresh is disabled", func() { It("should print 'refresh disabled'", func() { lists := map[string][]string{ - "gr1": {emptyFile.Name()}, + "gr1": {emptyFile.Path}, } - sut, err := NewListCache(ListCacheTypeBlacklist, lists, -1, NewDownloader(), defaultProcessingConcurrency) + sut, err := NewListCache(ListCacheTypeBlacklist, lists, -1, NewDownloader(), + defaultProcessingConcurrency, false) Expect(err).Should(Succeed()) c := sut.Configuration() @@ -353,6 +367,20 @@ var _ = Describe("ListCache", func() { }) }) }) + + Describe("StartStrategy", func() { + When("async load is enabled", func() { + It("should never return an error", func() { + lists := map[string][]string{ + "gr1": {"doesnotexist"}, + } + + _, err := NewListCache(ListCacheTypeBlacklist, lists, -1, NewDownloader(), + defaultProcessingConcurrency, true) + Expect(err).Should(Succeed()) + }) + }) + }) }) type MockDownloader struct { @@ -366,7 +394,7 @@ func (m *MockDownloader) DownloadFile(_ string) (io.ReadCloser, error) { } func createTestListFile(dir string, totalLines int) string { - file, err := ioutil.TempFile(dir, "blocky") + file, err := os.CreateTemp(dir, "blocky") if err != nil { log.Fatal(err) } diff --git a/log/logger.go b/log/logger.go index 6c3fc417..8f3223f0 100644 --- a/log/logger.go +++ b/log/logger.go @@ -1,9 +1,9 @@ package log -//go:generate go-enum -f=$GOFILE --marshal --names +//go:generate go run github.com/abice/go-enum -f=$GOFILE --marshal --names import ( - "io/ioutil" + "io" "strings" "github.com/sirupsen/logrus" @@ -69,7 +69,7 @@ func ConfigureLogger(logLevel Level, formatType FormatType, logTimestamp bool) { TimestampFormat: "2006-01-02 15:04:05", FullTimestamp: true, ForceFormatting: true, - ForceColors: true, + ForceColors: false, QuoteEmptyFields: true, DisableTimestamp: !logTimestamp} @@ -87,5 +87,5 @@ func ConfigureLogger(logLevel Level, formatType FormatType, logTimestamp bool) { // Silence disables the logger output func Silence() { - logger.Out = ioutil.Discard + logger.Out = io.Discard } diff --git a/log/logger_enum.go b/log/logger_enum.go index 7a060472..70da43db 100644 --- a/log/logger_enum.go +++ b/log/logger_enum.go @@ -20,6 +20,8 @@ const ( FormatTypeJson ) +var ErrInvalidFormatType = fmt.Errorf("not a valid FormatType, try [%s]", strings.Join(_FormatTypeNames, ", ")) + const _FormatTypeName = "textjson" var _FormatTypeNames = []string{ @@ -57,7 +59,7 @@ func ParseFormatType(name string) (FormatType, error) { if x, ok := _FormatTypeValue[name]; ok { return x, nil } - return FormatType(0), fmt.Errorf("%s is not a valid FormatType, try [%s]", name, strings.Join(_FormatTypeNames, ", ")) + return FormatType(0), fmt.Errorf("%s is %w", name, ErrInvalidFormatType) } // MarshalText implements the text marshaller method. @@ -91,6 +93,8 @@ const ( LevelFatal ) +var ErrInvalidLevel = fmt.Errorf("not a valid Level, try [%s]", strings.Join(_LevelNames, ", ")) + const _LevelName = "infotracedebugwarnerrorfatal" var _LevelNames = []string{ @@ -140,7 +144,7 @@ func ParseLevel(name string) (Level, error) { if x, ok := _LevelValue[name]; ok { return x, nil } - return Level(0), fmt.Errorf("%s is not a valid Level, try [%s]", name, strings.Join(_LevelNames, ", ")) + return Level(0), fmt.Errorf("%s is %w", name, ErrInvalidLevel) } // MarshalText implements the text marshaller method. diff --git a/main_static.go b/main_static.go new file mode 100644 index 00000000..2b66b5b4 --- /dev/null +++ b/main_static.go @@ -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 + } + } +} diff --git a/model/models.go b/model/models.go index 3925ca31..99aa2ebe 100644 --- a/model/models.go +++ b/model/models.go @@ -1,6 +1,6 @@ package model -//go:generate go-enum -f=$GOFILE --marshal --names +//go:generate go run github.com/abice/go-enum -f=$GOFILE --marshal --names import ( "net" "time" @@ -17,6 +17,8 @@ import ( // CUSTOMDNS // the query was resolved by a custom rule // HOSTSFILE // the query was resolved by looking up the hosts file // FILTERED // the query was filtered by query type +// NOTFQDN // the query was filtered as it is not fqdn conform +// SPECIAL // the query was resolved by the special use domain name resolver // ) type ResponseType int diff --git a/model/models_enum.go b/model/models_enum.go index 70f49f92..ad6f939c 100644 --- a/model/models_enum.go +++ b/model/models_enum.go @@ -20,6 +20,8 @@ const ( RequestProtocolUDP ) +var ErrInvalidRequestProtocol = fmt.Errorf("not a valid RequestProtocol, try [%s]", strings.Join(_RequestProtocolNames, ", ")) + const _RequestProtocolName = "TCPUDP" var _RequestProtocolNames = []string{ @@ -57,7 +59,7 @@ func ParseRequestProtocol(name string) (RequestProtocol, error) { if x, ok := _RequestProtocolValue[name]; ok { return x, nil } - return RequestProtocol(0), fmt.Errorf("%s is not a valid RequestProtocol, try [%s]", name, strings.Join(_RequestProtocolNames, ", ")) + return RequestProtocol(0), fmt.Errorf("%s is %w", name, ErrInvalidRequestProtocol) } // MarshalText implements the text marshaller method. @@ -98,9 +100,17 @@ const ( // ResponseTypeFILTERED is a ResponseType of type FILTERED. // the query was filtered by query type ResponseTypeFILTERED + // ResponseTypeNOTFQDN is a ResponseType of type NOTFQDN. + // the query was filtered as it is not fqdn conform + ResponseTypeNOTFQDN + // ResponseTypeSPECIAL is a ResponseType of type SPECIAL. + // the query was resolved by the special use domain name resolver + ResponseTypeSPECIAL ) -const _ResponseTypeName = "RESOLVEDCACHEDBLOCKEDCONDITIONALCUSTOMDNSHOSTSFILEFILTERED" +var ErrInvalidResponseType = fmt.Errorf("not a valid ResponseType, try [%s]", strings.Join(_ResponseTypeNames, ", ")) + +const _ResponseTypeName = "RESOLVEDCACHEDBLOCKEDCONDITIONALCUSTOMDNSHOSTSFILEFILTEREDNOTFQDNSPECIAL" var _ResponseTypeNames = []string{ _ResponseTypeName[0:8], @@ -110,6 +120,8 @@ var _ResponseTypeNames = []string{ _ResponseTypeName[32:41], _ResponseTypeName[41:50], _ResponseTypeName[50:58], + _ResponseTypeName[58:65], + _ResponseTypeName[65:72], } // ResponseTypeNames returns a list of possible string values of ResponseType. @@ -127,6 +139,8 @@ var _ResponseTypeMap = map[ResponseType]string{ ResponseTypeCUSTOMDNS: _ResponseTypeName[32:41], ResponseTypeHOSTSFILE: _ResponseTypeName[41:50], ResponseTypeFILTERED: _ResponseTypeName[50:58], + ResponseTypeNOTFQDN: _ResponseTypeName[58:65], + ResponseTypeSPECIAL: _ResponseTypeName[65:72], } // String implements the Stringer interface. @@ -145,6 +159,8 @@ var _ResponseTypeValue = map[string]ResponseType{ _ResponseTypeName[32:41]: ResponseTypeCUSTOMDNS, _ResponseTypeName[41:50]: ResponseTypeHOSTSFILE, _ResponseTypeName[50:58]: ResponseTypeFILTERED, + _ResponseTypeName[58:65]: ResponseTypeNOTFQDN, + _ResponseTypeName[65:72]: ResponseTypeSPECIAL, } // ParseResponseType attempts to convert a string to a ResponseType. @@ -152,7 +168,7 @@ func ParseResponseType(name string) (ResponseType, error) { if x, ok := _ResponseTypeValue[name]; ok { return x, nil } - return ResponseType(0), fmt.Errorf("%s is not a valid ResponseType, try [%s]", name, strings.Join(_ResponseTypeNames, ", ")) + return ResponseType(0), fmt.Errorf("%s is %w", name, ErrInvalidResponseType) } // MarshalText implements the text marshaller method. diff --git a/querylog/database_writer.go b/querylog/database_writer.go index eb5951b1..0c9e351c 100644 --- a/querylog/database_writer.go +++ b/querylog/database_writer.go @@ -2,6 +2,7 @@ package querylog import ( "fmt" + "reflect" "strings" "sync" "time" @@ -70,7 +71,7 @@ func newDatabaseWriter(target gorm.Dialector, logRetentionDays uint64, } // Migrate the schema - if err := db.AutoMigrate(&logEntry{}); err != nil { + if err := databaseMigration(db); err != nil { return nil, fmt.Errorf("can't perform auto migration: %w", err) } @@ -84,6 +85,35 @@ func newDatabaseWriter(target gorm.Dialector, logRetentionDays uint64, return w, nil } +func databaseMigration(db *gorm.DB) error { + if err := db.AutoMigrate(&logEntry{}); err != nil { + return err + } + + tableName := db.NamingStrategy.TableName(reflect.TypeOf(logEntry{}).Name()) + + // create unmapped primary key + switch db.Config.Name() { + case "mysql": + tx := db.Exec("ALTER TABLE `" + tableName + "` ADD `id` INT PRIMARY KEY AUTO_INCREMENT") + if tx.Error != nil { + // mysql doesn't support "add column if not exist" + if strings.Contains(tx.Error.Error(), "1060") { + // error 1060: duplicate column name + // ignore it + return nil + } + + return tx.Error + } + + case "postgres": + return db.Exec("ALTER TABLE " + tableName + " ADD column if not exists id serial primary key").Error + } + + return nil +} + func (d *DatabaseWriter) periodicFlush() { ticker := time.NewTicker(d.dbFlushPeriod) defer ticker.Stop() diff --git a/querylog/database_writer_test.go b/querylog/database_writer_test.go index da818337..dad0ce96 100644 --- a/querylog/database_writer_test.go +++ b/querylog/database_writer_test.go @@ -9,61 +9,102 @@ import ( "github.com/miekg/dns" "github.com/sirupsen/logrus" "gorm.io/driver/sqlite" + "gorm.io/gorm" . "github.com/onsi/gomega" . "github.com/onsi/ginkgo/v2" ) +var err error + var _ = Describe("DatabaseWriter", func() { - Describe("Database query log", func() { - When("New log entry was created", func() { - It("should be persisted in the database", func() { - sqlite := sqlite.Open("file::memory:") - writer, err := newDatabaseWriter(sqlite, 7, time.Millisecond) - Expect(err).Should(Succeed()) - request := &model.Request{ - Req: util.NewMsgWithQuestion("google.de.", dns.Type(dns.TypeA)), - Log: logrus.NewEntry(log.Log()), - } - res, err := util.NewMsgWithAnswer("example.com", 123, dns.Type(dns.TypeA), "123.124.122.122") + Describe("Database query log to sqlite", func() { + var ( + sqliteDB gorm.Dialector + writer *DatabaseWriter + request *model.Request + ) + BeforeEach(func() { + sqliteDB = sqlite.Open("file::memory:") + + request = &model.Request{ + Req: util.NewMsgWithQuestion("google.de.", dns.Type(dns.TypeA)), + Log: logrus.NewEntry(log.Log()), + } + }) + + When("New log entry was created", func() { + BeforeEach(func() { + writer, err = newDatabaseWriter(sqliteDB, 7, time.Millisecond) Expect(err).Should(Succeed()) + }) + + It("should be persisted in the database", func() { + res, err := util.NewMsgWithAnswer("example.com", 123, dns.Type(dns.TypeA), "123.124.122.122") + Expect(err).Should(Succeed()) + response := &model.Response{ Res: res, Reason: "Resolved", RType: model.ResponseTypeRESOLVED, } + + // one entry with now as timestamp writer.Write(&LogEntry{ Request: request, Response: response, Start: time.Now(), DurationMs: 20, }) + + // one entry before 2 days + writer.Write(&LogEntry{ + Request: request, + Response: response, + Start: time.Now().AddDate(0, 0, -2), + DurationMs: 20, + }) + + // force write + writer.doDBWrite() + + // 2 entries in the database + Eventually(func() int64 { + var res int64 + result := writer.db.Find(&logEntry{}) + + result.Count(&res) + + return res + }, "5s").Should(BeNumerically("==", 2)) + + // do cleanup now + writer.CleanUp() + + // now only 1 entry in the database Eventually(func() (res int64) { result := writer.db.Find(&logEntry{}) result.Count(&res) return res - }, "1s").Should(BeNumerically("==", 1)) + }, "5s").Should(BeNumerically("==", 2)) }) }) When("There are log entries with timestamp exceeding the retention period", func() { + BeforeEach(func() { + writer, err = newDatabaseWriter(sqliteDB, 1, time.Millisecond) + Expect(err).Should(Succeed()) + }) + It("these old entries should be deleted", func() { - sqlite := sqlite.Open("file::memory:") - writer, err := newDatabaseWriter(sqlite, 1, time.Millisecond) - Expect(err).Should(Succeed()) - - request := &model.Request{ - Req: util.NewMsgWithQuestion("google.de.", dns.Type(dns.TypeA)), - Log: logrus.NewEntry(log.Log()), - } res, err := util.NewMsgWithAnswer("example.com", 123, dns.Type(dns.TypeA), "123.124.122.122") - Expect(err).Should(Succeed()) + response := &model.Response{ Res: res, Reason: "Resolved", @@ -86,6 +127,9 @@ var _ = Describe("DatabaseWriter", func() { DurationMs: 20, }) + // force write + writer.doDBWrite() + // 2 entries in the database Eventually(func() int64 { var res int64 @@ -94,7 +138,7 @@ var _ = Describe("DatabaseWriter", func() { result.Count(&res) return res - }, "1s").Should(BeNumerically("==", 2)) + }, "5s").Should(BeNumerically("==", 2)) // do cleanup now writer.CleanUp() @@ -106,10 +150,12 @@ var _ = Describe("DatabaseWriter", func() { result.Count(&res) return res - }, "1s").Should(BeNumerically("==", 1)) + }, "5s").Should(BeNumerically("==", 1)) }) }) + }) + Describe("Database query log fails", func() { When("mysql connection parameters wrong", func() { It("should be log with fatal", func() { _, err := NewDatabaseWriter("mysql", "wrong param", 7, 1) diff --git a/querylog/file_writer.go b/querylog/file_writer.go index 78ee033d..1d7eca27 100644 --- a/querylog/file_writer.go +++ b/querylog/file_writer.go @@ -4,7 +4,6 @@ import ( "encoding/csv" "fmt" "io" - "io/ioutil" "os" "path/filepath" "regexp" @@ -59,14 +58,13 @@ func (d *FileWriter) Write(entry *LogEntry) { "can't create/open file", err) if err == nil { + defer file.Close() writer := createCsvWriter(file) err := writer.Write(createQueryLogRow(entry)) util.LogOnErrorWithEntry(log.PrefixedLog(loggerPrefixFileWriter).WithField("file_name", writePath), "can't write to file", err) writer.Flush() - - _ = file.Close() } } @@ -78,7 +76,7 @@ func (d *FileWriter) CleanUp() { logger.Trace("starting clean up") - files, err := ioutil.ReadDir(d.target) + files, err := os.ReadDir(d.target) util.LogOnErrorWithEntry(logger.WithField("target", d.target), "can't list log directory: ", err) diff --git a/querylog/file_writer_test.go b/querylog/file_writer_test.go index e1885e80..875cfa42 100644 --- a/querylog/file_writer_test.go +++ b/querylog/file_writer_test.go @@ -6,11 +6,10 @@ import ( "errors" "fmt" "io" - "io/ioutil" "os" - "path/filepath" "time" + "github.com/0xERR0R/blocky/helpertest" "github.com/0xERR0R/blocky/log" "github.com/0xERR0R/blocky/model" @@ -21,14 +20,16 @@ import ( ) var _ = Describe("FileWriter", func() { - var tmpDir string - var err error - BeforeEach(func() { - tmpDir, err = ioutil.TempDir("", "fileWriter") - Expect(err).Should(Succeed()) - }) - AfterEach(func() { - _ = os.RemoveAll(tmpDir) + var ( + tmpDir *helpertest.TmpFolder + err error + writer *FileWriter + ) + + JustBeforeEach(func() { + tmpDir = helpertest.NewTmpFolder("fileWriter") + Expect(tmpDir.Error).Should(Succeed()) + DeferCleanup(tmpDir.Clean) }) Describe("CSV writer", func() { @@ -40,9 +41,10 @@ var _ = Describe("FileWriter", func() { }) When("New log entry was created", func() { It("should be logged in one file", func() { - tmpDir, err = ioutil.TempDir("", "queryLoggingResolver") + writer, err = NewCSVWriter(tmpDir.Path, false, 0) + Expect(err).Should(Succeed()) - writer, _ := NewCSVWriter(tmpDir, false, 0) + res, err := util.NewMsgWithAnswer("example.com", 123, dns.Type(dns.TypeA), "123.124.122.122") Expect(err).Should(Succeed()) @@ -81,15 +83,17 @@ var _ = Describe("FileWriter", func() { }) }) - csvLines := readCsv(filepath.Join(tmpDir, fmt.Sprintf("%s_ALL.log", time.Now().Format("2006-01-02")))) - Expect(csvLines).Should(HaveLen(2)) - + Eventually(func(g Gomega) int { + return len(readCsv(tmpDir.JoinPath( + fmt.Sprintf("%s_ALL.log", time.Now().Format("2006-01-02"))))) + }).Should(Equal(2)) }) It("should be logged in separate files per client", func() { - tmpDir, err = ioutil.TempDir("", "queryLoggingResolver") + writer, err = NewCSVWriter(tmpDir.Path, true, 0) + Expect(err).Should(Succeed()) - writer, _ := NewCSVWriter(tmpDir, true, 0) + res, err := util.NewMsgWithAnswer("example.com", 123, dns.Type(dns.TypeA), "123.124.122.122") Expect(err).Should(Succeed()) @@ -128,19 +132,24 @@ var _ = Describe("FileWriter", func() { }) }) - csvLines := readCsv(filepath.Join(tmpDir, fmt.Sprintf("%s_client1.log", time.Now().Format("2006-01-02")))) - Expect(csvLines).Should(HaveLen(1)) + Eventually(func(g Gomega) int { + return len(readCsv(tmpDir.JoinPath( + fmt.Sprintf("%s_client1.log", time.Now().Format("2006-01-02"))))) + }).Should(Equal(1)) - csvLines = readCsv(filepath.Join(tmpDir, fmt.Sprintf("%s_client2.log", time.Now().Format("2006-01-02")))) - Expect(csvLines).Should(HaveLen(1)) + Eventually(func(g Gomega) int { + return len(readCsv(tmpDir.JoinPath( + fmt.Sprintf("%s_client2.log", time.Now().Format("2006-01-02"))))) + }).Should(Equal(1)) }) }) When("Cleanup is called", func() { It("should delete old files", func() { - tmpDir, err = ioutil.TempDir("", "queryLoggingResolver") + writer, err = NewCSVWriter(tmpDir.Path, false, 1) + Expect(err).Should(Succeed()) - writer, _ := NewCSVWriter(tmpDir, false, 1) + res, err := util.NewMsgWithAnswer("example.com", 123, dns.Type(dns.TypeA), "123.124.122.122") Expect(err).Should(Succeed()) @@ -173,19 +182,27 @@ var _ = Describe("FileWriter", func() { Reason: "Resolved", RType: model.ResponseTypeRESOLVED, }, - Start: time.Now().AddDate(0, 0, -2), + Start: time.Now().AddDate(0, 0, -3), DurationMs: 20, }) }) + fmt.Println(tmpDir.Path) - files, err := ioutil.ReadDir(tmpDir) - Expect(err).Should(Succeed()) - Expect(files).Should(HaveLen(2)) - writer.CleanUp() + Eventually(func(g Gomega) int { + filesCount, err := tmpDir.CountFiles() + g.Expect(err).Should(Succeed()) - files, err = ioutil.ReadDir(tmpDir) - Expect(err).Should(Succeed()) - Expect(files).Should(HaveLen(1)) + return filesCount + }, "20s", "1s").Should(Equal(2)) + + go writer.CleanUp() + + Eventually(func(g Gomega) int { + filesCount, err := tmpDir.CountFiles() + g.Expect(err).Should(Succeed()) + + return filesCount + }, "20s", "1s").Should(Equal(1)) }) }) }) diff --git a/resolver/blocking_resolver.go b/resolver/blocking_resolver.go index 6b4683da..0b69e16e 100644 --- a/resolver/blocking_resolver.go +++ b/resolver/blocking_resolver.go @@ -101,13 +101,15 @@ func NewBlockingResolver(cfg config.BlockingConfig, refreshPeriod := time.Duration(cfg.RefreshPeriod) downloader := createDownloader(cfg, bootstrap) blacklistMatcher, blErr := lists.NewListCache(lists.ListCacheTypeBlacklist, cfg.BlackLists, - refreshPeriod, downloader, cfg.ProcessingConcurrency) + refreshPeriod, downloader, cfg.ProcessingConcurrency, + (cfg.StartStrategy == config.StartStrategyTypeFast)) whitelistMatcher, wlErr := lists.NewListCache(lists.ListCacheTypeWhitelist, cfg.WhiteLists, - refreshPeriod, downloader, cfg.ProcessingConcurrency) + refreshPeriod, downloader, cfg.ProcessingConcurrency, + (cfg.StartStrategy == config.StartStrategyTypeFast)) whitelistOnlyGroups := determineWhitelistOnlyGroups(&cfg) err = multierror.Append(err, blErr, wlErr).ErrorOrNil() - if err != nil && cfg.FailStartOnListError { + if err != nil && cfg.StartStrategy == config.StartStrategyTypeFailOnError { return nil, err } @@ -465,6 +467,9 @@ func (r *BlockingResolver) isGroupDisabled(group string) bool { // returns groups which should be checked for client's request func (r *BlockingResolver) groupsToCheckForClient(request *model.Request) []string { + r.status.lock.RLock() + defer r.status.lock.RUnlock() + var groups []string // try client names for _, cName := range request.ClientNames { @@ -613,6 +618,9 @@ func (r *BlockingResolver) queryForFQIdentifierIPs(identifier string) (result [] } func (r *BlockingResolver) initFQDNIPCache() { + r.status.lock.Lock() + defer r.status.lock.Unlock() + identifiers := make([]string, 0) for identifier := range r.clientGroupsBlock { diff --git a/resolver/blocking_resolver_test.go b/resolver/blocking_resolver_test.go index cc62b26f..c9554998 100644 --- a/resolver/blocking_resolver_test.go +++ b/resolver/blocking_resolver_test.go @@ -11,7 +11,6 @@ import ( "github.com/alicebob/miniredis/v2" "github.com/creasty/defaults" - "os" "time" "github.com/miekg/dns" @@ -20,22 +19,26 @@ import ( "github.com/stretchr/testify/mock" ) -var group1File, group2File, defaultGroupFile *os.File +var group1File, group2File, defaultGroupFile *TmpFile +var tmpDir *TmpFolder var _ = BeforeSuite(func() { - group1File = TempFile("DOMAIN1.com") - group2File = TempFile("blocked2.com") - defaultGroupFile = TempFile( - `blocked3.com -123.145.123.145 -2001:db8:85a3:08d3::370:7344 -badcnamedomain.com`) -}) + tmpDir = NewTmpFolder("BlockingResolver") + Expect(tmpDir.Error).Should(Succeed()) + DeferCleanup(tmpDir.Clean) -var _ = AfterSuite(func() { - _ = group1File.Close() - _ = group2File.Close() - _ = defaultGroupFile.Close() + group1File = tmpDir.CreateStringFile("group1File", "DOMAIN1.com") + Expect(group1File.Error).Should(Succeed()) + + group2File = tmpDir.CreateStringFile("group2File", "blocked2.com") + Expect(group2File.Error).Should(Succeed()) + + defaultGroupFile = tmpDir.CreateStringFile("defaultGroupFile", + "blocked3.com", + "123.145.123.145", + "2001:db8:85a3:08d3::370:7344", + "badcnamedomain.com") + Expect(defaultGroupFile.Error).Should(Succeed()) }) var _ = Describe("BlockingResolver", Label("blockingResolver"), func() { @@ -85,8 +88,8 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() { BlockType: "ZEROIP", BlockTTL: config.Duration(time.Minute), BlackLists: map[string][]string{ - "gr1": {group1File.Name()}, - "gr2": {group2File.Name()}, + "gr1": {group1File.Path}, + "gr2": {group2File.Path}, }, } }) @@ -114,8 +117,8 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() { BlockType: "ZEROIP", BlockTTL: config.Duration(time.Minute), BlackLists: map[string][]string{ - "gr1": {group1File.Name()}, - "gr2": {group2File.Name()}, + "gr1": {group1File.Path}, + "gr2": {group2File.Path}, }, ClientGroupsBlock: map[string][]string{ "default": {"gr1"}, @@ -137,13 +140,12 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() { return nil } Bus().Publish(ApplicationStarted, "") - time.Sleep(time.Second) Eventually(func(g Gomega) { resp, err = sut.Resolve(newRequestWithClient("blocked2.com.", dns.Type(dns.TypeA), "192.168.178.39", "client1")) g.Expect(err).NotTo(HaveOccurred()) g.Expect(resp.Res.Answer).ShouldNot(BeNil()) g.Expect(resp.Res.Answer).Should(BeDNSRecord("blocked2.com.", dns.TypeA, 60, "0.0.0.0")) - }, "1s").Should(Succeed()) + }, "10s", "1s").Should(Succeed()) }) }) }) @@ -154,9 +156,9 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() { sutConfig = config.BlockingConfig{ BlockTTL: config.Duration(6 * time.Hour), BlackLists: map[string][]string{ - "gr1": {group1File.Name()}, - "gr2": {group2File.Name()}, - "defaultGroup": {defaultGroupFile.Name()}, + "gr1": {group1File.Path}, + "gr2": {group2File.Path}, + "defaultGroup": {defaultGroupFile.Path}, }, ClientGroupsBlock: map[string][]string{ "client1": {"gr1"}, @@ -293,7 +295,7 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() { sutConfig = config.BlockingConfig{ BlockTTL: config.Duration(time.Minute), BlackLists: map[string][]string{ - "defaultGroup": {defaultGroupFile.Name()}, + "defaultGroup": {defaultGroupFile.Path}, }, ClientGroupsBlock: map[string][]string{ "default": {"defaultGroup"}, @@ -318,7 +320,7 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() { sutConfig = config.BlockingConfig{ BlockType: "ZEROIP", BlackLists: map[string][]string{ - "defaultGroup": {defaultGroupFile.Name()}, + "defaultGroup": {defaultGroupFile.Path}, }, ClientGroupsBlock: map[string][]string{ "default": {"defaultGroup"}, @@ -353,7 +355,7 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() { sutConfig = config.BlockingConfig{ BlockTTL: config.Duration(6 * time.Hour), BlackLists: map[string][]string{ - "defaultGroup": {defaultGroupFile.Name()}, + "defaultGroup": {defaultGroupFile.Path}, }, ClientGroupsBlock: map[string][]string{ "default": {"defaultGroup"}, @@ -381,7 +383,7 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() { BeforeEach(func() { sutConfig = config.BlockingConfig{ BlackLists: map[string][]string{ - "defaultGroup": {defaultGroupFile.Name()}, + "defaultGroup": {defaultGroupFile.Path}, }, ClientGroupsBlock: map[string][]string{ "default": {"defaultGroup"}, @@ -451,8 +453,8 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() { sutConfig = config.BlockingConfig{ BlockType: "ZEROIP", BlockTTL: config.Duration(time.Minute), - BlackLists: map[string][]string{"gr1": {group1File.Name()}}, - WhiteLists: map[string][]string{"gr1": {group1File.Name()}}, + BlackLists: map[string][]string{"gr1": {group1File.Path}}, + WhiteLists: map[string][]string{"gr1": {group1File.Path}}, ClientGroupsBlock: map[string][]string{ "default": {"gr1"}, }, @@ -472,8 +474,8 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() { BlockType: "zeroIP", BlockTTL: config.Duration(60 * time.Second), WhiteLists: map[string][]string{ - "gr1": {group1File.Name()}, - "gr2": {group2File.Name()}, + "gr1": {group1File.Path}, + "gr2": {group2File.Path}, }, ClientGroupsBlock: map[string][]string{ "default": {"gr1"}, @@ -533,8 +535,8 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() { sutConfig = config.BlockingConfig{ BlockType: "ZEROIP", BlockTTL: config.Duration(time.Minute), - BlackLists: map[string][]string{"gr1": {group1File.Name()}}, - WhiteLists: map[string][]string{"gr1": {defaultGroupFile.Name()}}, + BlackLists: map[string][]string{"gr1": {group1File.Path}}, + WhiteLists: map[string][]string{"gr1": {defaultGroupFile.Path}}, ClientGroupsBlock: map[string][]string{ "default": {"gr1"}, }, @@ -555,7 +557,7 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() { sutConfig = config.BlockingConfig{ BlockType: "ZEROIP", BlockTTL: config.Duration(time.Minute), - BlackLists: map[string][]string{"gr1": {group1File.Name()}}, + BlackLists: map[string][]string{"gr1": {group1File.Path}}, ClientGroupsBlock: map[string][]string{ "default": {"gr1"}, }, @@ -590,8 +592,8 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() { BeforeEach(func() { sutConfig = config.BlockingConfig{ BlackLists: map[string][]string{ - "defaultGroup": {defaultGroupFile.Name()}, - "group1": {group1File.Name()}, + "defaultGroup": {defaultGroupFile.Path}, + "group1": {group1File.Path}, }, ClientGroupsBlock: map[string][]string{ "default": {"defaultGroup", "group1"}, @@ -821,7 +823,7 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() { sutConfig = config.BlockingConfig{ BlockType: "ZEROIP", BlockTTL: config.Duration(time.Minute), - BlackLists: map[string][]string{"gr1": {group1File.Name()}}, + BlackLists: map[string][]string{"gr1": {group1File.Path}}, ClientGroupsBlock: map[string][]string{ "default": {"gr1"}, }, @@ -856,14 +858,13 @@ var _ = Describe("BlockingResolver", Label("blockingResolver"), func() { MatchError("unknown blockType 'wrong', please use one of: ZeroIP, NxDomain or specify destination IP address(es)")) }) }) - When("failStartOnListError is active", func() { - + When("startStrategy is failOnError", func() { It("should fail if lists can't be downloaded", func() { _, err := NewBlockingResolver(config.BlockingConfig{ - BlackLists: map[string][]string{"gr1": {"wrongPath"}}, - WhiteLists: map[string][]string{"whitelist": {"wrongPath"}}, - FailStartOnListError: true, - BlockType: "zeroIp", + BlackLists: map[string][]string{"gr1": {"wrongPath"}}, + WhiteLists: map[string][]string{"whitelist": {"wrongPath"}}, + StartStrategy: config.StartStrategyTypeFailOnError, + BlockType: "zeroIp", }, nil, skipUpstreamCheck) Expect(err).Should(HaveOccurred()) }) diff --git a/resolver/bootstrap.go b/resolver/bootstrap.go index 5e11a198..14f13d97 100644 --- a/resolver/bootstrap.go +++ b/resolver/bootstrap.go @@ -26,7 +26,8 @@ var ( // Bootstrap allows resolving hostnames using the configured bootstrap DNS. type Bootstrap struct { - log *logrus.Entry + log *logrus.Entry + startVerifyUpstream bool resolver Resolver upstream Resolver // the upstream that's part of the above resolver @@ -64,9 +65,10 @@ func NewBootstrap(cfg *config.Config) (b *Bootstrap, err error) { // This also prevents the GC to clean up these two structs, but is not currently an // issue since they stay allocated until the process terminates b = &Bootstrap{ - log: log, - upstreamIPs: ips, - systemResolver: net.DefaultResolver, // allow replacing it during tests + log: log, + upstreamIPs: ips, + systemResolver: net.DefaultResolver, // allow replacing it during tests + startVerifyUpstream: cfg.StartVerifyUpstream, } if upstream.IsDefault() { @@ -96,26 +98,18 @@ func (b *Bootstrap) UpstreamIPs(r *UpstreamResolver) (*IPSet, error) { func (b *Bootstrap) resolveUpstream(r Resolver, host string) ([]net.IP, error) { // Use system resolver if no bootstrap is configured if b.resolver == nil { - filteredQTypes := config.GetConfig().Filtering.QueryTypes - - network := "ip" - if filteredQTypes.Contains(dns.Type(dns.TypeAAAA)) { - network = "ip4" - } else if filteredQTypes.Contains(dns.Type(dns.TypeA)) { - network = "ip6" - } - + cfg := config.GetConfig() ctx := context.Background() - timeout := config.GetConfig().UpstreamTimeout + timeout := cfg.UpstreamTimeout if timeout != 0 { var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(context.Background(), time.Duration(timeout)) + ctx, cancel = context.WithTimeout(ctx, time.Duration(timeout)) defer cancel() } - return b.systemResolver.LookupIP(ctx, network, host) + return b.systemResolver.LookupIP(ctx, cfg.ConnectIPVersion.Net(), host) } if r == b.upstream { @@ -145,14 +139,16 @@ func (b *Bootstrap) NewHTTPTransport() *http.Transport { return nil, err } - filteredQTypes := config.GetConfig().Filtering.QueryTypes + connectIPVersion := config.GetConfig().ConnectIPVersion var qTypes []dns.Type switch { - case strings.HasSuffix(network, "4") || filteredQTypes.Contains(dns.Type(dns.TypeAAAA)): + case connectIPVersion != config.IPVersionDual: // ignore `network` if a specific version is configured + qTypes = connectIPVersion.QTypes() + case strings.HasSuffix(network, "4"): qTypes = []dns.Type{dns.Type(dns.TypeA)} - case strings.HasSuffix(network, "6") || filteredQTypes.Contains(dns.Type(dns.TypeA)): + case strings.HasSuffix(network, "6"): qTypes = []dns.Type{dns.Type(dns.TypeAAAA)} default: qTypes = v4v6QTypes diff --git a/resolver/caching_resolver.go b/resolver/caching_resolver.go index 25c88fa8..5d81a2f9 100644 --- a/resolver/caching_resolver.go +++ b/resolver/caching_resolver.go @@ -112,7 +112,7 @@ func (r *CachingResolver) onExpired(cacheKey string) (val interface{}, ttl time. if response.Res.Rcode == dns.RcodeSuccess { evt.Bus().Publish(evt.CachingDomainPrefetched, domainName) - return cacheValue{response.Res.Answer, true}, time.Duration(r.adjustTTLs(response.Res.Answer)) * time.Second + return cacheValue{response.Res.Answer, true}, r.adjustTTLs(response.Res.Answer) } } else { util.LogOnError(fmt.Sprintf("can't prefetch '%s' ", domainName), err) @@ -232,7 +232,7 @@ func (r *CachingResolver) putInCache(cacheKey string, response *model.Response, if response.Res.Rcode == dns.RcodeSuccess { // put value into cache - r.resultCache.Put(cacheKey, cacheValue{answer, prefetch}, time.Duration(r.adjustTTLs(answer))*time.Second) + r.resultCache.Put(cacheKey, cacheValue{answer, prefetch}, r.adjustTTLs(answer)) } else if response.Res.Rcode == dns.RcodeNameError { if r.cacheTimeNegative > 0 { // put return code if NXDOMAIN @@ -249,7 +249,16 @@ func (r *CachingResolver) putInCache(cacheKey string, response *model.Response, } } -func (r *CachingResolver) adjustTTLs(answer []dns.RR) (maxTTL uint32) { +// adjustTTLs calculates and returns the max TTL (considers also the min and max cache time) +// for all records from answer or a negative cache time for empty answer +// adjust the TTL in the answer header accordingly +func (r *CachingResolver) adjustTTLs(answer []dns.RR) (maxTTL time.Duration) { + var max uint32 + + if len(answer) == 0 { + return r.cacheTimeNegative + } + for _, a := range answer { // if TTL < mitTTL -> adjust the value, set minTTL if r.minCacheTimeSec > 0 { @@ -264,10 +273,10 @@ func (r *CachingResolver) adjustTTLs(answer []dns.RR) (maxTTL uint32) { } } - if maxTTL < a.Header().Ttl { - maxTTL = a.Header().Ttl + if max < a.Header().Ttl { + max = a.Header().Ttl } } - return + return time.Duration(max) * time.Second } diff --git a/resolver/caching_resolver_test.go b/resolver/caching_resolver_test.go index 718add94..18ab2410 100644 --- a/resolver/caching_resolver_test.go +++ b/resolver/caching_resolver_test.go @@ -355,66 +355,98 @@ var _ = Describe("CachingResolver", func() { }) Describe("Negative cache (caching if upstream resolver returns NXDOMAIN)", func() { - When("Upstream resolver returns NXDOMAIN with caching", func() { - BeforeEach(func() { - mockAnswer.Rcode = dns.RcodeNameError - }) - - It("response should be cached", func() { - By("first request", func() { - resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA))) - Expect(err).Should(Succeed()) - Expect(resp.RType).Should(Equal(ResponseTypeRESOLVED)) - Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError)) - Expect(m.Calls).Should(HaveLen(1)) + Context("Caching if upstream resolver returns NXDOMAIN", func() { + When("Upstream resolver returns NXDOMAIN with caching", func() { + BeforeEach(func() { + mockAnswer.Rcode = dns.RcodeNameError }) - By("second request", func() { - Eventually(func(g Gomega) { + It("response should be cached", func() { + By("first request", func() { resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA))) - g.Expect(err).Should(Succeed()) - g.Expect(resp.RType).Should(Equal(ResponseTypeCACHED)) - g.Expect(resp.Reason).Should(Equal("CACHED NEGATIVE")) - g.Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError)) - // still one call to resolver - g.Expect(m.Calls).Should(HaveLen(1)) - }, "500ms").Should(Succeed()) + Expect(err).Should(Succeed()) + Expect(resp.RType).Should(Equal(ResponseTypeRESOLVED)) + Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError)) + Expect(m.Calls).Should(HaveLen(1)) + }) + + By("second request", func() { + Eventually(func(g Gomega) { + resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA))) + g.Expect(err).Should(Succeed()) + g.Expect(resp.RType).Should(Equal(ResponseTypeCACHED)) + g.Expect(resp.Reason).Should(Equal("CACHED NEGATIVE")) + g.Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError)) + // still one call to resolver + g.Expect(m.Calls).Should(HaveLen(1)) + }, "500ms").Should(Succeed()) + }) + }) + + }) + When("Upstream resolver returns NXDOMAIN without caching", func() { + BeforeEach(func() { + mockAnswer.Rcode = dns.RcodeNameError + sutConfig = config.CachingConfig{ + CacheTimeNegative: config.Duration(time.Minute * -1), + } + }) + + It("response shouldn't be cached", func() { + By("first request", func() { + resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA))) + Expect(err).Should(Succeed()) + Expect(resp.RType).Should(Equal(ResponseTypeRESOLVED)) + Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError)) + Expect(m.Calls).Should(HaveLen(1)) + }) + + By("second request", func() { + Eventually(func(g Gomega) { + resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA))) + g.Expect(err).Should(Succeed()) + g.Expect(resp.RType).Should(Equal(ResponseTypeRESOLVED)) + g.Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError)) + g.Expect(m.Calls).Should(HaveLen(2)) + }, "500ms").Should(Succeed()) + }) }) }) - }) - When("Upstream resolver returns NXDOMAIN without caching", func() { - BeforeEach(func() { - mockAnswer.Rcode = dns.RcodeNameError - sutConfig = config.CachingConfig{ - CacheTimeNegative: config.Duration(time.Minute * -1), - } - }) - - It("response shouldn't be cached", func() { - By("first request", func() { - resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA))) - Expect(err).Should(Succeed()) - Expect(resp.RType).Should(Equal(ResponseTypeRESOLVED)) - Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError)) - Expect(m.Calls).Should(HaveLen(1)) + Context("Caching if upstream resolver returns empty result", func() { + When("Upstream resolver returns empty result with caching", func() { + BeforeEach(func() { + mockAnswer.Rcode = dns.RcodeSuccess + mockAnswer.Answer = make([]dns.RR, 0) }) - By("second request", func() { - Eventually(func(g Gomega) { + It("response should be cached", func() { + By("first request", func() { resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA))) - g.Expect(err).Should(Succeed()) - g.Expect(resp.RType).Should(Equal(ResponseTypeRESOLVED)) - g.Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError)) - g.Expect(m.Calls).Should(HaveLen(2)) - }, "500ms").Should(Succeed()) - }) - }) + Expect(err).Should(Succeed()) + Expect(resp.RType).Should(Equal(ResponseTypeRESOLVED)) + Expect(resp.Res.Rcode).Should(Equal(dns.RcodeSuccess)) + Expect(m.Calls).Should(HaveLen(1)) + }) + By("second request", func() { + Eventually(func(g Gomega) { + resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA))) + g.Expect(err).Should(Succeed()) + g.Expect(resp.RType).Should(Equal(ResponseTypeCACHED)) + g.Expect(resp.Reason).Should(Equal("CACHED")) + g.Expect(resp.Res.Rcode).Should(Equal(dns.RcodeSuccess)) + // still one call to resolver + g.Expect(m.Calls).Should(HaveLen(1)) + }, "500ms").Should(Succeed()) + }) + }) + + }) }) }) - Describe("Not A / AAAA queries should also cached", func() { + Describe("Not A / AAAA queries should also be cached", func() { When("MX query will be performed", func() { BeforeEach(func() { mockAnswer, _ = util.NewMsgWithAnswer("google.de.", 180, dns.Type(dns.TypeMX), "10 alt1.aspmx.l.google.com.") diff --git a/resolver/custom_dns_resolver_test.go b/resolver/custom_dns_resolver_test.go index 5de9684e..4e93ca05 100644 --- a/resolver/custom_dns_resolver_test.go +++ b/resolver/custom_dns_resolver_test.go @@ -129,8 +129,8 @@ var _ = Describe("CustomDNSResolver", func() { Expect(resp.Res.Rcode).Should(Equal(dns.RcodeSuccess)) Expect(resp.Res.Answer).Should(HaveLen(2)) Expect(resp.Res.Answer).Should(ContainElements( - BeDNSRecord("multiple.ips.", dns.TypeA, TTL, "192.168.143.123")), - BeDNSRecord("multiple.ips.", dns.TypeA, TTL, "192.168.143.125")) + BeDNSRecord("multiple.ips.", dns.TypeA, TTL, "192.168.143.123"), + BeDNSRecord("multiple.ips.", dns.TypeA, TTL, "192.168.143.125"))) // will not delegate to next resolver m.AssertNotCalled(GinkgoT(), "Resolve", mock.Anything) }) @@ -157,9 +157,9 @@ var _ = Describe("CustomDNSResolver", func() { Expect(resp.Res.Answer).Should(HaveLen(2)) Expect(resp.Res.Answer).Should(ContainElements( BeDNSRecord("4.3.3.7.0.7.3.0.e.2.a.8.0.0.0.0.0.0.0.0.3.a.5.8.8.b.d.0.1.0.0.2.ip6.arpa.", - dns.TypePTR, TTL, "ip6.domain.")), + dns.TypePTR, TTL, "ip6.domain."), BeDNSRecord("4.3.3.7.0.7.3.0.e.2.a.8.0.0.0.0.0.0.0.0.3.a.5.8.8.b.d.0.1.0.0.2.ip6.arpa.", - dns.TypePTR, TTL, "multiple.ips.")) + dns.TypePTR, TTL, "multiple.ips."))) // will not delegate to next resolver m.AssertNotCalled(GinkgoT(), "Resolve", mock.Anything) }) diff --git a/resolver/ede_resolver.go b/resolver/ede_resolver.go new file mode 100644 index 00000000..ee7076e2 --- /dev/null +++ b/resolver/ede_resolver.go @@ -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 + } +} diff --git a/resolver/ede_resolver_test.go b/resolver/ede_resolver_test.go new file mode 100644 index 00000000..ab0ba098 --- /dev/null +++ b/resolver/ede_resolver_test.go @@ -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")) + }) + }) +}) diff --git a/resolver/fqdn_only_resolver.go b/resolver/fqdn_only_resolver.go new file mode 100644 index 00000000..040ea6c2 --- /dev/null +++ b/resolver/fqdn_only_resolver.go @@ -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 +} diff --git a/resolver/fqdn_only_resolver_test.go b/resolver/fqdn_only_resolver_test.go new file mode 100644 index 00000000..11243db5 --- /dev/null +++ b/resolver/fqdn_only_resolver_test.go @@ -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")) + }) + }) +}) diff --git a/resolver/hosts_file_resolver.go b/resolver/hosts_file_resolver.go index ee9e87c1..88e09242 100644 --- a/resolver/hosts_file_resolver.go +++ b/resolver/hosts_file_resolver.go @@ -14,16 +14,23 @@ import ( "github.com/sirupsen/logrus" ) +//nolint:gochecknoglobals +var ( + _, loopback4, _ = net.ParseCIDR("127.0.0.0/8") + loopback6 = net.ParseIP("::1") +) + const ( hostsFileResolverLogger = "hosts_file_resolver" ) type HostsFileResolver struct { NextResolver - HostsFilePath string - hosts []host - ttl uint32 - refreshPeriod time.Duration + HostsFilePath string + hosts []host + ttl uint32 + refreshPeriod time.Duration + filterLoopback bool } func (r *HostsFileResolver) handleReverseDNS(request *model.Request) *model.Response { @@ -119,6 +126,7 @@ func (r *HostsFileResolver) Configuration() (result []string) { result = append(result, fmt.Sprintf("hosts file path: %s", r.HostsFilePath)) result = append(result, fmt.Sprintf("hosts TTL: %d", r.ttl)) result = append(result, fmt.Sprintf("hosts refresh period: %s", r.refreshPeriod.String())) + result = append(result, fmt.Sprintf("filter loopback addresses: %t", r.filterLoopback)) } else { result = []string{"deactivated"} } @@ -128,9 +136,10 @@ func (r *HostsFileResolver) Configuration() (result []string) { func NewHostsFileResolver(cfg config.HostsFileConfig) ChainedResolver { r := HostsFileResolver{ - HostsFilePath: cfg.Filepath, - ttl: uint32(time.Duration(cfg.HostsTTL).Seconds()), - refreshPeriod: time.Duration(cfg.RefreshPeriod), + HostsFilePath: cfg.Filepath, + ttl: uint32(time.Duration(cfg.HostsTTL).Seconds()), + refreshPeriod: time.Duration(cfg.RefreshPeriod), + filterLoopback: cfg.FilterLoopback, } if err := r.parseHostsFile(); err != nil { @@ -150,6 +159,7 @@ type host struct { Aliases []string } +// nolint:funlen func (r *HostsFileResolver) parseHostsFile() error { const minColumnCount = 2 @@ -197,6 +207,11 @@ func (r *HostsFileResolver) parseHostsFile() error { h.IP = net.ParseIP(fields[0]) h.Hostname = fields[1] + // Check if loopback + if r.filterLoopback && (loopback4.Contains(h.IP) || loopback6.Equal(h.IP)) { + continue + } + if len(fields) > minColumnCount { for i := 2; i < len(fields); i++ { h.Aliases = append(h.Aliases, fields[i]) diff --git a/resolver/hosts_file_resolver_test.go b/resolver/hosts_file_resolver_test.go index b971c477..bfb1c707 100644 --- a/resolver/hosts_file_resolver_test.go +++ b/resolver/hosts_file_resolver_test.go @@ -16,19 +16,29 @@ import ( var _ = Describe("HostsFileResolver", func() { var ( - sut *HostsFileResolver - m *MockResolver - err error - resp *Response + sut *HostsFileResolver + m *MockResolver + err error + resp *Response + tmpDir *TmpFolder + tmpFile *TmpFile ) TTL := uint32(time.Now().Second()) BeforeEach(func() { + tmpDir = NewTmpFolder("HostsFileResolver") + Expect(tmpDir.Error).Should(Succeed()) + DeferCleanup(tmpDir.Clean) + + tmpFile = writeHostFile(tmpDir) + Expect(tmpFile.Error).Should(Succeed()) + cfg := config.HostsFileConfig{ - Filepath: "../testdata/hosts.txt", - HostsTTL: config.Duration(time.Duration(TTL) * time.Second), - RefreshPeriod: config.Duration(30 * time.Minute), + Filepath: tmpFile.Path, + HostsTTL: config.Duration(time.Duration(TTL) * time.Second), + RefreshPeriod: config.Duration(30 * time.Minute), + FilterLoopback: true, } sut = NewHostsFileResolver(cfg).(*HostsFileResolver) m = &MockResolver{} @@ -79,8 +89,8 @@ var _ = Describe("HostsFileResolver", func() { When("Hosts file can be located", func() { It("should parse it successfully", func() { - Expect(sut).Should(Not(BeNil())) - Expect(sut.hosts).Should(HaveLen(7)) + Expect(sut).ShouldNot(BeNil()) + Expect(sut.hosts).Should(HaveLen(4)) }) }) @@ -165,7 +175,7 @@ var _ = Describe("HostsFileResolver", func() { When("hosts file is provided", func() { It("should return configuration", func() { c := sut.Configuration() - Expect(c).Should(HaveLen(3)) + Expect(c).Should(HaveLen(4)) }) }) @@ -192,3 +202,21 @@ var _ = Describe("HostsFileResolver", func() { }) }) }) + +func writeHostFile(tmpDir *TmpFolder) *TmpFile { + return tmpDir.CreateStringFile("hosts.txt", + "# Random comment", + "127.0.0.1 localhost", + "127.0.1.1 localhost2 localhost2.local.lan", + "::1 localhost", + "# Two empty lines to follow", + "", + "", + "faaf:faaf:faaf:faaf::1 ipv6host ipv6host.local.lan", + "192.168.2.1 ipv4host ipv4host.local.lan", + "10.0.0.1 router0 router1 router2", + "10.0.0.2 router3 # Another comment", + "10.0.0.3 # Invalid entry", + "300.300.300.300 invalid4 # Invalid IPv4", + "abcd:efgh:ijkl::1 invalid6 # Invalud IPv6") +} diff --git a/resolver/mocks.go b/resolver/mocks.go index 26ab2782..b0412952 100644 --- a/resolver/mocks.go +++ b/resolver/mocks.go @@ -1,7 +1,7 @@ package resolver import ( - "io/ioutil" + "io" "net" "net/http" "net/http/httptest" @@ -101,7 +101,7 @@ func TestBootstrap(response *dns.Msg) *Bootstrap { func TestDOHUpstream(fn func(request *dns.Msg) (response *dns.Msg), reqFn ...func(w http.ResponseWriter)) config.Upstream { server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - body, err := ioutil.ReadAll(r.Body) + body, err := io.ReadAll(r.Body) util.FatalOnError("can't read request: ", err) diff --git a/resolver/parallel_best_resolver.go b/resolver/parallel_best_resolver.go index 431bd28e..759027b8 100644 --- a/resolver/parallel_best_resolver.go +++ b/resolver/parallel_best_resolver.go @@ -10,6 +10,7 @@ import ( "github.com/0xERR0R/blocky/config" "github.com/0xERR0R/blocky/model" "github.com/0xERR0R/blocky/util" + "github.com/miekg/dns" "github.com/mroth/weightedrand" "github.com/sirupsen/logrus" @@ -36,23 +37,56 @@ type requestResponse struct { err error } +// testResolver sends a test query to verify the resolver is reachable and working +func testResolver(r *UpstreamResolver) error { + request := newRequest("github.com.", dns.Type(dns.TypeA)) + + resp, err := r.Resolve(request) + if err != nil || resp.RType != model.ResponseTypeRESOLVED { + return fmt.Errorf("test resolve of upstream server failed: %w", err) + } + + return nil +} + // NewParallelBestResolver creates new resolver instance func NewParallelBestResolver(upstreamResolvers map[string][]config.Upstream, bootstrap *Bootstrap) (Resolver, error) { - s := make(map[string][]*upstreamResolverStatus, len(upstreamResolvers)) + logger := logger("parallel resolver") + s := make(map[string][]*upstreamResolverStatus) for name, res := range upstreamResolvers { - resolvers := make([]*upstreamResolverStatus, len(res)) + var resolvers []*upstreamResolverStatus - for i, u := range res { + var errResolvers int + + for _, u := range res { r, err := NewUpstreamResolver(u, bootstrap) if err != nil { - return nil, err + logger.Warnf("upstream group %s: %v", name, err) + errResolvers++ + + continue } - resolvers[i] = &upstreamResolverStatus{ + if bootstrap != skipUpstreamCheck { + err = testResolver(r) + if err != nil { + logger.Warn(err) + errResolvers++ + } + } + + resolver := &upstreamResolverStatus{ resolver: r, } - resolvers[i].lastErrorTime.Store(time.Unix(0, 0)) + resolver.lastErrorTime.Store(time.Unix(0, 0)) + resolvers = append(resolvers, resolver) + } + + if bootstrap != skipUpstreamCheck { + if bootstrap.startVerifyUpstream && errResolvers == len(res) { + return nil, fmt.Errorf("unable to reach any DNS resolvers configured for resolver group %s", name) + } } s[name] = resolvers diff --git a/resolver/parallel_best_resolver_test.go b/resolver/parallel_best_resolver_test.go index 3c40029d..948b57cb 100644 --- a/resolver/parallel_best_resolver_test.go +++ b/resolver/parallel_best_resolver_test.go @@ -26,6 +26,67 @@ var _ = Describe("ParallelBestResolver", Label("parallelBestResolver"), func() { }) }) + Describe("Some default upstream resolvers cannot be reached", func() { + It("should start normally", func() { + skipUpstreamCheck.startVerifyUpstream = true + + mockUpstream := NewMockUDPUpstreamServer().WithAnswerFn(func(request *dns.Msg) (response *dns.Msg) { + response, _ = util.NewMsgWithAnswer(request.Question[0].Name, 123, dns.Type(dns.TypeA), "123.124.122.122") + + return + }) + defer mockUpstream.Close() + + upstream := map[string][]config.Upstream{ + upstreamDefaultCfgName: { + config.Upstream{ + Host: "wrong", + }, + mockUpstream.Start(), + }, + } + + _, err := NewParallelBestResolver(upstream, skipUpstreamCheck) + Expect(err).Should(Not(HaveOccurred())) + }) + }) + + Describe("All default upstream resolvers cannot be reached", func() { + var ( + upstream map[string][]config.Upstream + b *Bootstrap + ) + + BeforeEach(func() { + b = TestBootstrap(&dns.Msg{MsgHdr: dns.MsgHdr{Rcode: dns.RcodeServerFailure}}) + + upstream = map[string][]config.Upstream{ + upstreamDefaultCfgName: { + config.Upstream{ + Host: "wrong", + }, + config.Upstream{ + Host: "127.0.0.2", + }, + }, + } + }) + + It("should fail to start if strict checking is enabled", func() { + b.startVerifyUpstream = true + + _, err := NewParallelBestResolver(upstream, b) + Expect(err).Should(HaveOccurred()) + }) + + It("should start if strict checking is disabled", func() { + b.startVerifyUpstream = false + + _, err := NewParallelBestResolver(upstream, b) + Expect(err).Should(Not(HaveOccurred())) + }) + }) + Describe("Resolving result from fastest upstream resolver", func() { var ( sut Resolver @@ -310,6 +371,8 @@ var _ = Describe("ParallelBestResolver", Label("parallelBestResolver"), func() { sut Resolver ) BeforeEach(func() { + config.GetConfig().StartVerifyUpstream = false + sut, _ = NewParallelBestResolver(map[string][]config.Upstream{upstreamDefaultCfgName: { {Host: "host1"}, {Host: "host2"}, diff --git a/resolver/query_logging_resolver_test.go b/resolver/query_logging_resolver_test.go index b12f3601..dfaf6a3c 100644 --- a/resolver/query_logging_resolver_test.go +++ b/resolver/query_logging_resolver_test.go @@ -6,11 +6,10 @@ import ( "errors" "fmt" "io" - "io/ioutil" "os" - "path/filepath" "time" + "github.com/0xERR0R/blocky/helpertest" "github.com/0xERR0R/blocky/querylog" "github.com/0xERR0R/blocky/config" @@ -30,7 +29,7 @@ type SlowMockWriter struct { func (m *SlowMockWriter) Write(entry *querylog.LogEntry) { m.entries = append(m.entries, entry) - time.Sleep(time.Millisecond) + time.Sleep(time.Second) } func (m *SlowMockWriter) CleanUp() { @@ -43,29 +42,26 @@ var _ = Describe("QueryLoggingResolver", func() { err error resp *Response m *MockResolver - tmpDir string + tmpDir *helpertest.TmpFolder mockAnswer *dns.Msg ) BeforeEach(func() { mockAnswer = new(dns.Msg) - tmpDir, err = ioutil.TempDir("", "queryLoggingResolver") - Expect(err).Should(Succeed()) + tmpDir = helpertest.NewTmpFolder("queryLoggingResolver") + Expect(tmpDir.Error).Should(Succeed()) + DeferCleanup(tmpDir.Clean) }) JustBeforeEach(func() { sut = NewQueryLoggingResolver(sutConfig).(*QueryLoggingResolver) + DeferCleanup(func() { close(sut.logChan) }) m = &MockResolver{} m.On("Resolve", mock.Anything).Return(&Response{Res: mockAnswer, Reason: "reason"}, nil) sut.Next(m) }) - AfterEach(func() { - Expect(err).Should(Succeed()) - _ = os.RemoveAll(tmpDir) - }) Describe("Process request", func() { - When("Resolver has no configuration", func() { BeforeEach(func() { sutConfig = config.QueryLogConfig{ @@ -83,7 +79,7 @@ var _ = Describe("QueryLoggingResolver", func() { When("Configuration with logging per client", func() { BeforeEach(func() { sutConfig = config.QueryLogConfig{ - Target: tmpDir, + Target: tmpDir.Path, Type: config.QueryLogTypeCsvClient, CreationAttempts: 1, CreationCooldown: config.Duration(time.Millisecond), @@ -106,7 +102,8 @@ var _ = Describe("QueryLoggingResolver", func() { By("check log for client1", func() { Eventually(func(g Gomega) { - csvLines, err := readCsv(filepath.Join(tmpDir, fmt.Sprintf("%s_client1.log", time.Now().Format("2006-01-02")))) + csvLines, err := readCsv(tmpDir.JoinPath( + fmt.Sprintf("%s_client1.log", time.Now().Format("2006-01-02")))) g.Expect(err).Should(Succeed()) g.Expect(csvLines).Should(Not(BeEmpty())) @@ -121,7 +118,7 @@ var _ = Describe("QueryLoggingResolver", func() { By("check log for client2", func() { Eventually(func(g Gomega) { - csvLines, err := readCsv(filepath.Join(tmpDir, + csvLines, err := readCsv(tmpDir.JoinPath( fmt.Sprintf("%s_cl_ient2_test.log", time.Now().Format("2006-01-02")))) g.Expect(err).Should(Succeed()) @@ -138,7 +135,7 @@ var _ = Describe("QueryLoggingResolver", func() { When("Configuration with logging in one file for all clients", func() { BeforeEach(func() { sutConfig = config.QueryLogConfig{ - Target: tmpDir, + Target: tmpDir.Path, Type: config.QueryLogTypeCsv, CreationAttempts: 1, CreationCooldown: config.Duration(time.Millisecond), @@ -159,7 +156,8 @@ var _ = Describe("QueryLoggingResolver", func() { By("check log", func() { Eventually(func(g Gomega) { - csvLines, err := readCsv(filepath.Join(tmpDir, fmt.Sprintf("%s_ALL.log", time.Now().Format("2006-01-02")))) + csvLines, err := readCsv(tmpDir.JoinPath( + fmt.Sprintf("%s_ALL.log", time.Now().Format("2006-01-02")))) g.Expect(err).Should(Succeed()) g.Expect(csvLines).Should(HaveLen(2)) @@ -196,16 +194,13 @@ var _ = Describe("QueryLoggingResolver", func() { mockWriter := &SlowMockWriter{} sut.writer = mockWriter - // run 10000 requests - for i := 0; i < 10000; i++ { - resp, err = sut.Resolve(newRequestWithClient("example.com.", dns.Type(dns.TypeA), "192.168.178.25", "client1")) - Expect(err).Should(Succeed()) - } + Eventually(func() int { + _, ierr := sut.Resolve(newRequestWithClient("example.com.", dns.Type(dns.TypeA), "192.168.178.25", "client1")) + Expect(ierr).Should(Succeed()) - // log channel is full - Expect(sut.logChan).Should(Not(BeEmpty())) + return len(sut.logChan) + }, "20s", "1µs").Should(Equal(cap(sut.logChan))) }) - }) }) @@ -213,7 +208,7 @@ var _ = Describe("QueryLoggingResolver", func() { When("resolver is enabled", func() { BeforeEach(func() { sutConfig = config.QueryLogConfig{ - Target: tmpDir, + Target: tmpDir.Path, Type: config.QueryLogTypeCsvClient, LogRetentionDays: 0, CreationAttempts: 1, @@ -229,81 +224,81 @@ var _ = Describe("QueryLoggingResolver", func() { Describe("Clean up of query log directory", func() { When("fallback logger is enabled, log retention is enabled", func() { - It("should do nothing", func() { - - sut := NewQueryLoggingResolver(config.QueryLogConfig{ + BeforeEach(func() { + sutConfig = config.QueryLogConfig{ LogRetentionDays: 7, Type: config.QueryLogTypeConsole, CreationAttempts: 1, CreationCooldown: config.Duration(time.Millisecond), - }).(*QueryLoggingResolver) - + } + }) + It("should do nothing", func() { sut.doCleanUp() }) }) When("log directory contains old files", func() { - It("should remove files older than defined log retention", func() { - - // create 2 files, 7 and 8 days old - dateBefore7Days := time.Now().AddDate(0, 0, -7) - dateBefore8Days := time.Now().AddDate(0, 0, -8) - - f1, err := os.Create(filepath.Join(tmpDir, fmt.Sprintf("%s-test.log", dateBefore7Days.Format("2006-01-02")))) - Expect(err).Should(Succeed()) - - f2, err := os.Create(filepath.Join(tmpDir, fmt.Sprintf("%s-test.log", dateBefore8Days.Format("2006-01-02")))) - Expect(err).Should(Succeed()) - - sut := NewQueryLoggingResolver(config.QueryLogConfig{ - Target: tmpDir, + BeforeEach(func() { + sutConfig = config.QueryLogConfig{ + Target: tmpDir.Path, Type: config.QueryLogTypeCsv, LogRetentionDays: 7, CreationAttempts: 1, CreationCooldown: config.Duration(time.Millisecond), - }) + } + }) + It("should remove files older than defined log retention", func() { + // create 2 files, 7 and 8 days old + dateBefore7Days := time.Now().AddDate(0, 0, -7) + dateBefore9Days := time.Now().AddDate(0, 0, -9) - sut.(*QueryLoggingResolver).doCleanUp() + f1 := tmpDir.CreateEmptyFile(fmt.Sprintf("%s-test.log", dateBefore7Days.Format("2006-01-02"))) + Expect(f1.Error).Should(Succeed()) - // file 1 exist - _, err = os.Stat(f1.Name()) - Expect(err).Should(Succeed()) + f2 := tmpDir.CreateEmptyFile(fmt.Sprintf("%s-test.log", dateBefore9Days.Format("2006-01-02"))) + Expect(f2.Error).Should(Succeed()) - // file 2 was deleted - _, err = os.Stat(f2.Name()) - Expect(err).Should(HaveOccurred()) - Expect(os.IsNotExist(err)).Should(BeTrue()) + sut.doCleanUp() + + Eventually(func(g Gomega) { + // file 1 exist + g.Expect(f1.Stat()).Should(Succeed()) + + // file 2 was deleted + ierr2 := f2.Stat() + g.Expect(ierr2).Should(HaveOccurred()) + g.Expect(os.IsNotExist(ierr2)).Should(BeTrue()) + }).Should(Succeed()) }) }) }) -}) - -var _ = Describe("Wrong target configuration", func() { - When("mysql database path is wrong", func() { - It("should use fallback", func() { - sutConfig := config.QueryLogConfig{ - Target: "dummy", - Type: config.QueryLogTypeMysql, - CreationAttempts: 1, - CreationCooldown: config.Duration(time.Millisecond), - } - resolver := NewQueryLoggingResolver(sutConfig) - loggingResolver := resolver.(*QueryLoggingResolver) - Expect(loggingResolver.logType).Should(Equal(config.QueryLogTypeConsole)) + Describe("Wrong target configuration", func() { + When("mysql database path is wrong", func() { + BeforeEach(func() { + sutConfig = config.QueryLogConfig{ + Target: "dummy", + Type: config.QueryLogTypeMysql, + CreationAttempts: 1, + CreationCooldown: config.Duration(time.Millisecond), + } + }) + It("should use fallback", func() { + Expect(sut.logType).Should(Equal(config.QueryLogTypeConsole)) + }) }) - }) - When("postgresql database path is wrong", func() { - It("should use fallback", func() { - sutConfig := config.QueryLogConfig{ - Target: "dummy", - Type: config.QueryLogTypePostgresql, - CreationAttempts: 1, - CreationCooldown: config.Duration(time.Millisecond), - } - resolver := NewQueryLoggingResolver(sutConfig) - loggingResolver := resolver.(*QueryLoggingResolver) - Expect(loggingResolver.logType).Should(Equal(config.QueryLogTypeConsole)) + When("postgresql database path is wrong", func() { + BeforeEach(func() { + sutConfig = config.QueryLogConfig{ + Target: "dummy", + Type: config.QueryLogTypePostgresql, + CreationAttempts: 1, + CreationCooldown: config.Duration(time.Millisecond), + } + }) + It("should use fallback", func() { + Expect(sut.logType).Should(Equal(config.QueryLogTypeConsole)) + }) }) }) }) @@ -315,6 +310,7 @@ func readCsv(file string) ([][]string, error) { if err != nil { return nil, err } + defer csvFile.Close() reader := csv.NewReader(bufio.NewReader(csvFile)) reader.Comma = '\t' diff --git a/resolver/resolver.go b/resolver/resolver.go index c297db8c..5848cbb8 100644 --- a/resolver/resolver.go +++ b/resolver/resolver.go @@ -40,6 +40,23 @@ func newRequestWithClient(question string, rType dns.Type, ip string, clientName } } +// newResponseMsg creates a new dns.Msg as response for a request +func newResponseMsg(request *model.Request) *dns.Msg { + response := new(dns.Msg) + response.SetReply(request.Req) + + return response +} + +// returnResponseModel wrapps a dns.Msg into a model.Response +func returnResponseModel(response *dns.Msg, rtype model.ResponseType, reason string) (*model.Response, error) { + return &model.Response{ + Res: response, + RType: rtype, + Reason: reason, + }, nil +} + func newRequestWithClientID(question string, rType dns.Type, ip string, requestClientID string) *model.Request { return &model.Request{ ClientIP: net.ParseIP(ip), diff --git a/resolver/rewriter_resolver.go b/resolver/rewriter_resolver.go index e0546df0..b8425818 100644 --- a/resolver/rewriter_resolver.go +++ b/resolver/rewriter_resolver.go @@ -18,8 +18,9 @@ import ( // yield a result, the normal resolving is continued. type RewriterResolver struct { NextResolver - rewrite map[string]string - inner Resolver + rewrite map[string]string + inner Resolver + fallbackUpstream bool } func NewRewriterResolver(cfg config.RewriteConfig, inner ChainedResolver) ChainedResolver { @@ -34,8 +35,9 @@ func NewRewriterResolver(cfg config.RewriteConfig, inner ChainedResolver) Chaine inner.Next(NewNoOpResolver()) return &RewriterResolver{ - rewrite: cfg.Rewrite, - inner: inner, + rewrite: cfg.Rewrite, + inner: inner, + fallbackUpstream: cfg.FallbackUpstream, } } @@ -70,13 +72,23 @@ func (r *RewriterResolver) Resolve(request *model.Request) (*model.Response, err logger.WithField("resolver", Name(r.inner)).Trace("go to inner resolver") response, err := r.inner.Resolve(request) - if err != nil { - return response, err - } + // Test for error after checking for fallbackUpstream // Revert the request: must be done before calling r.next request.Req = original + fallbackCondition := err != nil || (response != NoResponse && response.Res.Answer == nil) + if r.fallbackUpstream && fallbackCondition { + // Inner resolver had no answer, configuration requests fallback, continue with the normal chain + logger.WithField("next_resolver", Name(r.next)).Trace("fallback to next resolver") + + return r.next.Resolve(request) + } + + if err != nil { + return response, err + } + if response == NoResponse { // Inner resolver had no response, continue with the normal chain logger.WithField("next_resolver", Name(r.next)).Trace("go to next resolver") diff --git a/resolver/rewriter_resolver_test.go b/resolver/rewriter_resolver_test.go index 3f1a605b..04a5147d 100644 --- a/resolver/rewriter_resolver_test.go +++ b/resolver/rewriter_resolver_test.go @@ -11,6 +11,11 @@ import ( "github.com/stretchr/testify/mock" ) +const ( + sampleOriginal = "test.original." + sampleRewritten = "test.rewritten." +) + var _ = Describe("RewriterResolver", func() { var ( sut ChainedResolver @@ -53,6 +58,11 @@ var _ = Describe("RewriterResolver", func() { When("has rewrite", func() { var request *model.Request + var expectNilAnswer bool + + BeforeEach(func() { + expectNilAnswer = false + }) AfterEach(func() { request = newRequest(fqdnOriginal, dns.Type(dns.TypeA)) @@ -80,13 +90,17 @@ var _ = Describe("RewriterResolver", func() { Expect(err).Should(Succeed()) if resp != mNextResponse { Expect(resp.Res.Question[0].Name).Should(Equal(fqdnOriginal)) - Expect(resp.Res.Answer[0].Header().Name).Should(Equal(fqdnOriginal)) + if expectNilAnswer { + Expect(resp.Res.Answer).Should(BeEmpty()) + } else { + Expect(resp.Res.Answer[0].Header().Name).Should(Equal(fqdnOriginal)) + } } }) It("should modify names", func() { - fqdnOriginal = "test.original." - fqdnRewritten = "test.rewritten." + fqdnOriginal = sampleOriginal + fqdnRewritten = sampleRewritten }) It("should modify subdomains", func() { @@ -105,8 +119,9 @@ var _ = Describe("RewriterResolver", func() { }) It("should call next resolver", func() { - fqdnOriginal = "test.original." - fqdnRewritten = "test.rewritten." + fqdnOriginal = sampleOriginal + fqdnRewritten = sampleRewritten + expectNilAnswer = true // Make inner call the NoOpResolver mInner.ResolveFn = func(req *model.Request) (*model.Response, error) { @@ -126,6 +141,54 @@ var _ = Describe("RewriterResolver", func() { return mNextResponse, nil } }) + + It("should not call next resolver", func() { + fqdnOriginal = sampleOriginal + fqdnRewritten = sampleRewritten + expectNilAnswer = true + + // Make inner return a nil Answer but not an empty Response + mInner.ResolveFn = func(req *model.Request) (*model.Response, error) { + Expect(req).Should(Equal(request)) + + // Inner should see fqdnRewritten + Expect(req.Req.Question[0].Name).Should(Equal(fqdnRewritten)) + + return &model.Response{Res: &dns.Msg{Question: req.Req.Question, Answer: nil}}, nil + } + + // Resolver after RewriterResolver should not be called `fqdnOriginal` + mNext.AssertNotCalled(GinkgoT(), "Resolve", mock.Anything) + }) + + When("has fallbackUpstream", func() { + BeforeEach(func() { + sutConfig.FallbackUpstream = true + }) + + It("should call next resolver", func() { + fqdnOriginal = sampleOriginal + fqdnRewritten = sampleRewritten + + // Make inner return a nil Answer but not an empty Response + mInner.ResolveFn = func(req *model.Request) (*model.Response, error) { + Expect(req).Should(Equal(request)) + + // Inner should see fqdnRewritten + Expect(req.Req.Question[0].Name).Should(Equal(fqdnRewritten)) + + return &model.Response{Res: &dns.Msg{Question: req.Req.Question, Answer: nil}}, nil + } + + // Resolver after RewriterResolver should see `fqdnOriginal` + mNext.On("Resolve", mock.Anything) + mNext.ResolveFn = func(req *model.Request) (*model.Response, error) { + Expect(req.Req.Question[0].Name).Should(Equal(fqdnOriginal)) + + return mNextResponse, nil + } + }) + }) }) Describe("Configuration output", func() { diff --git a/resolver/sudn_resolver.go b/resolver/sudn_resolver.go new file mode 100644 index 00000000..4ca48024 --- /dev/null +++ b/resolver/sudn_resolver.go @@ -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") +} diff --git a/resolver/sudn_resolver_test.go b/resolver/sudn_resolver_test.go new file mode 100644 index 00000000..3ee64f93 --- /dev/null +++ b/resolver/sudn_resolver_test.go @@ -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)) + }) + }) +}) diff --git a/resolver/upstream_resolver.go b/resolver/upstream_resolver.go index 544fddf5..1635f0c5 100644 --- a/resolver/upstream_resolver.go +++ b/resolver/upstream_resolver.go @@ -5,7 +5,7 @@ import ( "crypto/tls" "errors" "fmt" - "io/ioutil" + "io" "net" "net/http" "strconv" @@ -53,15 +53,21 @@ type dnsUpstreamClient struct { type httpUpstreamClient struct { client *http.Client + host string } func createUpstreamClient(cfg config.Upstream) upstreamClient { timeout := time.Duration(config.GetConfig().UpstreamTimeout) + tlsConfig := tls.Config{ ServerName: cfg.Host, MinVersion: tls.VersionTLS12, } + if cfg.CommonName != "" { + tlsConfig.ServerName = cfg.CommonName + } + switch cfg.Net { case config.NetProtocolHttps: return &httpUpstreamClient{ @@ -73,6 +79,7 @@ func createUpstreamClient(cfg config.Upstream) upstreamClient { }, Timeout: timeout, }, + host: cfg.Host, } case config.NetProtocolTcpTls: @@ -106,7 +113,7 @@ func createUpstreamClient(cfg config.Upstream) upstreamClient { } func (r *httpUpstreamClient) fmtURL(ip net.IP, port uint16, path string) string { - return fmt.Sprintf("https://%s:%d%s", ip.String(), port, path) + return fmt.Sprintf("https://%s%s", net.JoinHostPort(ip.String(), strconv.Itoa(int(port))), path) } func (r *httpUpstreamClient) callExternal(msg *dns.Msg, @@ -127,6 +134,8 @@ func (r *httpUpstreamClient) callExternal(msg *dns.Msg, req.Header.Set("User-Agent", config.GetConfig().DoHUserAgent) req.Header.Set("Content-Type", dnsContentType) + req.Host = r.host + httpResponse, err := r.client.Do(req) if err != nil { @@ -147,7 +156,7 @@ func (r *httpUpstreamClient) callExternal(msg *dns.Msg, dnsContentType, contentType) } - body, err := ioutil.ReadAll(httpResponse.Body) + body, err := io.ReadAll(httpResponse.Body) if err != nil { return nil, 0, fmt.Errorf("can't read response body: %w", err) } @@ -162,7 +171,7 @@ func (r *httpUpstreamClient) callExternal(msg *dns.Msg, return &response, time.Since(start), nil } -func (r *dnsUpstreamClient) fmtURL(ip net.IP, port uint16, _path string) string { +func (r *dnsUpstreamClient) fmtURL(ip net.IP, port uint16, _ string) string { return net.JoinHostPort(ip.String(), strconv.Itoa(int(port))) } diff --git a/resolver/upstream_resolver_test.go b/resolver/upstream_resolver_test.go index 186ae61c..49b22b20 100644 --- a/resolver/upstream_resolver_test.go +++ b/resolver/upstream_resolver_test.go @@ -205,7 +205,7 @@ var _ = Describe("UpstreamResolver", Label("upstreamResolver"), func() { It("should return error", func() { _, err := sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeA))) Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring("no such host")) + Expect(err.Error()).Should(Or(ContainSubstring("no such host"), ContainSubstring("i/o timeout"))) }) }) }) diff --git a/server/server.go b/server/server.go index 0ad28829..df46f1ba 100644 --- a/server/server.go +++ b/server/server.go @@ -2,12 +2,14 @@ package server import ( "bytes" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" - "crypto/rsa" "crypto/tls" "crypto/x509" "encoding/pem" "fmt" + "math" "math/big" mrand "math/rand" "net" @@ -36,7 +38,6 @@ const ( maxUDPBufferSize = 65535 caExpiryYears = 10 certExpiryYears = 5 - certRSAsize = 4096 ) // Server controls the endpoints for DNS and HTTP @@ -295,7 +296,7 @@ func createUDPServer(address string) (*dns.Server, error) { func createSelfSignedCert() (tls.Certificate, error) { // Create CA ca := &x509.Certificate{ - SerialNumber: big.NewInt(int64(mrand.Intn(certRSAsize))), //nolint:gosec + SerialNumber: big.NewInt(int64(mrand.Intn(math.MaxInt))), //nolint:gosec NotBefore: time.Now(), NotAfter: time.Now().AddDate(caExpiryYears, 0, 0), IsCA: true, @@ -304,7 +305,7 @@ func createSelfSignedCert() (tls.Certificate, error) { BasicConstraintsValid: true, } - caPrivKey, err := rsa.GenerateKey(rand.Reader, certRSAsize) + caPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return tls.Certificate{}, err } @@ -323,16 +324,22 @@ func createSelfSignedCert() (tls.Certificate, error) { } caPrivKeyPEM := new(bytes.Buffer) + + b, err := x509.MarshalECPrivateKey(caPrivKey) + if err != nil { + return tls.Certificate{}, err + } + if err = pem.Encode(caPrivKeyPEM, &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey), + Type: "EC PRIVATE KEY", + Bytes: b, }); err != nil { return tls.Certificate{}, err } // Create certificate cert := &x509.Certificate{ - SerialNumber: big.NewInt(int64(mrand.Intn(certRSAsize))), //nolint:gosec + SerialNumber: big.NewInt(int64(mrand.Intn(math.MaxInt))), //nolint:gosec DNSNames: []string{"*"}, NotBefore: time.Now(), NotAfter: time.Now().AddDate(certExpiryYears, 0, 0), @@ -341,7 +348,7 @@ func createSelfSignedCert() (tls.Certificate, error) { KeyUsage: x509.KeyUsageDigitalSignature, } - certPrivKey, err := rsa.GenerateKey(rand.Reader, certRSAsize) + certPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return tls.Certificate{}, err } @@ -360,9 +367,15 @@ func createSelfSignedCert() (tls.Certificate, error) { } certPrivKeyPEM := new(bytes.Buffer) + + b, err = x509.MarshalECPrivateKey(certPrivKey) + if err != nil { + return tls.Certificate{}, err + } + if err = pem.Encode(certPrivKeyPEM, &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey), + Type: "EC PRIVATE KEY", + Bytes: b, }); err != nil { return tls.Certificate{}, err } @@ -397,7 +410,9 @@ func createQueryResolver( r = resolver.Chain( resolver.NewFilteringResolver(cfg.Filtering), + resolver.NewFqdnOnlyResolver(*cfg), clientNamesResolver, + resolver.NewEdeResolver(cfg.Ede), resolver.NewQueryLoggingResolver(cfg.QueryLog), resolver.NewMetricsResolver(cfg.Prometheus), resolver.NewRewriterResolver(cfg.CustomDNS.RewriteConfig, resolver.NewCustomDNSResolver(cfg.CustomDNS)), @@ -405,6 +420,7 @@ func createQueryResolver( blockingResolver, resolver.NewCachingResolver(cfg.Caching, redisClient), resolver.NewRewriterResolver(cfg.Conditional.RewriteConfig, conditionalUpstreamResolver), + resolver.NewSpecialUseDomainNamesResolver(), parallelResolver, ) @@ -467,6 +483,12 @@ func toMB(b uint64) uint64 { return b / bytesInKB / bytesInKB } +const ( + readHeaderTimeout = 20 * time.Second + readTimeout = 20 * time.Second + writeTimeout = 20 * time.Second +) + // Start starts the server func (s *Server) Start(errCh chan<- error) { logger().Info("Starting server") @@ -488,7 +510,14 @@ func (s *Server) Start(errCh chan<- error) { go func() { logger().Infof("http server is up and running on addr/port %s", address) - if err := http.Serve(listener, s.httpMux); err != nil { + srv := &http.Server{ + ReadTimeout: readTimeout, + ReadHeaderTimeout: readHeaderTimeout, + WriteTimeout: writeTimeout, + Handler: s.httpsMux, + } + + if err := srv.Serve(listener); err != nil { errCh <- fmt.Errorf("start http listener failed: %w", err) } }() @@ -502,7 +531,10 @@ func (s *Server) Start(errCh chan<- error) { logger().Infof("https server is up and running on addr/port %s", address) server := http.Server{ - Handler: s.httpsMux, + Handler: s.httpsMux, + ReadTimeout: readTimeout, + ReadHeaderTimeout: readHeaderTimeout, + WriteTimeout: writeTimeout, //nolint:gosec TLSConfig: &tls.Config{ MinVersion: minTLSVersion(), diff --git a/server/server_endpoints.go b/server/server_endpoints.go index 47dc5a74..07c1c772 100644 --- a/server/server_endpoints.go +++ b/server/server_endpoints.go @@ -5,7 +5,7 @@ import ( "encoding/json" "fmt" "html/template" - "io/ioutil" + "io" "net" "net/http" "strings" @@ -25,9 +25,12 @@ import ( ) const ( - dohMessageLimit = 512 - dnsContentType = "application/dns-message" - corsMaxAge = 5 * time.Minute + dohMessageLimit = 512 + contentTypeHeader = "content-type" + dnsContentType = "application/dns-message" + jsonContentType = "application/json" + htmlContentType = "text/html; charset=UTF-8" + corsMaxAge = 5 * time.Minute ) func secureHeader(next http.Handler) http.Handler { @@ -83,7 +86,7 @@ func (s *Server) dohPostRequestHandler(rw http.ResponseWriter, req *http.Request return } - rawMsg, err := ioutil.ReadAll(req.Body) + rawMsg, err := io.ReadAll(req.Body) if err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) @@ -173,6 +176,9 @@ func extractIP(r *http.Request) string { // @Router /query [post] func (s *Server) apiQuery(rw http.ResponseWriter, req *http.Request) { var queryRequest api.QueryRequest + + rw.Header().Set(contentTypeHeader, jsonContentType) + err := json.NewDecoder(req.Body).Decode(&queryRequest) if err != nil { @@ -253,7 +259,7 @@ func createRouter(cfg *config.Config) *chi.Mux { func configureRootHandler(cfg *config.Config, router *chi.Mux) { router.Get("/", func(writer http.ResponseWriter, request *http.Request) { - writer.Header().Set("content-type", dnsContentType) + writer.Header().Set(contentTypeHeader, htmlContentType) t := template.New("index") _, _ = t.Parse(web.IndexTmpl) diff --git a/server/server_test.go b/server/server_test.go index 5b6f02a6..6002a906 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -4,7 +4,7 @@ import ( "bytes" "encoding/base64" "encoding/json" - "io/ioutil" + "io" "net" "net/http" "strings" @@ -61,8 +61,15 @@ var _ = BeforeSuite(func() { DeferCleanup(fritzboxMockUpstream.Close) clientMockUpstream := resolver.NewMockUDPUpstreamServer().WithAnswerFn(func(request *dns.Msg) (response *dns.Msg) { + var clientName string + client := mockClientName.Load() + + if client != nil { + clientName = mockClientName.Load().(string) + } + response, err := util.NewMsgWithAnswer( - util.ExtractDomain(request.Question[0]), 3600, dns.Type(dns.TypePTR), mockClientName.Load().(string), + util.ExtractDomain(request.Question[0]), 3600, dns.Type(dns.TypePTR), clientName, ) Expect(err).Should(Succeed()) @@ -75,6 +82,28 @@ var _ = BeforeSuite(func() { upstreamFritzbox = fritzboxMockUpstream.Start() upstreamGoogle = googleMockUpstream.Start() + tmpDir := NewTmpFolder("server") + Expect(tmpDir.Error).Should(Succeed()) + DeferCleanup(tmpDir.Clean) + + certPem := writeCertPem(tmpDir) + Expect(certPem.Error).Should(Succeed()) + + keyPem := writeKeyPem(tmpDir) + Expect(keyPem.Error).Should(Succeed()) + + doubleclickFile := tmpDir.CreateStringFile("doubleclick.net.txt", "doubleclick.net", "doubleclick.net.cn") + Expect(doubleclickFile.Error).Should(Succeed()) + + bildFile := tmpDir.CreateStringFile("www.bild.de.txt", "www.bild.de") + Expect(bildFile.Error).Should(Succeed()) + + heiseFile := tmpDir.CreateStringFile("heise.de.txt", "heise.de") + Expect(heiseFile.Error).Should(Succeed()) + + youtubeFile := tmpDir.CreateStringFile("youtube.com.txt", "youtube.com") + Expect(youtubeFile.Error).Should(Succeed()) + // create server sut, err = NewServer(&config.Config{ CustomDNS: config.CustomDNSConfig{ @@ -97,13 +126,13 @@ var _ = BeforeSuite(func() { Blocking: config.BlockingConfig{ BlackLists: map[string][]string{ "ads": { - "../testdata/doubleclick.net.txt", - "../testdata/www.bild.de.txt", - "../testdata/heise.de.txt"}, - "youtube": {"../testdata/youtube.com.txt"}}, + doubleclickFile.Path, + bildFile.Path, + heiseFile.Path}, + "youtube": {youtubeFile.Path}}, WhiteLists: map[string][]string{ - "ads": {"../testdata/heise.de.txt"}, - "whitelist": {"../testdata/heise.de.txt"}, + "ads": {heiseFile.Path}, + "whitelist": {heiseFile.Path}, }, ClientGroupsBlock: map[string][]string{ "default": {"ads"}, @@ -123,8 +152,8 @@ var _ = BeforeSuite(func() { DNSPorts: config.ListenConfig{"55555"}, TLSPorts: config.ListenConfig{"8853"}, - CertFile: "../testdata/cert.pem", - KeyFile: "../testdata/key.pem", + CertFile: certPem.Path, + KeyFile: keyPem.Path, HTTPPorts: config.ListenConfig{"4000"}, HTTPSPorts: config.ListenConfig{"4443"}, Prometheus: config.PrometheusConfig{ @@ -290,18 +319,19 @@ var _ = Describe("Running DNS server", func() { Describe("Prometheus endpoint", func() { When("Prometheus URL is called", func() { It("should return prometheus data", func() { - r, err := http.Get("http://localhost:4000/metrics") + resp, err := http.Get("http://localhost:4000/metrics") Expect(err).Should(Succeed()) - Expect(r.StatusCode).Should(Equal(http.StatusOK)) + Expect(resp).Should(HaveHTTPStatus(http.StatusOK)) }) }) }) Describe("Root endpoint", func() { When("Root URL is called", func() { It("should return root page", func() { - r, err := http.Get("http://localhost:4000/") + resp, err := http.Get("http://localhost:4000/") Expect(err).Should(Succeed()) - Expect(r.StatusCode).Should(Equal(http.StatusOK)) + Expect(resp).Should(HaveHTTPStatus(http.StatusOK)) + Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "text/html; charset=UTF-8")) }) }) }) @@ -321,7 +351,8 @@ var _ = Describe("Running DNS server", func() { Expect(err).Should(Succeed()) defer resp.Body.Close() - Expect(resp.StatusCode).Should(Equal(http.StatusOK)) + Expect(resp).Should(HaveHTTPStatus(http.StatusOK)) + Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/json")) var result api.QueryResult err = json.NewDecoder(resp.Body).Decode(&result) @@ -384,7 +415,9 @@ var _ = Describe("Running DNS server", func() { defer resp.Body.Close() Expect(resp).Should(HaveHTTPStatus(http.StatusOK)) - rawMsg, err := ioutil.ReadAll(resp.Body) + Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/dns-message")) + + rawMsg, err := io.ReadAll(resp.Body) Expect(err).Should(Succeed()) msg := new(dns.Msg) @@ -446,7 +479,8 @@ var _ = Describe("Running DNS server", func() { Expect(err).Should(Succeed()) defer resp.Body.Close() Expect(resp).Should(HaveHTTPStatus(http.StatusOK)) - rawMsg, err := ioutil.ReadAll(resp.Body) + Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/dns-message")) + rawMsg, err := io.ReadAll(resp.Body) Expect(err).Should(Succeed()) msg = new(dns.Msg) @@ -465,7 +499,8 @@ var _ = Describe("Running DNS server", func() { Expect(err).Should(Succeed()) defer resp.Body.Close() Expect(resp).Should(HaveHTTPStatus(http.StatusOK)) - rawMsg, err := ioutil.ReadAll(resp.Body) + Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/dns-message")) + rawMsg, err := io.ReadAll(resp.Body) Expect(err).Should(Succeed()) msg = new(dns.Msg) @@ -522,7 +557,7 @@ var _ = Describe("Running DNS server", func() { Expect(cErr).Should(Succeed()) cfg.Upstream.ExternalResolvers = map[string][]config.Upstream{ - "default": {config.Upstream{Net: config.NetProtocolTcpUdp, Host: "4.4.4.4", Port: 53}}} + "default": {config.Upstream{Net: config.NetProtocolTcpUdp, Host: "1.1.1.1", Port: 53}}} cfg.Redis.Address = "test-fail" }) @@ -652,7 +687,7 @@ var _ = Describe("Running DNS server", func() { Expect(cErr).Should(Succeed()) cfg.Upstream.ExternalResolvers = map[string][]config.Upstream{ - "default": {config.Upstream{Net: config.NetProtocolTcpUdp, Host: "4.4.4.4", Port: 53}}} + "default": {config.Upstream{Net: config.NetProtocolTcpUdp, Host: "1.1.1.1", Port: 53}}} }) It("should create self-signed certificate if key/cert files are not provided", func() { @@ -701,3 +736,41 @@ func requestServer(request *dns.Msg) *dns.Msg { return nil } + +func writeCertPem(tmpDir *TmpFolder) *TmpFile { + return tmpDir.CreateStringFile("cert.pem", + "-----BEGIN CERTIFICATE-----", + "MIICMzCCAZygAwIBAgIRAJCCrDTGEtZfRpxDY1KAoswwDQYJKoZIhvcNAQELBQAw", + "EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2", + "MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw", + "gYkCgYEA4mEaF5yWYYrTfMgRXdBpgGnqsHIADQWlw7BIJWD/gNp+fgp4TUZ/7ggV", + "rrvRORvRFjw14avd9L9EFP7XLi8ViU3uoE1UWI32MlrKqLbGNCXyUIApIoqlbRg6", + "iErxIk5+ChzFuysQOx01S2yv/ML6dx7NOGHs1S38MUzRZtcXBH8CAwEAAaOBhjCB", + "gzAOBgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/", + "BAUwAwEB/zAdBgNVHQ4EFgQUslNI6tYIv909RttHaZVMS/u/VYYwLAYDVR0RBCUw", + "I4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3DQEB", + "CwUAA4GBAJ2gRpQHr5Qj7dt26bYVMdN4JGXTsvjbVrJfKI0VfPGJ+SUY/uTVBUeX", + "+Cwv4DFEPBlNx/lzuUkwmRaExC4/w81LWwxe5KltYsjyJuYowiUbLZ6tzLaQ9Bcx", + "jxClAVvgj90TGYOwsv6ESOX7GWteN1FlD3+jk7vefjFagaKKFYR9", + "-----END CERTIFICATE-----") +} + +func writeKeyPem(tmpDir *TmpFolder) *TmpFile { + return tmpDir.CreateStringFile("key.pem", + "-----BEGIN PRIVATE KEY-----", + "MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAOJhGheclmGK03zI", + "EV3QaYBp6rByAA0FpcOwSCVg/4Dafn4KeE1Gf+4IFa670Tkb0RY8NeGr3fS/RBT+", + "1y4vFYlN7qBNVFiN9jJayqi2xjQl8lCAKSKKpW0YOohK8SJOfgocxbsrEDsdNUts", + "r/zC+ncezThh7NUt/DFM0WbXFwR/AgMBAAECgYEA1exixstPhI+2+OTrHFc1S4dL", + "oz+ncqbSlZEBLGl0KWTQQfVM5+FmRR7Yto1/0lLKDBQL6t0J2x3fjWOhHmCaHKZA", + "VAvZ8+OKxwofih3hlO0tGCB8szUJygp2FAmd0rOUqvPQ+PTohZEUXyDaB8MOIbX+", + "qoo7g19+VlbyKqmM8HkCQQDs4GQJwEn7GXKllSMyOfiYnjQM2pwsqO0GivXkH+p3", + "+h5KDp4g3O4EbmbrvZyZB2euVsBjW3pFMu+xPXuOXf91AkEA9KfC7LGLD2OtLmrM", + "iCZAqHlame+uEEDduDmqjTPnNKUWVeRtYKMF5Hltbeo1jMXMSbVZ+fRWKfQ+HAhQ", + "xjFJowJAV6U7PqRoe0FSO1QwXrA2fHnk9nCY4qlqckZObyckAVqJhIteFPjKFNeo", + "u0dAPxsPUOGGc/zwA9Sx/ZmrMuUy1QJBALl7bqawO/Ng6G0mfwZBqgeQaYYHVnnw", + "E6iV353J2eHpvzNDSUFYlyEOhk4soIindSf0m9CK08Be8a+jBkocF+0CQQC+Hi7L", + "kZV1slpW82BxYIhs9Gb0OQgK8SsI4aQPTFGUarQXXAm4eRqBO0kaG+jGX6TtW353", + "EHK784GIxwVXKej/", + "-----END PRIVATE KEY-----") +} diff --git a/testdata/cert.pem b/testdata/cert.pem deleted file mode 100644 index 2234c4d4..00000000 --- a/testdata/cert.pem +++ /dev/null @@ -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----- diff --git a/testdata/config.yml b/testdata/config.yml deleted file mode 100644 index 7b8ae235..00000000 --- a/testdata/config.yml +++ /dev/null @@ -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 diff --git a/testdata/config/config1.yaml b/testdata/config/config1.yaml deleted file mode 100644 index 1a6f1edb..00000000 --- a/testdata/config/config1.yaml +++ /dev/null @@ -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 - \ No newline at end of file diff --git a/testdata/config/config2.yaml b/testdata/config/config2.yaml deleted file mode 100644 index 9e32a730..00000000 --- a/testdata/config/config2.yaml +++ /dev/null @@ -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 diff --git a/testdata/doubleclick.net.txt b/testdata/doubleclick.net.txt deleted file mode 100644 index bf96e9ab..00000000 --- a/testdata/doubleclick.net.txt +++ /dev/null @@ -1,2 +0,0 @@ -doubleclick.net -doubleclick.net.cn \ No newline at end of file diff --git a/testdata/heise.de.txt b/testdata/heise.de.txt deleted file mode 100644 index 5eab4544..00000000 --- a/testdata/heise.de.txt +++ /dev/null @@ -1 +0,0 @@ -heise.de \ No newline at end of file diff --git a/testdata/hosts.txt b/testdata/hosts.txt deleted file mode 100644 index 6836f071..00000000 --- a/testdata/hosts.txt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/testdata/key.pem b/testdata/key.pem deleted file mode 100644 index e8071297..00000000 --- a/testdata/key.pem +++ /dev/null @@ -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----- diff --git a/testdata/www.bild.de.txt b/testdata/www.bild.de.txt deleted file mode 100644 index eaddd548..00000000 --- a/testdata/www.bild.de.txt +++ /dev/null @@ -1 +0,0 @@ -www.bild.de \ No newline at end of file diff --git a/testdata/youtube.com.txt b/testdata/youtube.com.txt deleted file mode 100644 index 7bfa9c11..00000000 --- a/testdata/youtube.com.txt +++ /dev/null @@ -1 +0,0 @@ -youtube.com \ No newline at end of file diff --git a/tools.go b/tools.go new file mode 100644 index 00000000..c87544d0 --- /dev/null +++ b/tools.go @@ -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" +) diff --git a/util/buildinfo.go b/util/buildinfo.go index 0827d623..9001341b 100644 --- a/util/buildinfo.go +++ b/util/buildinfo.go @@ -6,4 +6,6 @@ var ( Version = "undefined" // BuildTime build time of the binary BuildTime = "undefined" + // Architecture current CPU architecture + Architecture = "undefined" ) diff --git a/web/index.go b/web/index.go index ef145b42..afdf38c2 100644 --- a/web/index.go +++ b/web/index.go @@ -3,5 +3,6 @@ package web import _ "embed" // IndexTmpl html template for the start page +// //go:embed index.html var IndexTmpl string